# -*- coding: utf-8 -*- # Copyright (c) 2011-2012 Dustin Lacewell # Copyright (c) 2011 Mounier Florian # Copyright (c) 2012 Craig Barnes # Copyright (c) 2012 Maximilian Köhl # Copyright (c) 2012, 2014-2015 Tycho Andersen # Copyright (c) 2013 jpic # Copyright (c) 2013 babadoo # Copyright (c) 2013 Jure Ham # Copyright (c) 2013 Tao Sauvage # Copyright (c) 2014 ramnes # Copyright (c) 2014 Sean Vig # Copyright (c) 2014 dmpayton # Copyright (c) 2014 dequis # Copyright (c) 2014 Florian Scherf # Copyright (c) 2017 Dirk Hartmann # Copyright (c) 2021 Jakob Helmecke # # 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 libqtile.layout.base import _SimpleLayoutBase class MonadTall(_SimpleLayoutBase): """Emulate the behavior of XMonad's default tiling scheme. Master-Pane: A master pane that contains a set number of windows takes up a vertical portion of the screen_rect based on the ratio setting. This ratio can be adjusted with the ``cmd_grow_master`` and ``cmd_shrink_master``. :: --------------------- | | | | | | | | | | | | | | | | | | --------------------- Using the ``cmd_flip`` method will switch which horizontal side the master pane will occupy. The master pane is considered the "top" of the stack. :: --------------------- | | | | | | | | | | | | | | | | | | --------------------- Slave-panes: Occupying the rest of the screen_rect are one or more slave panes. The slave panes will share the vertical space of the screen_rect. :: --------------------- | | | | |______| | | | | |______| | | | | | | --------------------- Panes can be moved with the ``cmd_shuffle_up`` and ``cmd_shuffle_down`` methods. As mentioned the master pane is considered the top of the stack; moving up is counter-clockwise and moving down is clockwise. The opposite is true if the layout is "flipped". :: --------------------- --------------------- | | 2 | | 2 | | | |______| |_______| | | | 3 | | 3 | | | 1 |______| |_______| 1 | | | 4 | | 4 | | | | | | | | --------------------- --------------------- Normalizing/Resetting: To restore master slave ratio use the ``cmd_normalize`` method. To reset the layout to its default state, including the master windows, master slave ratio and flip, use the ``cmd_reset`` method. Maximizing: To toggle a maximized layout, showing a single window, simply use the ``cmd_maximize`` on a focused client. Suggested Bindings:: # these mirror the xmonad default config Key([modkey], "j", lazy.layout.down()), Key([modkey], "k", lazy.layout.up()), Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), Key([modkey], "h", lazy.layout.shink_master()), Key([modkey], "l", lazy.layout.grow_master()), Key([modkey], "n", lazy.layout.normalize()), Key([modkey], "m", lazy.layout.master()), Key([modkey], "comma", lazy.layout.decrease_nmaster()), Key([modkey], "period", lazy.layout.increase_nmaster()), Key([modkey], "Return", lazy.layout.swap_master()) # keybindings not in xmonad default config Key([modkey, "shift"], "n", lazy.layout.reset()), Key([modkey], "space", lazy.layout.flip()), Key([modkey, "shift"], "space", lazy.layout.flip_master()), Key([modkey, "shift"], "m", lazy.layout.maximize()), """ _left = 0 _right = 1 _vert = 0 _hori = 1 _med_ratio = 0.5 defaults = [ ("border_focus", "#ff0000", "Border colour(s) for the focused window."), ("border_normal", "#000000", "Border colour(s) for un-focused windows."), ("border_width", 2, "Border width."), ("single_border_width", None, "Border width for single window"), ("single_margin", None, "Margin size for single window"), ("margin", 0, "Margin of the layout"), ( "ratio", 0.5, "The percent of the screen-space the master pane should occupy by default.", ), ( "min_ratio", 0.25, "The percent of the screen-space the master pane should occupy at minimum.", ), ( "max_ratio", 0.75, "The percent of the screen-space the master pane should occupy at maximum.", ), ( "align", _left, "Which side master plane will be placed (one of ``MonadTall._left`` or " "``MonadTall._right``)", ), ("change_ratio", 0.05, "Resize ratio"), ( "new_client_position", "before_current", "Place new windows: " " after_current - after the active window." " before_current - before the active window," " top - at the top of the stack," " bottom - at the bottom of the stack,", ), ( "master_length", 1, "Amount of windows displayed in the master stack. Surplus windows will be moved to " "the slave stack.", ), ( "orientation", _vert, "Orientation in which master windows will be " "placed (one of ``MonadTall._vert`` or ``MonadTall._hori``)", ), ("maximized", False, "Start maximized"), ] def __init__(self, **config): _SimpleLayoutBase.__init__(self, **config) self.add_defaults(MonadTall.defaults) if self.single_border_width is None: self.single_border_width = self.border_width if self.single_margin is None: self.single_margin = self.margin self.screen_rect = None @property def focused(self): return self.clients.current_index @property def master_windows(self): return self.clients[: self.master_length] @property def slave_windows(self): return self.clients[self.master_length :] def clone(self, group): "Clone layout for other groups" c = _SimpleLayoutBase.clone(self, group) c.screen_rect = group.screen.get_rect() if group.screen else None c.ratio = self.ratio c.align = self.align c.orientation = self.orientation return c def add(self, client): "Add client to layout" self.clients.add(client, client_position=self.new_client_position) self.do_normalize = True def remove(self, client): "Remove client from layout" self.do_normalize = True return self.clients.remove(client) def cmd_normalize(self, redraw=True): "Evenly distribute screen-space between master and slave pane" if redraw: self.ratio = self._med_ratio self.group.layout_all() self.do_normalize = False def cmd_reset(self, redraw=True): "Reset Layout." self.ratio = self._med_ratio if self.align == self._right: self.align = self._left if self.orientation == self._hori: self.orientation = self._vert self.master_length = 1 self.cmd_normalize(redraw) def cmd_maximize(self): "Grow the currently focused client to the max size" if self.maximized: self.maximized = False else: self.maximized = True self.group.layout_all() def configure(self, client, screen_rect): "Position client based on order and sizes" self.screen_rect = screen_rect if self.do_normalize: self.cmd_normalize(False) # if client not in this layout if not self.clients or client not in self.clients: client.hide() return # determine focus border-color if client.has_focus: px = self.border_focus else: px = self.border_normal # single client - fullscreen if len(self.clients) == 1 or self.maximized: if self.clients and client is self.clients.current_client: client.place( self.screen_rect.x, self.screen_rect.y, self.screen_rect.width - 2 * self.single_border_width, self.screen_rect.height - 2 * self.single_border_width, self.single_border_width, px, margin=self.single_margin, ) client.unhide() else: client.hide() return cidx = self.clients.index(client) self._configure_specific(client, screen_rect, px, cidx) client.unhide() def _configure_specific(self, client, screen_rect, px, cidx): """Specific configuration for xmonad tall.""" self.screen_rect = screen_rect # calculate master/slave pane size width_master = int(self.screen_rect.width * self.ratio) width_slave = self.screen_rect.width - width_master if len(self.master_windows) == 0: width_master = 0 width_slave = self.screen_rect.width if len(self.slave_windows) == 0: width_master = self.screen_rect.width width_slave = 0 # calculate client's x offset if self.align == self._left: # left or up orientation if client in self.master_windows: # master client xpos = self.screen_rect.x else: # slave client xpos = self.screen_rect.x + width_master else: # right or down orientation if client in self.master_windows: # master client xpos = self.screen_rect.x + width_slave - self.margin else: # slave client xpos = self.screen_rect.x # calculate client height and place if client in self.slave_windows: pos = self.clients.index(client) # slave client width = width_slave - 2 * self.border_width # ypos is the sum of all clients above it height = self.screen_rect.height // len(self.slave_windows) ypos = self.screen_rect.y + self.clients[self.master_length :].index(client) * height # fix double margin if cidx > 1: ypos -= self.margin height += self.margin # place client based on calculated dimensions client.place( xpos, ypos, width, height - 2 * self.border_width, self.border_width, px, margin=self.margin, ) else: pos = self.clients.index(client) if self.orientation == self._vert: height = self.screen_rect.height // self.master_length width = width_master ypos = self.screen_rect.y + pos * height else: height = self.screen_rect.height width = width_master // self.master_length overflow = width_master % self.master_length ypos = self.screen_rect.y if self.align == self._left: xpos = self.screen_rect.x + pos * width + overflow else: xpos = self.screen_rect.x + width_slave + pos * width # master client client.place( xpos, ypos, width, height, self.border_width, px, margin=[ self.margin, 2 * self.border_width, self.margin + 2 * self.border_width, self.margin, ], ) def info(self): d = _SimpleLayoutBase.info(self) d.update( dict( master=[c.name for c in self.master_windows], slave=[c.name for c in self.slave_windows], ) ) return d def _grow_master(self, amt): """Will grow the client that is currently in the master pane""" self.ratio += amt self.ratio = min(self.max_ratio, self.ratio) def cmd_grow_master(self): """Grow master pane Will grow the master pane, reducing the size of clients in the slave pane. """ self._grow_master(self.change_ratio) self.group.layout_all() def cmd_shrink_master(self): """Shrink master pane Will shrink the master pane, increasing the size of clients in the slave pane. """ self._shrink_master(self.change_ratio) self.group.layout_all() def _shrink_master(self, amt): """Will shrink the client that currently in the master pane""" self.ratio -= amt self.ratio = max(self.min_ratio, self.ratio) cmd_next = _SimpleLayoutBase.next cmd_previous = _SimpleLayoutBase.previous cmd_up = cmd_previous cmd_down = cmd_next def cmd_shuffle_up(self): """Shuffle the client up the stack""" self.clients.shuffle_up() self.group.layout_all() self.group.focus(self.clients.current_client) def cmd_shuffle_down(self): """Shuffle the client down the stack""" self.clients.shuffle_down() self.group.layout_all() self.group.focus(self.clients[self.focused]) def cmd_flip(self): """Flip the layout horizontally""" self.align = self._left if self.align == self._right else self._right self.group.layout_all() def cmd_swap(self, window1, window2): """Swap two windows""" self.clients.swap(window1, window2, 1) self.group.layout_all() self.group.focus(window1) def cmd_swap_master(self): """Swap current window to master pane""" win = self.clients.current_client cidx = self.clients.index(win) if cidx < self.master_length - 1: target = self.clients[cidx + 1] else: target = self.clients[0] self.cmd_swap(win, target) def cmd_decrease_nmaster(self): """Decrease number of windows in master pane""" self.master_length -= 1 if self.master_length <= 0: self.master_length = 0 self.group.layout_all() def cmd_increase_nmaster(self): """Increase number of windows in master pane""" self.master_length += 1 if self.master_length >= len(self.clients): self.master_length = len(self.clients) self.group.layout_all() def cmd_master(self): """Focus windows in master pane""" win = self.clients.current_client cidx = self.clients.index(win) if cidx < self.master_length - 1: self.group.focus(self.clients[cidx + 1]) else: self.group.focus(self.clients[0]) def cmd_flip_master(self): """Flip the layout horizontally""" self.orientation = self._vert if self.orientation == self._hori else self._hori self.group.layout_all() class MonadWide(MonadTall): """Emulate the behavior of XMonad's horizontal tiling scheme. This layout attempts to emulate the behavior of XMonad wide tiling scheme. Master-Pane: A master pane that contains a single window takes up a horizontal portion of the screen_rect based on the ratio setting. This ratio can be adjusted with the ``cmd_grow_master`` and ``cmd_shrink_master`` or. :: --------------------- | | | | | | |___________________| | | | | --------------------- Using the ``cmd_flip`` method will switch which vertical side the master pane will occupy. The master pane is considered the "top" of the stack. :: --------------------- | | |___________________| | | | | | | | | --------------------- Slave-panes: Occupying the rest of the screen_rect are one or more slave panes. The slave panes will share the horizontal space of the screen_rect. :: --------------------- | | | | | | |___________________| | | | | | | | | --------------------- Panes can be moved with the ``cmd_shuffle_up`` and ``cmd_shuffle_down`` methods. As mentioned the master pane is considered the top of the stack; moving up is counter-clockwise and moving down is clockwise. The opposite is true if the layout is "flipped". :: --------------------- --------------------- | | | 2 | 3 | 4 | | 1 | |_____|_______|_____| | | | | |___________________| | | | | | | | 1 | | 2 | 3 | 4 | | | --------------------- --------------------- Normalizing/Resetting: To restore master slave ratio use the ``cmd_normalize`` method. To reset the layout to its default state, including the master windows, master slave ratio and flip, use the ``cmd_reset`` method. Maximizing: To toggle a maximized layout, showing a single window, simply use the ``cmd_maximize`` on a focused client. Suggested Bindings:: # these mirror the xmonad default config Key([modkey], "j", lazy.layout.down()), Key([modkey], "k", lazy.layout.up()), Key([modkey, "shift"], "j", lazy.layout.shuffle_down()), Key([modkey, "shift"], "k", lazy.layout.shuffle_up()), Key([modkey], "h", lazy.layout.shink_master()), Key([modkey], "l", lazy.layout.grow_master()), Key([modkey], "n", lazy.layout.normalize()), Key([modkey], "m", lazy.layout.master()), Key([modkey], "comma", lazy.layout.decrease_nmaster()), Key([modkey], "period", lazy.layout.increase_nmaster()), Key([modkey], "Return", lazy.layout.swap_master()) # keybindings not in xmonad default config Key([modkey, "shift"], "n", lazy.layout.reset()), Key([modkey], "space", lazy.layout.flip()), Key([modkey, "shift"], "space", lazy.layout.flip_master()), Key([modkey, "shift"], "m", lazy.layout.maximize()), """ _up = 0 _down = 1 _hori = 0 _vert = 1 def _configure_specific(self, client, screen_rect, px, cidx): """Specific configuration for xmonad wide.""" self.screen_rect = screen_rect # calculate master/slave column widths height_master = int(self.screen_rect.height * self.ratio) height_slave = self.screen_rect.height - height_master if len(self.master_windows) == 0: height_master = 0 height_slave = self.screen_rect.height if len(self.slave_windows) == 0: height_master = self.screen_rect.height height_slave = 0 # calculate client's x offset if self.align == self._up: # up orientation if client in self.master_windows: # master client ypos = self.screen_rect.y else: # slave client ypos = self.screen_rect.y + height_master else: # right or down orientation if client in self.master_windows: # master client ypos = self.screen_rect.y + height_slave - self.margin else: # slave client ypos = self.screen_rect.y # calculate client height and place if client in self.slave_windows: # slave client height = height_slave - 2 * self.border_width # xpos is the sum of all clients left of it width = self.screen_rect.width // len(self.slave_windows) xpos = self.screen_rect.x + self.clients[self.master_length :].index(client) * width # get width from precalculated width list width = self.screen_rect.width // len(self.slave_windows) # fix double margin if cidx > 1: xpos -= self.margin width += self.margin # place client based on calculated dimensions client.place( xpos, ypos, width - 2 * self.border_width, height, self.border_width, px, margin=self.margin, ) else: pos = self.clients.index(client) if self.orientation == self._hori: width = self.screen_rect.width // self.master_length height = height_master xpos = self.screen_rect.x + pos * width else: width = self.screen_rect.width height = height_master // self.master_length overflow = height_master % self.master_length xpos = self.screen_rect.x if self.align == self._up: ypos = self.screen_rect.y + pos * height + overflow else: ypos = self.screen_rect.y + height_slave + pos * height # master client client.place( xpos, ypos, width, height, self.border_width, px, margin=[ self.margin, self.margin + 2 * self.border_width, 2 * self.border_width, self.margin, ], )