Source code for libqtile.layout.plasma

# Copyright (c) 2017 numirias
# Copyright (c) 2024 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.
import copy
import time
from enum import Enum, Flag, auto
from math import isclose
from typing import NamedTuple

from libqtile import hook
from libqtile.command.base import expose_command
from libqtile.hook import Hook, qtile_hooks
from libqtile.layout.base import Layout

plasma_hook = Hook(
    "plasma_add_mode",
    """
    Used to flag when the add mode of the Plasma layout has changed.

    The hooked function should take one argument being the layout object.
    """,
)


qtile_hooks.register_hook(plasma_hook)


class NotRestorableError(Exception):
    pass


class Point(NamedTuple):
    x: int
    y: int


class Dimensions(NamedTuple):
    x: int
    y: int
    width: int
    height: int


class Orient(Flag):
    HORIZONTAL = 0
    VERTICAL = 1


class Direction(Enum):
    UP = auto()
    DOWN = auto()
    LEFT = auto()
    RIGHT = auto()

    @property
    def orient(self):
        return Orient.HORIZONTAL if self in [self.LEFT, self.RIGHT] else Orient.VERTICAL

    @property
    def offset(self):
        return 1 if self in [self.RIGHT, self.DOWN] else -1


class Priority(Enum):
    FIXED = auto()
    BALANCED = auto()


class AddMode(Flag):
    HORIZONTAL = auto()
    VERTICAL = auto()
    SPLIT = auto()

    @property
    def orient(self):
        return Orient.VERTICAL if self & self.VERTICAL else Orient.HORIZONTAL


border_check = {
    Direction.UP: lambda a, b: isclose(a.y, b.y_end),
    Direction.DOWN: lambda a, b: isclose(a.y_end, b.y),
    Direction.LEFT: lambda a, b: isclose(a.x, b.x_end),
    Direction.RIGHT: lambda a, b: isclose(a.x_end, b.x),
}


def flatten(value):
    """Flattens a nested list of lists into a single list."""
    out = []
    for x in value:
        if not isinstance(x, list):
            out.append(x)
        else:
            out.extend(flatten(x))
    return out


class Node:
    """
    A tree node.

    Each node represents a container that can hold a payload and child nodes.
    """

    min_size_default = 100
    root_orient = Orient.HORIZONTAL
    priority = Priority.FIXED

    def __init__(self, payload=None, x=None, y=None, width=None, height=None):
        self.payload = payload
        self._x = x
        self._y = y
        self._width = width
        self._height = height
        self._size = None
        self.children = []
        self.last_accessed = 0
        self.parent = None
        self.restorables = {}

    def __repr__(self):
        info = self.payload or ""
        if self:
            info += " +%d" % len(self)
        return "<Node %s %x>" % (info, id(self))

    # Define dunder methods to treat Node objects like an iterable
    def __contains__(self, node):
        if node is self:
            return True
        for child in self:
            if node in child:
                return True
        return False

    def __iter__(self):
        yield from self.children

    def __getitem__(self, key):
        return self.children[key]

    def __setitem__(self, key, value):
        self.children[key] = value

    def __len__(self):
        return len(self.children)

    @property
    def root(self):
        try:
            # Walk way up the tree until we find the root
            return self.parent.root
        except AttributeError:
            # Node has no parent (self.parent is None) so node must be root
            return self

    @property
    def is_root(self):
        return self.parent is None

    @property
    def is_leaf(self):
        return not self.children

    @property
    def index(self):
        return self.parent.children.index(self)

    @property
    def tree(self):
        return [c.tree if c else c for c in self]

    @property
    def siblings(self):
        if self.is_root:
            return list()
        return [c for c in self.parent if c is not self]

    @property
    def first_leaf(self):
        if self.is_leaf:
            return self
        return self[0].first_leaf

    @property
    def last_leaf(self):
        if self.is_leaf:
            return self
        return self[-1].last_leaf

    @property
    def recent_leaf(self):
        if self.is_leaf:
            return self
        return max(self, key=lambda n: n.last_accessed).recent_leaf

    @property
    def prev_leaf(self):
        if self.is_root:
            return self.last_leaf
        idx = self.index - 1
        if idx < 0:
            return self.parent.prev_leaf
        return self.parent[idx].last_leaf

    @property
    def next_leaf(self):
        if self.is_root:
            return self.first_leaf
        idx = self.index + 1
        if idx >= len(self.parent):
            return self.parent.next_leaf
        return self.parent[idx].first_leaf

    @property
    def all_leafs(self):
        if self.is_leaf:
            yield self
        for child in self:
            yield from child.all_leafs

    @property
    def orient(self):
        if self.is_root:
            return self.root_orient
        return ~self.parent.orient

    @property
    def horizontal(self):
        return self.orient is Orient.HORIZONTAL

    @property
    def vertical(self):
        return self.orient is Orient.VERTICAL

    @property
    def x(self):
        if self.is_root:
            return self._x
        if self.horizontal:
            return self.parent.x
        return self.parent.x + self.size_offset

    @x.setter
    def x(self, val):
        if not self.is_root:
            return
        self._x = val

    @property
    def y(self):
        if self.is_root:
            return self._y
        if self.vertical:
            return self.parent.y
        return self.parent.y + self.size_offset

    @y.setter
    def y(self, val):
        if not self.is_root:
            return
        self._y = val

    @property
    def pos(self):
        return Point(self.x, self.y)

    @property
    def width(self):
        if self.is_root:
            return self._width
        if self.horizontal:
            return self.parent.width
        return self.size

    @width.setter
    def width(self, val):
        if self.is_root:
            self._width = val
        elif self.horizontal:
            self.parent.size = val
        else:
            self.size = val

    @property
    def height(self):
        if self.is_root:
            return self._height
        if self.vertical:
            return self.parent.height
        return self.size

    @height.setter
    def height(self, val):
        if self.is_root:
            self._height = val
        elif self.vertical:
            self.parent.size = val
        else:
            self.size = val

    @property
    def x_end(self):
        return self.x + self.width

    @property
    def y_end(self):
        return self.y + self.height

    @property
    def x_center(self):
        return self.x + self.width / 2

    @property
    def y_center(self):
        return self.y + self.height / 2

    @property
    def center(self):
        return Point(self.x_center, self.y_center)

    @property
    def pixel_perfect(self):
        """
        Return pixel-perfect int dimensions (x, y, width, height) which
        compensate for gaps in the layout grid caused by plain int conversions.
        """
        x, y, width, height = self.x, self.y, self.width, self.height
        threshold = 0.99999
        if (x - int(x)) + (width - int(width)) > threshold:
            width += 1
        if (y - int(y)) + (height - int(height)) > threshold:
            height += 1
        return Dimensions(*map(int, (x, y, width, height)))

    @property
    def capacity(self):
        return self.width if self.horizontal else self.height

    @property
    def size(self):
        """Return amount of space taken in parent container."""
        if self.is_root:
            return None
        if self.fixed:
            return self._size
        if self.flexible:
            # Distribute space evenly among flexible nodes
            taken = sum(n.size for n in self.siblings if not n.flexible)
            flexibles = [n for n in self.parent if n.flexible]
            return (self.parent.capacity - taken) / len(flexibles)
        return max(sum(gc.min_size for gc in c) for c in self)

    @size.setter
    def size(self, val):
        if self.is_root or not self.siblings:
            return
        if val is None:
            self.reset_size()
            return
        occupied = sum(s.min_size_bound for s in self.siblings)
        val = max(min(val, self.parent.capacity - occupied), self.min_size_bound)
        self.force_size(val)

    def force_size(self, val):
        """Set size without considering available space."""
        Node.fit_into(self.siblings, self.parent.capacity - val)
        if val == 0:
            return
        if self:
            Node.fit_into([self], val)
        self._size = val

    @property
    def size_offset(self):
        return sum(c.size for c in self.parent[: self.index])

    @staticmethod
    def fit_into(nodes, space):
        """Resize nodes to fit them into the available space."""
        if not nodes:
            return
        occupied = sum(n.min_size for n in nodes)
        if space >= occupied and any(n.flexible for n in nodes):
            # If any flexible node exists, it will occupy the space
            # automatically, not requiring any action.
            return
        nodes_left = nodes[:]
        space_left = space
        if space < occupied:
            for node in nodes:
                if node.min_size_bound != node.min_size:
                    continue
                # Substract nodes that are already at their minimal possible
                # size because they can't be shrinked any further.
                space_left -= node.min_size
                nodes_left.remove(node)
        if not nodes_left:
            return
        factor = space_left / sum(n.size for n in nodes_left)
        for node in nodes_left:
            new_size = node.size * factor
            if node.fixed:
                node._size = new_size  # pylint: disable=protected-access
            for child in node:
                Node.fit_into(child, new_size)

    @property
    def fixed(self):
        """A node is fixed if it has a specified size."""
        return self._size is not None

    @property
    def min_size(self):
        if self.fixed:
            return self._size
        if self.is_leaf:
            return self.min_size_default
        size = max(sum(gc.min_size for gc in c) for c in self)
        return max(size, self.min_size_default)

    @property
    def min_size_bound(self):
        if self.is_leaf:
            return self.min_size_default
        return max(sum(gc.min_size_bound for gc in c) or self.min_size_default for c in self)

    def reset_size(self):
        self._size = None

    @property
    def flexible(self):
        """
        A node is flexible if its size isn't (explicitly or implicitly)
        determined.
        """
        if self.fixed:
            return False
        return all((any(gc.flexible for gc in c) or c.is_leaf) for c in self)

    def access(self):
        self.last_accessed = time.time()
        try:
            self.parent.access()
        except AttributeError:
            pass

    def neighbor(self, direction):
        """Return adjacent leaf node in specified direction."""
        if self.is_root:
            return None
        if direction.orient is self.parent.orient:
            target_idx = self.index + direction.offset
            if 0 <= target_idx < len(self.parent):
                return self.parent[target_idx].recent_leaf
            if self.parent.is_root:
                return None
            return self.parent.parent.neighbor(direction)
        return self.parent.neighbor(direction)

    @property
    def up(self):
        return self.neighbor(Direction.UP)

    @property
    def down(self):
        return self.neighbor(Direction.DOWN)

    @property
    def left(self):
        return self.neighbor(Direction.LEFT)

    @property
    def right(self):
        return self.neighbor(Direction.RIGHT)

    def common_border(self, node, direction):
        """Return whether a common border with given node in specified
        direction exists.
        """
        if not border_check[direction](self, node):
            return False
        if direction in [Direction.UP, Direction.DOWN]:
            detached = node.x >= self.x_end or node.x_end <= self.x
        else:
            detached = node.y >= self.y_end or node.y_end <= self.y
        return not detached

    def close_neighbor(self, direction):
        """Return visually adjacent leaf node in specified direction."""
        nodes = [n for n in self.root.all_leafs if self.common_border(n, direction)]
        if not nodes:
            return None
        most_recent = max(nodes, key=lambda n: n.last_accessed)
        if most_recent.last_accessed > 0:
            return most_recent
        if direction in [Direction.UP, Direction.DOWN]:
            match = lambda n: n.x <= self.x_center <= n.x_end  # noqa: E731
        else:
            match = lambda n: n.y <= self.y_center <= n.y_end  # noqa: E731
        return next(n for n in nodes if match(n))

    @property
    def close_up(self):
        return self.close_neighbor(Direction.UP)

    @property
    def close_down(self):
        return self.close_neighbor(Direction.DOWN)

    @property
    def close_left(self):
        return self.close_neighbor(Direction.LEFT)

    @property
    def close_right(self):
        return self.close_neighbor(Direction.RIGHT)

    def add_child(self, node, idx=None):
        if idx is None:
            idx = len(self)
        self.children.insert(idx, node)
        node.parent = self
        if len(self) == 1:
            return
        total = self.capacity
        if Node.priority is Priority.FIXED:
            # Prioritising windows with fixed sizes means the most space the siblings
            # must fit into is total width less the minimum size for a new node.
            # However, the new node doesn't have a fixed size so will expand to fit
            # available space
            space = total - Node.min_size_default
        else:
            # Balanced approach means that space for existing nodes is reduced so that
            # all nodes would be distributed evenly if none had fixed widths
            space = total - (total / len(self))
        Node.fit_into(node.siblings, space)

    def add_child_after(self, new, old):
        self.add_child(new, idx=old.index + 1)

    def remove_child(self, node):
        node._save_restore_state()  # pylint: disable=W0212
        node.force_size(0)
        self.children.remove(node)
        if len(self) == 1:
            child = self[0]
            if self.is_root:
                # A single child doesn't need a fixed size
                child.reset_size()
            else:
                # Collapse tree with a single child
                self.parent.replace_child(self, child)
                Node.fit_into(child, self.capacity)

    def remove(self):
        self.parent.remove_child(self)

    def replace_child(self, old, new):
        self[old.index] = new
        new.parent = self
        new._size = old._size  # pylint: disable=protected-access

    def flip_with(self, node, reverse=False):
        """Join with node in a new, orthogonal container."""
        container = Node()
        self.parent.replace_child(self, container)
        self.reset_size()
        for child in [node, self] if reverse else [self, node]:
            container.add_child(child)

    def add_node(self, node, mode=None):
        """Add node according to the mode.

        This can result in adding it as a child, joining with it in a new
        flipped sub-container, or splitting the space with it.
        """
        if self.is_root:
            self.add_child(node)
        elif mode is None:
            self.parent.add_child_after(node, self)
        elif mode.orient is self.parent.orient:
            if mode & AddMode.SPLIT:
                node._size = 0  # pylint: disable=protected-access
                self.parent.add_child_after(node, self)
                self._size = node._size = self.size / 2
            else:
                self.parent.add_child_after(node, self)
        else:
            self.flip_with(node)

    def restore(self, node):
        """Restore node.

        Try to add the node in a place where a node with the same payload
        has previously been.
        """
        restorables = self.root.restorables
        try:
            parent, idx, sizes, fixed, flip = restorables[node.payload]
        except KeyError:
            raise NotRestorableError()  # pylint: disable=raise-missing-from
        if parent not in self.root:
            # Don't try to restore if parent is not part of the tree anymore
            raise NotRestorableError()
        node.reset_size()
        if flip:
            old_parent_size = parent.size
            parent.flip_with(node, reverse=(idx == 0))
            node.size, parent.size = sizes
            Node.fit_into(parent, old_parent_size)
        else:
            parent.add_child(node, idx=idx)
            node.size = sizes[0]
            if len(sizes) == 2:
                node.siblings[0].size = sizes[1]
        if not fixed:
            node.reset_size()
        del restorables[node.payload]

    def _save_restore_state(self):
        parent = self.parent
        sizes = (self.size,)
        flip = False
        if len(self.siblings) == 1:
            # If there is only one node left in the container, we need to save
            # its size too because the size will be lost.
            sizes += (self.siblings[0]._size,)  # pylint: disable=W0212
            if not self.parent.is_root:
                flip = True
                parent = self.siblings[0]
        self.root.restorables[self.payload] = (parent, self.index, sizes, self.fixed, flip)

    def move(self, direction):
        """Move this node in `direction`. Return whether node was moved."""
        if self.is_root:
            return False
        if direction.orient is self.parent.orient:
            old_idx = self.index
            new_idx = old_idx + direction.offset
            if 0 <= new_idx < len(self.parent):
                p = self.parent
                p[old_idx], p[new_idx] = p[new_idx], p[old_idx]
                return True
            new_sibling = self.parent.parent
        else:
            new_sibling = self.parent
        try:
            new_parent = new_sibling.parent
            idx = new_sibling.index
        except AttributeError:
            return False
        self.reset_size()
        self.parent.remove_child(self)
        new_parent.add_child(self, idx + (1 if direction.offset == 1 else 0))
        return True

    def move_up(self):
        return self.move(Direction.UP)

    def move_down(self):
        return self.move(Direction.DOWN)

    def move_right(self):
        return self.move(Direction.RIGHT)

    def move_left(self):
        return self.move(Direction.LEFT)

    def _move_and_integrate(self, direction):
        old_parent = self.parent
        self.move(direction)
        if self.parent is not old_parent:
            self.integrate(direction)

    def integrate(self, direction):
        if direction.orient != self.parent.orient:
            self._move_and_integrate(direction)
            return
        target_idx = self.index + direction.offset
        if target_idx < 0 or target_idx >= len(self.parent):
            self._move_and_integrate(direction)
            return
        self.reset_size()
        target = self.parent[target_idx]
        self.parent.remove_child(self)
        if target.is_leaf:
            target.flip_with(self)
        else:
            target.add_child(self)

    def integrate_up(self):
        self.integrate(Direction.UP)

    def integrate_down(self):
        self.integrate(Direction.DOWN)

    def integrate_left(self):
        self.integrate(Direction.LEFT)

    def integrate_right(self):
        self.integrate(Direction.RIGHT)

    def find_payload(self, payload):
        if self.payload is payload:
            return self
        for child in self:
            needle = child.find_payload(payload)
            if needle is not None:
                return needle
        return None


[docs]class Plasma(Layout): """ A flexible tree-based layout. Each tree node represents a container whose children are aligned either horizontally or vertically. Each window is attached to a leaf of the tree and takes either a calculated relative amount or a custom absolute amount of space in its parent container. Windows can be resized, rearranged and integrated into other containers. Windows in a container will all open in the same direction. Calling ``lazy.layout.mode_vertical/horizontal()`` will insert a new container allowing windows to be added in the new direction. You can use the ``Plasma`` widget to show which mode will apply when opening a new window based on the currently focused node. Windows can be focused selectively by using ``lazy.layout.up/down/left/right()`` to focus the nearest window in that direction relative to the currently focused window. "Integrating" windows is best explained with an illustation. Starting with three windows, a, b, c. b is currently focused. Calling ``lazy.layout.integrate_left()`` will have the following effect: :: ---------------------- ---------------------- | a | b | c | | a | c | | | | | | | | | | | | --> | | | | | | | |----------| | | | | | | b | | | | | | | | | | | | | | | | ---------------------- ---------------------- Finally, windows can me moved around the layout with ``lazy.layout.move_up/down/left/right()``. Example keybindings: .. code:: python from libqtile.config import EzKey from libqtile.lazy import lazy ... keymap = { 'M-h': lazy.layout.left(), 'M-j': lazy.layout.down(), 'M-k': lazy.layout.up(), 'M-l': lazy.layout.right(), 'M-S-h': lazy.layout.move_left(), 'M-S-j': lazy.layout.move_down(), 'M-S-k': lazy.layout.move_up(), 'M-S-l': lazy.layout.move_right(), 'M-A-h': lazy.layout.integrate_left(), 'M-A-j': lazy.layout.integrate_down(), 'M-A-k': lazy.layout.integrate_up(), 'M-A-l': lazy.layout.integrate_right(), 'M-d': lazy.layout.mode_horizontal(), 'M-v': lazy.layout.mode_vertical(), 'M-S-d': lazy.layout.mode_horizontal_split(), 'M-S-v': lazy.layout.mode_vertical_split(), 'M-a': lazy.layout.grow_width(30), 'M-x': lazy.layout.grow_width(-30), 'M-S-a': lazy.layout.grow_height(30), 'M-S-x': lazy.layout.grow_height(-30), 'M-C-5': lazy.layout.size(500), 'M-C-8': lazy.layout.size(800), 'M-n': lazy.layout.reset_size(), } keys = [EzKey(k, v) for k, v in keymap.items()] Acknowledgements: This layout was developed by numirias and published at https://github.com/numirias/qtile-plasma A few minor amendments have been made to that layout as part of incorporating this into the main qtile codebase but the majority of the work is theirs. """ defaults = [ ("name", "Plasma", "Layout name"), ("border_normal", "#333333", "Unfocused window border color"), ("border_focus", "#00e891", "Focused window border color"), ("border_normal_fixed", "#333333", "Unfocused fixed-size window border color"), ("border_focus_fixed", "#00e8dc", "Focused fixed-size window border color"), ("border_width", 1, "Border width"), ("border_width_single", 0, "Border width for single window"), ("margin", 0, "Layout margin"), ( "fair", False, "When ``False`` effort will be made to preserve nodes with a fixed size. " "Set to ``True`` to enable new windows to take more space from fixed size nodes.", ), ] # If windows are added before configure() was called, the screen size is # still unknown, so we need to set some arbitrary initial root dimensions default_dimensions = (0, 0, 1000, 1000) def __init__(self, **config): Layout.__init__(self, **config) self.add_defaults(Plasma.defaults) self.root = Node(None, *self.default_dimensions) self._focused = None self._add_mode = None Node.priority = Priority.BALANCED if self.fair else Priority.FIXED @staticmethod def convert_names(tree): return [Plasma.convert_names(n) if isinstance(n, list) else n.payload.name for n in tree] @property def add_mode(self): if self._add_mode is None: node = self.root_or_focused_node if node.width >= node.height: return AddMode.HORIZONTAL else: return AddMode.VERTICAL return self._add_mode @add_mode.setter def add_mode(self, value): self._add_mode = value # We trigger a redraw so that the different borders can be drawn based on the add_mode # We check self._group to avoid raising a runtime error from libqtile.layout.base if self._group is not None: hook.fire("plasma_add_mode", self) self.group.layout_all() @property def focused(self): return self._focused @focused.setter def focused(self, value): self._focused = value hook.fire("plasma_add_mode", self) @property def focused_node(self): return self.root.find_payload(self.focused) @property def root_or_focused_node(self): return self.root if self.focused_node is None else self.focused_node @property def horizontal(self): if self.focused_node is None: return True if self.add_mode is not None: if self.add_mode & AddMode.HORIZONTAL: return True else: return False if self.focused_node.parent is None: if self.focused_node.orient is Orient.HORIZONTAL: return True else: return False return self.focused_node.parent.horizontal @property def vertical(self): return not self.horizontal @property def split(self): if self.add_mode is not None and self.add_mode & AddMode.SPLIT: return True return False
[docs] @expose_command def info(self): info = super().info() tree = self.convert_names(self.root.tree) info["tree"] = tree info["clients"] = flatten(tree) return info
def clone(self, group): clone = copy.copy(self) clone._group = group clone.root = Node(None, *self.default_dimensions) clone.focused = None clone.add_mode = None return clone def get_windows(self): clients = [] for leaf in self.root.all_leafs: if leaf.payload is not None: clients.append(leaf.payload) return clients def add_client(self, client): new = Node(client) try: self.root.restore(new) except NotRestorableError: self.root_or_focused_node.add_node(new, self.add_mode) self.add_mode = None def remove(self, client): self.root.find_payload(client).remove() def configure(self, client, screen_rect): self.root.x = screen_rect.x self.root.y = screen_rect.y self.root.width = screen_rect.width self.root.height = screen_rect.height node = self.root.find_payload(client) border_width = self.border_width_single if self.root.tree == [node] else self.border_width border_color = getattr( self, "border_" + ("focus" if client.has_focus else "normal") + ("" if node.flexible else "_fixed"), ) x, y, width, height = node.pixel_perfect client.place( x, y, width - 2 * border_width, height - 2 * border_width, border_width, border_color, margin=self.margin, ) # Always keep tiles below floating windows client.unhide() def focus(self, client): self.focused = client self.root.find_payload(client).access() def focus_first(self): return self.root.first_leaf.payload def focus_last(self): return self.root.last_leaf.payload def focus_next(self, win): next_leaf = self.root.find_payload(win).next_leaf return None if next_leaf is self.root.first_leaf else next_leaf.payload def focus_previous(self, win): prev_leaf = self.root.find_payload(win).prev_leaf return None if prev_leaf is self.root.last_leaf else prev_leaf.payload def focus_node(self, node): if node is None: return self.group.focus(node.payload) def refocus(self): self.group.focus(self.focused)
[docs] @expose_command def next(self): """Focus next window.""" self.focus_node(self.focused_node.next_leaf)
[docs] @expose_command def previous(self): """Focus previous window.""" self.focus_node(self.focused_node.prev_leaf)
[docs] @expose_command def recent(self): """Focus most recently focused window. (Toggles between the two latest active windows.) """ nodes = [n for n in self.root.all_leafs if n is not self.focused_node] most_recent = max(nodes, key=lambda n: n.last_accessed) self.focus_node(most_recent)
[docs] @expose_command def left(self): """Focus window to the left.""" self.focus_node(self.focused_node.close_left)
[docs] @expose_command def right(self): """Focus window to the right.""" self.focus_node(self.focused_node.close_right)
[docs] @expose_command def up(self): """Focus window above.""" self.focus_node(self.focused_node.close_up)
[docs] @expose_command def down(self): """Focus window below.""" self.focus_node(self.focused_node.close_down)
[docs] @expose_command def move_left(self): """Move current window left.""" self.focused_node.move_left() self.refocus()
[docs] @expose_command def move_right(self): """Move current window right.""" self.focused_node.move_right() self.refocus()
[docs] @expose_command def move_up(self): """Move current window up.""" self.focused_node.move_up() self.refocus()
[docs] @expose_command def move_down(self): """Move current window down.""" self.focused_node.move_down() self.refocus()
[docs] @expose_command def integrate_left(self): """Integrate current window left.""" self.focused_node.integrate_left() self.refocus()
[docs] @expose_command def integrate_right(self): """Integrate current window right.""" self.focused_node.integrate_right() self.refocus()
[docs] @expose_command def integrate_up(self): """Integrate current window up.""" self.focused_node.integrate_up() self.refocus()
[docs] @expose_command def integrate_down(self): """Integrate current window down.""" self.focused_node.integrate_down() self.refocus()
[docs] @expose_command def mode_horizontal(self): """Next window will be added horizontally.""" self.add_mode = AddMode.HORIZONTAL
[docs] @expose_command def mode_vertical(self): """Next window will be added vertically.""" self.add_mode = AddMode.VERTICAL
[docs] @expose_command def mode_horizontal_split(self): """Next window will be added horizontally, splitting space of current window. """ self.add_mode = AddMode.HORIZONTAL | AddMode.SPLIT
[docs] @expose_command def mode_vertical_split(self): """Next window will be added vertically, splitting space of current window. """ self.add_mode = AddMode.VERTICAL | AddMode.SPLIT
[docs] @expose_command def set_size(self, x: int): """Change size of current window. (It's recommended to use `width()`/`height()` instead.) """ self.focused_node.size = x self.refocus()
[docs] @expose_command def set_width(self, x: int): """Set width of current window.""" self.focused_node.width = x self.refocus()
[docs] @expose_command def set_height(self, x: int): """Set height of current window.""" self.focused_node.height = x self.refocus()
[docs] @expose_command def reset_size(self): """Reset size of current window to automatic (relative) sizing.""" self.focused_node.reset_size() self.refocus()
[docs] @expose_command def grow(self, x: int): """Grow size of current window. (It's recommended to use `grow_width()`/`grow_height()` instead.) """ self.focused_node.size += x self.refocus()
[docs] @expose_command def grow_width(self, x: int): """Grow width of current window.""" self.focused_node.width += x self.refocus()
[docs] @expose_command def grow_height(self, x: int): """Grow height of current window.""" self.focused_node.height += x self.refocus()