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