1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
import json
import os
import shutil
from functools import partial
from pathlib import Path
import cairocffi
import pytest
from libqtile.bar import Bar
from libqtile.command.base import expose_command
from libqtile.config import Group, Screen
@pytest.fixture(scope="function")
def vertical(request):
yield getattr(request, "param", False)
vertical_bar = pytest.mark.parametrize("vertical", [True], indirect=True)
@pytest.fixture(scope="session")
def target():
folder = Path(__file__).parent / "screenshots"
docs_folder = (
Path(__file__).parent
/ ".."
/ ".."
/ ".."
/ "docs"
/ "_static"
/ "screenshots"
/ "widgets"
)
log = os.path.join(docs_folder, "shots.json")
if folder.is_dir():
shutil.rmtree(folder)
folder.mkdir()
key = {}
def get_file_name(w_name, config):
nonlocal key
# Convert config into a string of key=value
entry = ", ".join(f"{k}={repr(v)}" for k, v in config.items())
# Check if widget is in the key dict
if w_name not in key:
key[w_name] = {}
# Increment the index number
indexes = [int(x) for x in key[w_name]]
index = max(indexes) + 1 if indexes else 1
# Record the config
key[w_name][index] = entry
# Define the target folder and check it exists
shots_dir = os.path.join(folder, w_name)
if not os.path.isdir(shots_dir):
os.mkdir(shots_dir)
# Returnt the path for the screenshot
return os.path.join(shots_dir, f"{index}.png")
yield get_file_name
# We copy the screenshots from the test folder to the docs folder at the end
# This prevents pytest deleting the files itself
# Remove old screenshots
if os.path.isdir(docs_folder):
shutil.rmtree(docs_folder)
# Copy to the docs folder
shutil.copytree(folder, docs_folder)
with open(log, "w") as f:
json.dump(key, f)
# Clear up the tests folder
shutil.rmtree(folder)
@pytest.fixture
def screenshot_manager(widget, request, manager_nospawn, minimal_conf_noscreen, target, vertical):
"""
Create a manager instance for the screenshots. Individual "tests" should only call
`screenshot_manager.take_screenshot()` but the destination path is also available in
`screenshot_manager.target`.
Widgets should create their own `widget` fixture in the relevant file (applying
monkeypatching etc as necessary).
Configs can then be passed by parametrizing "screenshot_manager".
"""
# Partials are used to hide some aspects of the config from being displayed in the
# docs. We need to split these out into their constituent parts.
if type(widget) is partial:
widget_class = widget.func
widget_config = widget.keywords
else:
widget_class = widget
widget_config = {}
class ScreenshotWidget(widget_class):
def __init__(self, *args, **kwargs):
widget_class.__init__(self, *args, **kwargs)
# We need the widget's name to be the name of the inherited class
self.name = widget_class.__name__.lower()
def _configure(self, bar, screen):
widget_class._configure(self, bar, screen)
# By setting `has_mirrors` to True, the drawer will keep a copy of the latest
# contents in a separate RecordingSurface which we can access for our screenshots.
self.drawer.has_mirrors = True
@expose_command()
def take_screenshot(self, target):
if not self.configured:
return
source = self.drawer.last_surface
dest = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, self.width, self.height)
with cairocffi.Context(dest) as ctx:
ctx.set_source_surface(source)
ctx.paint()
dest.write_to_png(target)
class ScreenshotBar(Bar):
def _configure(self, qtile, screen, **kwargs):
Bar._configure(self, qtile, screen, **kwargs)
# By setting `has_mirrors` to True, the drawer will keep a copy of the latest
# contents in a separate RecordingSurface which we can access for our screenshots.
self.drawer.has_mirrors = True
@expose_command()
def take_screenshot(self, target, x=0, y=0, width=None, height=None):
"""Takes a screenshot of the bar. The area can be selected."""
if not self._configured:
return
if width is None:
width = self.drawer.width
if height is None:
height = self.drawer.height
# Widgets aren't drawn to the bar's drawer so we first need to render them all to a single surface
bar_copy = cairocffi.ImageSurface(
cairocffi.FORMAT_ARGB32, self.drawer.width, self.drawer.height
)
with cairocffi.Context(bar_copy) as ctx:
ctx.set_source_surface(self.drawer.last_surface)
ctx.paint()
for i in self.widgets:
ctx.set_source_surface(i.drawer.last_surface, i.offsetx, i.offsety)
ctx.paint()
# Then we copy the desired area to our destination surface
dest = cairocffi.ImageSurface(cairocffi.FORMAT_ARGB32, width, height)
with cairocffi.Context(dest) as ctx:
ctx.set_source_surface(bar_copy, x=x, y=y)
ctx.paint()
dest.write_to_png(target)
# Get the widget and config
config = getattr(request, "param", dict())
wdgt = ScreenshotWidget(**{**widget_config, **config})
name = wdgt.name
# Create a function to generate filename
def filename():
return target(name, config)
# define bars
position = "left" if vertical else "top"
bar1 = {position: ScreenshotBar([wdgt], 32)}
bar2 = {position: ScreenshotBar([], 32)}
# Add the widget to our config
minimal_conf_noscreen.groups = [Group(i) for i in "123456789"]
minimal_conf_noscreen.fake_screens = [
Screen(**bar1, x=0, y=0, width=300, height=300),
Screen(**bar2, x=0, y=300, width=300, height=300),
]
manager_nospawn.start(minimal_conf_noscreen)
# Add some convenience attributes for taking screenshots
manager_nospawn.target = filename
ss_widget = manager_nospawn.c.widget[name]
manager_nospawn.take_screenshot = lambda f=filename: ss_widget.take_screenshot(f())
yield manager_nospawn
def widget_config(params):
return pytest.mark.parametrize("screenshot_manager", params, indirect=True)
|