diff --git a/CHANGELOG b/CHANGELOG index 789b1fbd..8539a5b8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,4 @@ +2024-07-06: [FEATURE] Add `ConditionalBorderWidth` to set border width depending on window conditions 2024-06-13: [FEATURE] Add new `CustomBorder` to draw window borders with user-defined functions 2024-06-06: [FEATURE] Add new `ConditionalBorder` to set window border depending on window conditions (name, class etc.) 2024-06-01: [BUGFIX] Fix `GlobalMenu` crash after reloading config diff --git a/docs/manual/ref/borders.rst b/docs/manual/ref/borders.rst index 4e8a72e3..ece29a96 100644 --- a/docs/manual/ref/borders.rst +++ b/docs/manual/ref/borders.rst @@ -18,3 +18,12 @@ Window Border Decorations :exclude-base: :no-commands: :show-config: + + +Changing border widths +======================= + +.. qtile_module:: qtile_extras.layout.decorations.borders + :baseclass: qtile_extras.layout.decorations.borders.ConditionalBorderWidth + :no-commands: + :show-config: diff --git a/qtile_extras/layout/decorations/__init__.py b/qtile_extras/layout/decorations/__init__.py index 9c57dfbb..e3d0ff87 100644 --- a/qtile_extras/layout/decorations/__init__.py +++ b/qtile_extras/layout/decorations/__init__.py @@ -21,6 +21,7 @@ from qtile_extras.layout.decorations.borders import ( # noqa: F401 ConditionalBorder, + ConditionalBorderWidth, CustomBorder, GradientBorder, GradientFrame, @@ -53,3 +54,24 @@ def inject_border_methods(): from qtile_extras.layout.decorations.injections import x11_paint_borders XWindow.paint_borders = x11_paint_borders + + +@hook.subscribe.startup_once +def inject_border_width_methods(): + from libqtile import qtile + + from qtile_extras.layout.decorations.injections import new_place + + if qtile.core.name == "wayland": + from libqtile.backend.wayland.xdgwindow import XdgWindow + from libqtile.backend.wayland.xwindow import XWindow + + for base in (XdgWindow, XWindow): + base._place = base.place + base.place = new_place + + else: + from libqtile.backend.x11.window import _Window + + _Window._place = _Window.place + _Window.place = new_place diff --git a/qtile_extras/layout/decorations/borders.py b/qtile_extras/layout/decorations/borders.py index b9f15184..d90805d7 100644 --- a/qtile_extras/layout/decorations/borders.py +++ b/qtile_extras/layout/decorations/borders.py @@ -571,3 +571,60 @@ def draw(self, surface, bw, x, y, width, height): with cairocffi.Context(surface) as ctx: ctx.translate(x, y) self.func(ctx, bw, width, height) + + +class ConditionalBorderWidth(Configurable): + """ + A class that allows finer control as to which border width is applied to which window. + + To configure the border width, you need to provide two parameters: + + * ``matches``: a list of tuples of (Match rules, border width) + * ``default``: border width to apply if no matches + + Matches are applied in order and will return a border width as soon as a rule matches. + + It can be used in place of the integer border width layout when defining layouts in your + config. For example: + + .. code:: python + + from qtile_extras.layout.decorations import ConditionalBorderWidth + + layouts = [ + layout.Columns( + border_focus_stack=["#d75f5f", "#8f3d3d"], + border_width=ConditionalBorderWidth( + default=2, + matches=[(Match(wm_class="vlc"), 0)]) + ), + ... + ] + + The above code will default to a border width of 2 but will apply a border width of zero + for VLC windows. + + """ + + defaults = [ + ("default", 0, "Default border width value if no rule is matched"), + ("matches", [], "List of rules to apply border widths. See docs for more details."), + ] + + def __init__(self, **config): + Configurable.__init__(self, **config) + self.add_defaults(ConditionalBorderWidth.defaults) + + def get_border_for_window(self, win): + for rule, value in self.matches: + if rule.compare(win): + return value + return self.default + + # Layouts size windows by subtracting the border width so we + # need to allow the multiplication to work on the custom class + # The size will be fixed with the injected window.place code. + def __mul__(self, other): + return other * self.default + + __rmul__ = __mul__ diff --git a/qtile_extras/layout/decorations/injections.py b/qtile_extras/layout/decorations/injections.py index 33187e43..827e9136 100644 --- a/qtile_extras/layout/decorations/injections.py +++ b/qtile_extras/layout/decorations/injections.py @@ -27,7 +27,11 @@ from libqtile.backend.wayland.window import SceneRect, Window, _rgb from xcffib.wrappers import GContextID, PixmapID -from qtile_extras.layout.decorations.borders import ConditionalBorder, _BorderStyle +from qtile_extras.layout.decorations.borders import ( + ConditionalBorder, + ConditionalBorderWidth, + _BorderStyle, +) if TYPE_CHECKING: from libqtile.backend.wayland.window import Core, Qtile, S @@ -208,3 +212,38 @@ def x11_paint_borders(self, depth, colors, borderwidth, width, height): core.PolyFillRectangle(pixmap, gc, 1, [rect]) coord += borderwidths[i] self._set_borderpixmap(depth, pixmap, gc, borderwidth, width, height) + + +def new_place( + self, + x, + y, + width, + height, + borderwidth, + bordercolor, + above=False, + margin=None, + respect_hints=False, +): + if isinstance(borderwidth, ConditionalBorderWidth): + newborder = borderwidth.get_border_for_window(self) + if newborder != borderwidth.default: + width += borderwidth.default * 2 + width -= newborder * 2 + height += borderwidth.default * 2 + height -= newborder * 2 + else: + newborder = borderwidth + + self._place( + x, + y, + width, + height, + newborder, + bordercolor, + above=above, + margin=margin, + respect_hints=respect_hints, + ) diff --git a/test/layout/decorations/test_border_decorations.py b/test/layout/decorations/test_border_decorations.py index 39b98633..89ec17c6 100644 --- a/test/layout/decorations/test_border_decorations.py +++ b/test/layout/decorations/test_border_decorations.py @@ -24,6 +24,7 @@ from qtile_extras.layout.decorations import ( ConditionalBorder, + ConditionalBorderWidth, CustomBorder, GradientBorder, GradientFrame, @@ -110,3 +111,20 @@ def test_window_decoration(manager): def test_decoration_config_errors(classname, config): with pytest.raises(ConfigError): classname(**config) + + +def test_conditional_border_width_default(): + bw = ConditionalBorderWidth(default=2) + assert 2 * bw == 4 + assert bw * 2 == 4 + + +def test_conditional_border_width_matching(): + bw = ConditionalBorderWidth( + default=2, + matches=[(Match(func=lambda w: w is True), 4), (Match(func=lambda w: w is False), 0)], + ) + + assert bw.get_border_for_window(True) == 4 + assert bw.get_border_for_window(False) == 0 + assert bw.get_border_for_window("Something else") == 2