qtile-config/layouts/tabbed.py
2025-02-26 13:26:54 +01:00

312 lines
9.8 KiB
Python

"""Proof-of-concept for a Tabbed layout in Qtile.
See GitHub page for more information: https://github.com/hanschen/qtile_tabbed
Created by Hans Chen (contact@hanschen.org).
"""
from libqtile import hook
from libqtile.command.base import expose_command
from libqtile.layout.base import _ClientList, _SimpleLayoutBase
from libqtile.layout.base import Layout
def count_windows(group, include_floating=True):
count = 0
for window in group.windows:
if include_floating or not window.floating:
count += 1
return count
class Tab:
"""A tab representing a window."""
def __init__(self, window):
self.window = window
self._left = 0
self._right = 0
def button_press(self, x, y):
del y
if self._left <= x < self._right:
return self
def draw(self, layout, left):
if not layout.group.screen:
return
layout._layout.font_size = layout.fontsize
layout._layout.text = self.window.name
if self.window is layout.clients.current_client:
fg = layout.active_fg
bg = layout.active_bg
elif self.window.urgent:
fg = layout.urgent_fg
bg = layout.urgent_bg
else:
fg = layout.inactive_fg
bg = layout.inactive_bg
ntabs = len(layout.clients)
width = layout.group.screen.width / ntabs
layout._layout.width = width
layout._layout.colour = fg
# get a text frame from the above
framed = layout._layout.framed(
border_width=0,
border_color=bg,
pad_x=0,
pad_y=layout.padding_y,
)
# draw the text frame at the given point
framed.draw_fill(left, 0, rounded=layout.rounded_tabs)
self._left = left
self._right = left + framed.width
left += framed.width + layout.hspace
return left
class ClientList(_ClientList):
"""Similar to libqtile.layout.base._ClientList, but allows wraping when
shuffling windows.
"""
def shuffle_up(self, maintain_index=True):
"""
Shuffle the list. The current client swaps position with its
predecessor. If maintain_index is True the current_index is adjusted,
such that the same client stays current and goes up in list.
"""
idx = self._current_idx
if idx > 0:
self.clients[idx], self.clients[idx - 1] = self[idx - 1], self[idx]
if maintain_index:
self.current_index -= 1
else:
self.clients.append(self.clients.pop(0))
if maintain_index:
self.current_index = len(self.clients) - 1
def shuffle_down(self, maintain_index=True):
"""
Shuffle the list. The current client swaps position with its successor.
If maintain_index is True the current_index is adjusted,
such that the same client stays current and goes down in list.
"""
idx = self._current_idx
if idx + 1 < len(self.clients):
self.clients[idx], self.clients[idx + 1] = self[idx + 1], self[idx]
if maintain_index:
self.current_index += 1
else:
self.clients.insert(0, self.clients.pop(-1))
if maintain_index:
self.current_index = 0
class Tabbed(_SimpleLayoutBase):
"""Tabbed layout
A simple layout that displays one window at a time, similar to the Max
layout. The major difference from Max is that Tabbed will show a tab bar
with all windows if there are more than one or two windows in the layout,
depending on your settings.
"""
defaults = [
("border_width", 0, "Border width"),
("border_focus", None, "Border color for focused window"),
("border_normal", None, "Border color for unfocused window"),
("margin", 0, "Margin"),
("bg_color", "000000", "Background color of tab bar"),
("active_fg", "ffffff", "Foreground color of active tab"),
("active_bg", "000080", "Background color of active tab"),
("urgent_fg", "ffffff", "Foreground color of urgent tab"),
("urgent_bg", "ff0000", "Background color of urgent tab"),
("inactive_fg", "ffffff", "Foreground color of inactive tab"),
("inactive_bg", "606060", "Background color of inactive tab"),
("rounded_tabs", False, "Draw tabs rounded"),
("padding_y", 2, "Top and bottom padding for tab label"),
("hspace", 2, "Space between tabs"),
("font", "sans", "Font"),
("fontsize", 14, "Font pixel size"),
("fontshadow", None, "Font shadow color, default is None (no shadow)"),
("bar_height", 24, "Height of tab bar"),
("place_bottom", False, "Place tab bar at the bottom instead of top"),
("show_single_tab", True, "Show tabs if there is only a single tab"),
]
def __init__(self, **config):
_SimpleLayoutBase.__init__(self, **config)
self.clients = ClientList()
self.add_defaults(Tabbed.defaults)
self._drawer = None
self._panel = None
self._tabs = {}
def add_client(self, client):
tab = Tab(client)
self._tabs[client] = tab
return super().add_client(client, 1)
def clone(self, group):
c = Layout.clone(self, group)
c.clients = ClientList()
return c
def configure(self, client, screen_rect):
if self.clients and client is self.clients.current_client:
client.place(
screen_rect.x,
screen_rect.y,
screen_rect.width - self.border_width * 2,
screen_rect.height - self.border_width * 2,
self.border_width,
self.border_focus if client.has_focus else self.border_normal,
margin=self.margin
)
client.unhide()
else:
client.hide()
@expose_command("previous")
def up(self):
_SimpleLayoutBase.previous(self)
@expose_command("next")
def down(self):
_SimpleLayoutBase.next(self)
left = up
right = down
@expose_command("shuffle_right")
def shuffle_down(self):
self.clients.shuffle_down()
self.draw_panel()
@expose_command("shuffle_left")
def shuffle_up(self):
self.clients.shuffle_up()
self.draw_panel()
def draw_panel(self, *args):
del args
if not self._panel:
return
self._drawer.clear(self.bg_color)
left = 0
for client in self.clients:
left = self._tabs[client].draw(self, left)
self._drawer.draw(height=self.bar_height)
def finalize(self):
if self._panel:
self._panel.kill()
Layout.finalize(self)
if self._drawer is not None:
self._drawer.finalize()
def hide(self):
if self._panel:
self._panel.hide()
def layout(self, windows, screen_rect):
if not self._show_tabs():
body = screen_rect
if self._panel:
self._panel.hide()
else:
if self.place_bottom:
body, panel = screen_rect.vsplit(screen_rect.height -
self.bar_height)
else:
panel, body = screen_rect.vsplit(self.bar_height)
self._resize_panel(panel)
if self._panel:
self._panel.unhide()
Layout.layout(self, windows, body)
def process_button_click(self, x, y, button):
if button == 4:
self.up()
elif button == 5:
self.down()
else:
for client in self.clients:
tab = self._tabs[client].button_press(x, y)
if tab:
self.group.focus(tab.window, False)
def remove(self, win):
super().remove(win)
self._tabs.pop(win)
self.draw_panel()
def show(self, screen_rect):
if not self._panel:
self._create_panel(screen_rect)
if not self._show_tabs():
return
if self.place_bottom:
_, panel = screen_rect.vsplit(screen_rect.height -
self.bar_height)
else:
panel, _ = screen_rect.vsplit(self.bar_height)
self._resize_panel(panel)
self._panel.unhide()
def _create_drawer(self, screen_rect):
if self._drawer is None:
self._drawer = self._panel.create_drawer(
screen_rect.width,
self.bar_height,
)
else:
self._drawer.width = screen_rect.width
self._drawer.clear(self.bg_color)
self._layout = self._drawer.textlayout(
"", "#ffffff", self.font, self.fontsize, self.fontshadow,
wrap=False
)
def _create_panel(self, screen_rect):
self._panel = self.group.qtile.core.create_internal(
screen_rect.x, screen_rect.y, screen_rect.width, self.bar_height,
)
self._create_drawer(screen_rect)
self._panel.process_window_expose = self.draw_panel
self._panel.process_button_click = self.process_button_click
hook.subscribe.client_name_updated(self.draw_panel)
hook.subscribe.focus_change(self.draw_panel)
def _resize_panel(self, screen_rect):
if self._panel:
self._panel.place(
screen_rect.x,
screen_rect.y,
screen_rect.width,
screen_rect.height,
0,
None,
)
self._create_drawer(screen_rect)
self.draw_panel()
def _show_tabs(self):
nwindows = count_windows(self.group, include_floating=False)
if self.show_single_tab:
return nwindows > 0
else:
return nwindows > 1