1# Copyright (c) 2021 Matt Colligan 2# 3# Permission is hereby granted, free of charge, to any person obtaining a copy 4# of this software and associated documentation files (the "Software"), to deal 5# in the Software without restriction, including without limitation the rights 6# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7# copies of the Software, and to permit persons to whom the Software is 8# furnished to do so, subject to the following conditions: 9# 10# The above copyright notice and this permission notice shall be included in 11# all copies or substantial portions of the Software. 12# 13# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19# SOFTWARE. 20 21from __future__ import annotations 22 23import functools 24import operator 25from typing import TYPE_CHECKING 26 27import cairocffi 28from pywayland.server import Listener 29from wlroots.wlr_types import Texture 30from wlroots.wlr_types.keyboard import KeyboardModifier 31 32from libqtile.log_utils import logger 33from libqtile.utils import QtileError 34 35if TYPE_CHECKING: 36 from typing import Callable, List 37 38 from pywayland.server import Signal 39 40 41class WlrQError(QtileError): 42 pass 43 44 45ModMasks = { 46 "shift": KeyboardModifier.SHIFT, 47 "lock": KeyboardModifier.CAPS, 48 "control": KeyboardModifier.CTRL, 49 "mod1": KeyboardModifier.ALT, 50 "mod2": KeyboardModifier.MOD2, 51 "mod3": KeyboardModifier.MOD3, 52 "mod4": KeyboardModifier.LOGO, 53 "mod5": KeyboardModifier.MOD5, 54} 55 56# from linux/input-event-codes.h 57_KEY_MAX = 0x2ff 58# These are mouse buttons 1-9 59BTN_LEFT = 0x110 60BTN_MIDDLE = 0x112 61BTN_RIGHT = 0x111 62SCROLL_UP = _KEY_MAX + 1 63SCROLL_DOWN = _KEY_MAX + 2 64SCROLL_LEFT = _KEY_MAX + 3 65SCROLL_RIGHT = _KEY_MAX + 4 66BTN_SIDE = 0x113 67BTN_EXTRA = 0x114 68 69buttons = [ 70 BTN_LEFT, 71 BTN_MIDDLE, 72 BTN_RIGHT, 73 SCROLL_UP, 74 SCROLL_DOWN, 75 SCROLL_LEFT, 76 SCROLL_RIGHT, 77 BTN_SIDE, 78 BTN_EXTRA, 79] 80 81# from drm_fourcc.h 82DRM_FORMAT_ARGB8888 = 875713089 83 84 85def translate_masks(modifiers: List[str]) -> int: 86 """ 87 Translate a modifier mask specified as a list of strings into an or-ed 88 bit representation. 89 """ 90 masks = [] 91 for i in modifiers: 92 try: 93 masks.append(ModMasks[i]) 94 except KeyError as e: 95 raise WlrQError("Unknown modifier: %s" % i) from e 96 if masks: 97 return functools.reduce(operator.or_, masks) 98 else: 99 return 0 100 101 102class Painter: 103 def __init__(self, core): 104 self.core = core 105 106 def paint(self, screen, image_path, mode=None): 107 try: 108 with open(image_path, 'rb') as f: 109 image, _ = cairocffi.pixbuf.decode_to_image_surface(f.read()) 110 except IOError as e: 111 logger.error('Wallpaper: %s' % e) 112 return 113 114 surface = cairocffi.ImageSurface( 115 cairocffi.FORMAT_ARGB32, screen.width, screen.height 116 ) 117 with cairocffi.Context(surface) as context: 118 if mode == 'fill': 119 context.rectangle(0, 0, screen.width, screen.height) 120 context.clip() 121 image_w = image.get_width() 122 image_h = image.get_height() 123 width_ratio = screen.width / image_w 124 if width_ratio * image_h >= screen.height: 125 context.scale(width_ratio) 126 else: 127 height_ratio = screen.height / image_h 128 context.translate( 129 - (image_w * height_ratio - screen.width) // 2, 0 130 ) 131 context.scale(height_ratio) 132 elif mode == 'stretch': 133 context.scale( 134 sx=screen.width / image.get_width(), 135 sy=screen.height / image.get_height(), 136 ) 137 context.set_source_surface(image) 138 context.paint() 139 140 stride = surface.format_stride_for_width(cairocffi.FORMAT_ARGB32, screen.width) 141 surface.flush() 142 texture = Texture.from_pixels( 143 self.core.renderer, 144 DRM_FORMAT_ARGB8888, 145 stride, 146 screen.width, 147 screen.height, 148 cairocffi.cairo.cairo_image_surface_get_data(surface._pointer) 149 ) 150 outputs = [output for output in self.core.outputs if output.wlr_output.enabled] 151 outputs[screen.index].wallpaper = texture 152 153 154class HasListeners: 155 """ 156 Classes can subclass this to get some convenience handlers around 157 `pywayland.server.Listener`. 158 159 This guarantees that all listeners that set up and then removed in reverse order. 160 """ 161 def add_listener(self, event: Signal, callback: Callable): 162 if not hasattr(self, "_listeners"): 163 self._listeners = [] 164 listener = Listener(callback) 165 event.add(listener) 166 self._listeners.append(listener) 167 168 def finalize_listeners(self): 169 for listener in reversed(self._listeners): 170 listener.remove() 171