Source code for libqtile.layout.screensplit

# Copyright (c) 2022 elParaguayo
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import annotations

from typing import TYPE_CHECKING

from libqtile import hook
from libqtile.command.base import expose_command
from libqtile.config import ScreenRect, _Match
from libqtile.layout import Columns, Max
from libqtile.layout.base import Layout
from libqtile.log_utils import logger

if TYPE_CHECKING:
    from collections.abc import Callable
    from typing import Any

    from libqtile.backend.base import Window
    from libqtile.group import _Group

    Rect = tuple[float, float, float, float]


class Split:
    def __init__(
        self, *, name: str, rect: Rect, layout: Layout, matches: list[_Match] = list()
    ) -> None:
        # Check that rect is correctly defined
        if not isinstance(rect, tuple | list):
            raise ValueError("Split rect should be a list/tuple.")

        if len(rect) != 4 or not all(isinstance(x, float | int) for x in rect):
            raise ValueError("Split rect should have 4 float/int members.")

        if isinstance(layout, ScreenSplit):
            raise ValueError("ScreenSplit layouts cannot be nested.")

        if matches:
            if isinstance(matches, list):
                if not all(isinstance(m, _Match) for m in matches):
                    raise ValueError("Invalid object in 'matches'.")
            else:
                raise ValueError("'matches' must be a list of 'Match' objects.")

        self.name = name
        self.rect = rect
        self.layout = layout
        self.matches = matches

    def clone(self, group) -> Split:
        return Split(
            name=self.name, rect=self.rect, layout=self.layout.clone(group), matches=self.matches
        )


[docs]class ScreenSplit(Layout): """ A layout that allows you to split the screen into separate areas, each of which can be assigned its own layout. This layout is intended to be used on large monitors where separate layouts may be desirable. However, unlike creating virtual screens, this layout retains the full screen configuration meaning that full screen windows will continue to fill the entire screen. Each split is defined as a dictionary with the following keys: - ``name``: this is used with the ``ScreenSplit`` widget (see below) - ``rect``: a tuple of (x, y, width, height) with each value being between 0 and 1. These are relative values based on the screen's dimensions e.g. a value of ``(0.5, 0, 0.5, 1)`` would define an area starting at the top middle of the screen and extending to the bottom left corner. - ``layout``: the layout to occupy the defined split. - ``matches``: (optional) list of ``Match`` objects which define which windows will open in the defined split. Different splits can be selected by using the following ``lazy.layout.next_split()`` and ``lazy.layout.previous_split()`` commands. To identify which split is active, users can use the ``ScreenSplit`` widget will show the name of the split and the relevant layout. Scrolling up and down on the widget will change the active split. .. note:: While keybindings will be passed to the active split's layout, bindings using the ``.when(layout=...)``` syntax will not be applied as the primary layout is ``ScreenSplit``. """ defaults = [ ( "splits", [ {"name": "top", "rect": (0, 0, 1, 0.5), "layout": Max()}, {"name": "bottom", "rect": (0, 0.5, 1, 0.5), "layout": Columns()}, ], "Screen splits. See documentation for details.", ) ] def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(ScreenSplit.defaults) self._split_index = 0 self.layouts = {} self._move_win = None self._has_matches = None print(self.splits) splits = [] for s in self.splits: try: split_obj = Split(**s) except TypeError: raise ValueError("Splits must define 'name', 'rect' and 'layout'.") splits.append(split_obj) self.splits = splits def _should_check(self, win): return win not in self.layouts and self._move_win is None @property def has_matches(self): if self._has_matches is None: self._has_matches = any(split.matches for split in self.splits) return self._has_matches @property def active_split(self): return self.splits[self._split_index] @active_split.setter def active_split(self, split): for i, sp in enumerate(self.splits): if sp == split: self._split_index = i hook.fire("layout_change", self, self.group) @property def active_layout(self): return self.active_split.layout
[docs] @expose_command def commands(self): c = super().commands() c.extend(self.active_layout.commands()) return c
def command(self, name: str) -> Callable | None: if name in self._commands: return self._commands.get(name) elif name in self.active_split.layout._commands: return getattr(self.active_split.layout, name) return None def _get_rect(self, rect: Rect, screen: ScreenRect) -> ScreenRect: x, y, w, h = rect return ScreenRect( int(screen.x + x * screen.width), int(screen.y + y * screen.height), int(screen.width * w), int(screen.height * h), ) def _set_hooks(self) -> None: hook.subscribe.focus_change(self.focus_split) def _unset_hooks(self) -> None: hook.unsubscribe.focus_change(self.focus_split) def _match_win(self, win: Window) -> Split | None: for split in self.splits: if not split.matches: continue for m in split.matches: if win.match(m): return split return None def clone(self, group: _Group) -> ScreenSplit: result = Layout.clone(self, group) new_splits = [split.clone(group) for split in self.splits] result.splits = new_splits return result def add_client(self, win: Window) -> None: split = None # If this is a new window and we're not moving this window between splits # then we should check for match rules if self.has_matches and self._should_check(win): split = self._match_win(win) if split is not None: self.active_split = split self.active_layout.add_client(win) self.layouts[win] = self.active_split def remove(self, win: Window) -> None: self.layouts[win].layout.remove(win) del self.layouts[win] def hide(self) -> None: self._unset_hooks() def show(self, _rect) -> None: self._set_hooks() def configure(self, client: Window, screen_rect: ScreenRect) -> None: if client not in self.layouts: logger.warning("Unknown client: %s", client) return layout = self.layouts[client].layout rect = self._get_rect(self.layouts[client].rect, screen_rect) layout.configure(client, rect) def get_windows(self) -> list[Window]: return self.active_layout.get_windows() def _change_split(self, step: int = 1) -> None: self._split_index = (self._split_index + step) % len(self.splits) def _move_win_to_split(self, step: int = 1) -> None: # We get the ID of the next split now as removing window from a group # will shift focus to another window which could change the active # split. next_split = (self._split_index + step) % len(self.splits) self._move_win = self.group.current_window self.group.remove(self._move_win) self._split_index = next_split self.group.add(self._move_win) self.layouts[self._move_win] = self.active_split self._move_win = None hook.fire("layout_change", self, self.group)
[docs] @expose_command def next(self) -> None: """Move to next client.""" self.__getattr__("next")
[docs] @expose_command def previous(self) -> None: """Move to previous client.""" self.__getattr__("previous")
def focus_first(self) -> Window: return self.active_layout.focus_first() def focus_last(self) -> Window: return self.active_layout.focus_last() def focus_next(self, win: Window) -> Window: return self.active_layout.focus_next(win) def focus_previous(self, win: Window) -> Window: return self.active_layout.focus_previous(win) def focus_split(self, win: Window | None = None) -> None: if win is None: win = self.group.current_window for split in self.splits: if win in split.layout.get_windows(): if split is not self.active_split: self.active_split = split hook.fire("layout_change", self, self.group) break def focus(self, client: Window) -> None: self.focus_split(client) self.active_layout.focus(client)
[docs] @expose_command def next_split(self) -> None: """Move to next split.""" self._change_split() hook.fire("layout_change", self, self.group)
[docs] @expose_command def previous_split(self) -> None: """Move to previous client.""" self._change_split(-1) hook.fire("layout_change", self, self.group)
[docs] @expose_command def move_window_to_next_split(self) -> None: """Move current window to next split.""" self._move_win_to_split()
[docs] @expose_command def move_window_to_previous_split(self) -> None: """Move current window to previous split.""" self._move_win_to_split(-1)
[docs] @expose_command def info(self) -> dict[str, Any]: inf = Layout.info(self) inf["current_split"] = self.active_split.name inf["current_layout"] = self.active_layout.name inf["current_clients"] = [] inf["clients"] = [] inf["splits"] = [] for split in self.splits: clients = split.layout.info()["clients"] s_info = { "name": split.name, "rect": split.rect, "layout": split.layout.name, "clients": clients, } inf["splits"].append(s_info) inf["clients"].extend(clients) if split is self.active_split: inf["current_clients"] = clients return inf