from __future__ import annotations
import contextlib
import enum
import math
import typing
from abc import ABCMeta, abstractmethod
import cairocffi
from libqtile import drawer, pangocffi, utils
from libqtile.command.base import CommandObject
if typing.TYPE_CHECKING:
from typing import Any, Dict, List, Optional, Tuple, Union
from libqtile import config
from libqtile.command.base import ItemT
from libqtile.core.manager import Qtile
from libqtile.group import _Group
from libqtile.utils import ColorType
class Core(metaclass=ABCMeta):
painter: Any
@property
@abstractmethod
def name(self) -> str:
"""The name of the backend"""
pass
@abstractmethod
def finalize(self):
"""Destructor/Clean up resources"""
@property
@abstractmethod
def display_name(self) -> str:
pass
@abstractmethod
def setup_listener(self, qtile: Qtile) -> None:
"""Setup a listener for the given qtile instance"""
@abstractmethod
def remove_listener(self) -> None:
"""Setup a listener for the given qtile instance"""
def update_desktops(self, groups: List[_Group], index: int) -> None:
"""Set the current desktops of the window manager"""
@abstractmethod
def get_screen_info(self) -> List[Tuple[int, int, int, int]]:
"""Get the screen information"""
@abstractmethod
def grab_key(self, key: Union[config.Key, config.KeyChord]) -> Tuple[int, int]:
"""Configure the backend to grab the key event"""
@abstractmethod
def ungrab_key(self, key: Union[config.Key, config.KeyChord]) -> Tuple[int, int]:
"""Release the given key event"""
@abstractmethod
def ungrab_keys(self) -> None:
"""Release the grabbed key events"""
@abstractmethod
def grab_button(self, mouse: config.Mouse) -> int:
"""Configure the backend to grab the mouse event"""
@abstractmethod
def ungrab_buttons(self) -> None:
"""Release the grabbed button events"""
@abstractmethod
def grab_pointer(self) -> None:
"""Configure the backend to grab mouse events"""
@abstractmethod
def ungrab_pointer(self) -> None:
"""Release grabbed pointer events"""
def scan(self) -> None:
"""Scan for clients if required."""
def warp_pointer(self, x: int, y: int) -> None:
"""Warp the pointer to the given coordinates relative."""
def update_client_list(self, windows_map: Dict[int, WindowType]) -> None:
"""Update the list of windows being managed"""
@contextlib.contextmanager
def masked(self):
"""A context manager to suppress window events while operating on many windows."""
yield
def create_internal(self, x: int, y: int, width: int, height: int) -> Internal:
"""Create an internal window controlled by Qtile."""
raise NotImplementedError # Only error when called, not when instantiating class
def flush(self) -> None:
"""If needed, flush the backend's event queue."""
def graceful_shutdown(self):
"""Try to close windows gracefully before exiting"""
def simulate_keypress(self, modifiers: List[str], key: str) -> None:
"""Simulate a keypress with given modifiers"""
def keysym_from_name(self, name: str) -> int:
"""Get the keysym for a key from its name"""
raise NotImplementedError
def change_vt(self, vt: int) -> bool:
"""Change virtual terminal, returning success."""
return False
@enum.unique
class FloatStates(enum.Enum):
NOT_FLOATING = 1
FLOATING = 2
MAXIMIZED = 3
FULLSCREEN = 4
TOP = 5
MINIMIZED = 6
class _Window(CommandObject, metaclass=ABCMeta):
def __init__(self):
self.borderwidth: int = 0
self.name: str = "<no name>"
self.reserved_space: Optional[Tuple[int, int, int, int]] = None
self.defunct: bool = False
@property
@abstractmethod
def group(self) -> Optional[_Group]:
"""The group to which this window belongs."""
@property
@abstractmethod
def wid(self) -> int:
"""The unique window ID"""
@abstractmethod
def hide(self) -> None:
"""Hide the window"""
@abstractmethod
def unhide(self) -> None:
"""Unhide the window"""
@abstractmethod
def kill(self) -> None:
"""Kill the window"""
def get_wm_class(self) -> Optional[List]:
"""Return the class(es) of the window"""
return None
def get_wm_type(self) -> Optional[str]:
"""Return the type of the window"""
return None
def get_wm_role(self) -> Optional[str]:
"""Return the role of the window"""
return None
@property
def can_steal_focus(self):
"""Is it OK for this window to steal focus?"""
return True
def has_fixed_ratio(self) -> bool:
"""Does this window want a fixed aspect ratio?"""
return False
def has_fixed_size(self) -> bool:
"""Does this window want a fixed size?"""
return False
@property
def urgent(self):
"""Whether this window urgently wants focus"""
return False
@abstractmethod
def place(self, x, y, width, height, borderwidth, bordercolor,
above=False, margin=None, respect_hints=False):
"""Place the window in the given position."""
def _items(self, name: str) -> ItemT:
return None
def _select(self, name, sel):
return None
def info(self) -> Dict[str, Any]:
"""Return information on this window."""
return {}
class Window(_Window, metaclass=ABCMeta):
"""A regular Window belonging to a client."""
# If float_x or float_y are None, the window has never floated
float_x: Optional[int]
float_y: Optional[int]
def __repr__(self):
return "Window(name=%r, wid=%i)" % (self.name, self.wid)
@property
def floating(self) -> bool:
"""Whether this window is floating."""
return False
@property
def maximized(self) -> bool:
"""Whether this window is maximized."""
return False
@property
def fullscreen(self) -> bool:
"""Whether this window is fullscreened."""
return False
@property
def wants_to_fullscreen(self) -> bool:
"""Does this window want to be fullscreen?"""
return False
def match(self, match: config.Match) -> bool:
"""Compare this window against a Match instance."""
return match.compare(self)
@abstractmethod
def focus(self, warp: bool) -> None:
"""Focus this window and optional warp the pointer to it."""
@abstractmethod
def togroup(
self, group_name: Optional[str] = None, *, switch_group: bool = False
) -> None:
"""Move window to a specified group
Also switch to that group if switch_group is True.
"""
@property
def has_focus(self):
return self == self.qtile.current_window
def has_user_set_position(self) -> bool:
"""Whether this window has user-defined geometry"""
return False
def is_transient_for(self) -> Optional["WindowType"]:
"""What window is this window a transient window for?"""
return None
@abstractmethod
def get_pid(self) -> int:
"""Return the PID that owns the window."""
def paint_borders(self, color, width) -> None:
"""Paint the window borders with the given color and width"""
@abstractmethod
def cmd_focus(self, warp: bool = True) -> None:
"""Focuses the window."""
@abstractmethod
def cmd_info(self) -> Dict:
"""Return a dictionary of info."""
@abstractmethod
def cmd_get_position(self) -> Tuple[int, int]:
"""Get the (x, y) of the window"""
@abstractmethod
def cmd_get_size(self) -> Tuple[int, int]:
"""Get the (width, height) of the window"""
@abstractmethod
def cmd_move_floating(self, dx: int, dy: int) -> None:
"""Move window by dx and dy"""
@abstractmethod
def cmd_resize_floating(self, dw: int, dh: int) -> None:
"""Add dw and dh to size of window"""
@abstractmethod
def cmd_set_position_floating(self, x: int, y: int) -> None:
"""Move window to x and y"""
@abstractmethod
def cmd_set_size_floating(self, w: int, h: int) -> None:
"""Set window dimensions to w and h"""
@abstractmethod
def cmd_place(self, x, y, width, height, borderwidth, bordercolor,
above=False, margin=None) -> None:
"""Place the window with the given position and geometry."""
@abstractmethod
def cmd_toggle_floating(self) -> None:
"""Toggle the floating state of the window."""
@abstractmethod
def cmd_enable_floating(self) -> None:
"""Float the window."""
@abstractmethod
def cmd_disable_floating(self) -> None:
"""Tile the window."""
@abstractmethod
def cmd_toggle_maximize(self) -> None:
"""Toggle the fullscreen state of the window."""
@abstractmethod
def cmd_toggle_fullscreen(self) -> None:
"""Toggle the fullscreen state of the window."""
@abstractmethod
def cmd_enable_fullscreen(self) -> None:
"""Fullscreen the window"""
@abstractmethod
def cmd_disable_fullscreen(self) -> None:
"""Un-fullscreen the window"""
@abstractmethod
def cmd_bring_to_front(self) -> None:
"""Bring the window to the front"""
def cmd_togroup(
self, group_name: Optional[str] = None, *, switch_group: bool = False
) -> None:
"""Move window to a specified group
Also switch to that group if switch_group is True.
"""
self.togroup(group_name, switch_group=switch_group)
[docs] def cmd_opacity(self, opacity):
"""Set the window's opacity"""
if opacity < .1:
self.opacity = .1
elif opacity > 1:
self.opacity = 1
else:
self.opacity = opacity
[docs] def cmd_down_opacity(self):
"""Decrease the window's opacity"""
if self.opacity > .2:
# don't go completely clear
self.opacity -= .1
else:
self.opacity = .1
[docs] def cmd_up_opacity(self):
"""Increase the window's opacity"""
if self.opacity < .9:
self.opacity += .1
else:
self.opacity = 1
@abstractmethod
def cmd_kill(self) -> None:
"""Kill the window. Try to be polite."""
class Internal(_Window, metaclass=ABCMeta):
"""An Internal window belonging to Qtile."""
def __repr__(self):
return "Internal(wid=%s)" % self.wid
@abstractmethod
def create_drawer(self, width: int, height: int) -> Drawer:
"""Create a Drawer that draws to this window."""
def process_window_expose(self) -> None:
"""Respond to the window being exposed. Required by X11 backend."""
def process_button_click(self, x: int, y: int, button: int) -> None:
"""Handle a pointer button click."""
def process_button_release(self, x: int, y: int, button: int) -> None:
"""Handle a pointer button release."""
def process_pointer_enter(self, x: int, y: int) -> None:
"""Handle the pointer entering the window."""
def process_pointer_leave(self, x: int, y: int) -> None:
"""Handle the pointer leaving the window."""
def process_pointer_motion(self, x: int, y: int) -> None:
"""Handle pointer motion within the window."""
def process_key_press(self, keycode: int) -> None:
"""Handle a key press."""
class Static(_Window, metaclass=ABCMeta):
"""A Window not bound to a single Group."""
screen: config.Screen
def __repr__(self):
return "Static(name=%r, wid=%s)" % (self.name, self.wid)
WindowType = typing.Union[Window, Internal, Static]
class Drawer:
"""A helper class for drawing to Internal windows.
We stage drawing operations locally in memory using a cairo RecordingSurface before
finally drawing all operations to a backend-specific target.
"""
# We need to track extent of drawing to know when to redraw.
previous_rect: Tuple[int, int, Optional[int], Optional[int]]
current_rect: Tuple[int, int, Optional[int], Optional[int]]
def __init__(self, qtile: Qtile, win: Internal, width: int, height: int):
self.qtile = qtile
self._win = win
self._width = width
self._height = height
self.surface: cairocffi.RecordingSurface
self.ctx: cairocffi.Context
self._reset_surface()
self.mirrors: Dict[Drawer, bool] = {}
self.current_rect = (0, 0, 0, 0)
self.previous_rect = (-1, -1, -1, -1)
def finalize(self):
"""Destructor/Clean up resources"""
self.surface = None
self.ctx = None
def add_mirror(self, mirror: Drawer):
"""Keep details of other drawers that are mirroring this one."""
self.mirrors[mirror] = False
def reset_mirrors(self):
"""Reset the drawn status of mirrors."""
self.mirrors = {m: False for m in self.mirrors}
@property
def mirrors_drawn(self) -> bool:
"""Returns True if all mirrors have been drawn with the current surface."""
return all(v for v in self.mirrors.values())
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, width: int):
self._width = width
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, height: int):
self._height = height
def _reset_surface(self):
"""This creates a fresh surface and cairo context."""
self.surface = cairocffi.RecordingSurface(
cairocffi.CONTENT_COLOR_ALPHA,
None,
)
self.ctx = self.new_ctx()
@property
def needs_update(self) -> bool:
# We can't test for the surface's ink_extents here on its own as a completely
# transparent background would not show any extents but we may still need to
# redraw (e.g. if a Spacer widget has changed position and/or size)
# Check if the size of the area being drawn has changed
rect_changed = self.current_rect != self.previous_rect
# Check if draw has content (would be False for completely transparent drawer)
ink_changed = any(not math.isclose(0.0, i) for i in self.surface.ink_extents())
return ink_changed or rect_changed
def paint_to(self, drawer: Drawer) -> None:
drawer.ctx.set_source_surface(self.surface)
drawer.ctx.paint()
self.mirrors[drawer] = True
if self.mirrors_drawn:
self._reset_surface()
self.reset_mirrors()
def _rounded_rect(self, x, y, width, height, linewidth):
aspect = 1.0
corner_radius = height / 10.0
radius = corner_radius / aspect
degrees = math.pi / 180.0
self.ctx.new_sub_path()
delta = radius + linewidth / 2
self.ctx.arc(x + width - delta, y + delta, radius,
-90 * degrees, 0 * degrees)
self.ctx.arc(x + width - delta, y + height - delta,
radius, 0 * degrees, 90 * degrees)
self.ctx.arc(x + delta, y + height - delta, radius,
90 * degrees, 180 * degrees)
self.ctx.arc(x + delta, y + delta, radius,
180 * degrees, 270 * degrees)
self.ctx.close_path()
def rounded_rectangle(self, x: int, y: int, width: int, height: int, linewidth: int):
self._rounded_rect(x, y, width, height, linewidth)
self.ctx.set_line_width(linewidth)
self.ctx.stroke()
def rounded_fillrect(self, x: int, y: int, width: int, height: int, linewidth: int):
self._rounded_rect(x, y, width, height, linewidth)
self.ctx.fill()
def rectangle(self, x: int, y: int, width: int, height: int, linewidth: int = 2):
self.ctx.set_line_width(linewidth)
self.ctx.rectangle(x, y, width, height)
self.ctx.stroke()
def fillrect(self, x: int, y: int, width: int, height: int, linewidth: int = 2):
self.ctx.set_line_width(linewidth)
self.ctx.rectangle(x, y, width, height)
self.ctx.fill()
self.ctx.stroke()
def draw(
self,
offsetx: int = 0,
offsety: int = 0,
width: Optional[int] = None,
height: Optional[int] = None,
):
"""
This draws our cached operations to the Internal window.
Parameters
==========
offsetx :
the X offset to start drawing at.
offsety :
the Y offset to start drawing at.
width :
the X portion of the canvas to draw at the starting point.
height :
the Y portion of the canvas to draw at the starting point.
"""
def new_ctx(self):
return pangocffi.patch_cairo_context(cairocffi.Context(self.surface))
def set_source_rgb(self, colour: Union[ColorType, List[ColorType]], ctx: cairocffi.Context = None):
# If an alternate context is not provided then we draw to the
# drawer's default context
if ctx is None:
ctx = self.ctx
if isinstance(colour, list):
if len(colour) == 0:
# defaults to black
ctx.set_source_rgba(*utils.rgb("#000000"))
elif len(colour) == 1:
ctx.set_source_rgba(*utils.rgb(colour[0]))
else:
linear = cairocffi.LinearGradient(0.0, 0.0, 0.0, self.height)
step_size = 1.0 / (len(colour) - 1)
step = 0.0
for c in colour:
rgb_col = utils.rgb(c)
if len(rgb_col) < 4:
rgb_col[3] = 1
linear.add_color_stop_rgba(step, *rgb_col)
step += step_size
ctx.set_source(linear)
else:
ctx.set_source_rgba(*utils.rgb(colour))
def clear(self, colour):
self.set_source_rgb(colour)
self.ctx.rectangle(0, 0, self.width, self.height)
self.ctx.fill()
def textlayout(
self, text, colour, font_family, font_size, font_shadow, markup=False, **kw
):
"""Get a text layout"""
textlayout = drawer.TextLayout(
self, text, colour, font_family, font_size, font_shadow, markup=markup, **kw
)
return textlayout
def max_layout_size(self, texts, font_family, font_size):
sizelayout = self.textlayout("", "ffffff", font_family, font_size, None)
widths, heights = [], []
for i in texts:
sizelayout.text = i
widths.append(sizelayout.width)
heights.append(sizelayout.height)
return max(widths), max(heights)
def text_extents(self, text):
return self.ctx.text_extents(utils.scrub_to_utf8(text))
def font_extents(self):
return self.ctx.font_extents()
def fit_fontsize(self, heightlimit):
"""Try to find a maximum font size that fits any strings within the height"""
self.ctx.set_font_size(heightlimit)
asc, desc, height, _, _ = self.font_extents()
self.ctx.set_font_size(
int(heightlimit * heightlimit / height))
return self.font_extents()
def fit_text(self, strings, heightlimit):
"""Try to find a maximum font size that fits all strings within the height"""
self.ctx.set_font_size(heightlimit)
_, _, _, maxheight, _, _ = self.ctx.text_extents("".join(strings))
if not maxheight:
return 0, 0
self.ctx.set_font_size(
int(heightlimit * heightlimit / maxheight))
maxwidth, maxheight = 0, 0
for i in strings:
_, _, x, y, _, _ = self.ctx.text_extents(i)
maxwidth = max(maxwidth, x)
maxheight = max(maxheight, y)
return maxwidth, maxheight
def draw_vbar(self, color, x, y1, y2, linewidth=1):
self.set_source_rgb(color)
self.ctx.move_to(x, y1)
self.ctx.line_to(x, y2)
self.ctx.set_line_width(linewidth)
self.ctx.stroke()
def draw_hbar(self, color, x1, x2, y, linewidth=1):
self.set_source_rgb(color)
self.ctx.move_to(x1, y)
self.ctx.line_to(x2, y)
self.ctx.set_line_width(linewidth)
self.ctx.stroke()