import itertools
import os
from libqtile import bar, hook
from libqtile.images import Img
from libqtile.log_utils import logger
from libqtile.widget import base
[docs]
class CurrentLayout(base._TextBox):
"""
Display the icon or the name of the current layout of the current group
of the screen on which the bar containing the widget is.
If you are using custom layouts, a default icon with question mark
will be displayed for them. If you want to use custom icon for your own
layout, for example, `FooGrid`, then create a file named
"layout-foogrid.png" and place it in `~/.icons` directory. You can as well
use other directories, but then you need to specify those directories
in `custom_icon_paths` argument for this plugin.
The widget will look for icons with a `png` or `svg` extension.
The order of icon search is:
- dirs in `custom_icon_paths` config argument
- `~/.icons`
- built-in qtile icons
"""
defaults = [
("icon_first", True, "When ``mode='both'``, the icon will display before the text."),
(
"mode",
"text",
"Display only the name of the layout with ``text`` "
"or only the icon of the layout with ``icon``. "
"Altenatively display the two in order with ``both``",
),
("scale", 1, "Icon's scale factor relative to the bar height."),
(
"custom_icon_paths",
[],
"List of folders where to search icons before "
"using built-in icons or icons in ~/.icons dir. "
"This can also be used to provide "
"missing icons for custom layouts.",
),
]
def __init__(self, width=bar.CALCULATED, **config):
base._TextBox.__init__(self, "", width, **config)
self.add_defaults(CurrentLayout.defaults)
self.icons_loaded = False
self.img_length = 0
def _configure(self, qtile, bar):
assert self.mode in ("text", "icon", "both")
base._TextBox._configure(self, qtile, bar)
layout_id = self.bar.screen.group.current_layout
self.text = self.bar.screen.group.layouts[layout_id].name
self.icon_paths = []
self.surfaces = {}
self._update_icon_paths()
self._setup_images()
self.setup_hooks()
self.add_callbacks(
{
"Button1": qtile.next_layout,
"Button2": qtile.prev_layout,
"Button3": self.change_draw_method,
}
)
@property
def current_layout(self):
return self.text
def calculate_length(self):
if self.mode == "text":
return base._TextBox.calculate_length(self)
elif self.mode == "icon":
return self.img_length + self.padding * 2
# self.mode == "both"
# add only one padding because base._TextBox.calculate_length already add two
return base._TextBox.calculate_length(self) + self.img_length + self.padding
def hook_response(self, layout, group):
if group.screen is not None and group.screen == self.bar.screen:
self.text = layout.name
self.bar.draw()
def setup_hooks(self):
"""
Listens for layout change and performs a redraw when it occurs.
"""
hook.subscribe.layout_change(self.hook_response)
def remove_hooks(self):
"""
Listens for layout change and performs a redraw when it occurs.
"""
hook.unsubscribe.layout_change(self.hook_response)
def change_draw_method(self):
if self.mode == "both":
return
self.mode = "text" if self.mode != "text" else "icon"
self.bar.draw()
def draw(self):
if self.mode != "text":
self.draw_icon()
else:
base._TextBox.draw(self)
def draw_icon(self):
if not self.icons_loaded:
return
try:
surface = self.surfaces[self.current_layout]
except KeyError:
logger.error("No icon for layout %s", self.current_layout)
return
self.drawer.clear(self.background or self.bar.background)
self.drawer.ctx.save()
self.rotate_drawer()
translatex, translatey = self.width, self.height
if self.mode == "both":
y = (self.bar.size - self.layout.height) / 2 + 1
if self.bar.horizontal:
if self.icon_first:
# padding - icon - padding - text - padding
x = self.padding + self.img_length + self.padding
translatex -= base._TextBox.calculate_length(self) - self.padding
else:
# padding - text - padding - icon - padding
x = self.padding
translatex += base._TextBox.calculate_length(self) - self.padding
elif self.rotate:
if self.icon_first:
# padding - icon - padding - text - padding
x = self.padding + self.img_length + self.padding
translatey -= base._TextBox.calculate_length(self) - self.padding
else:
# padding - text - padding - icon - padding
x = self.padding
translatey += base._TextBox.calculate_length(self) - self.padding
else:
x = 0
if self.icon_first:
# padding - icon - padding - text - padding
y = self.padding + self.img_length + self.padding
translatey -= base._TextBox.calculate_length(self) - self.padding
else:
# padding - text - padding - icon - padding
y = self.padding
translatey += base._TextBox.calculate_length(self) - self.padding
self.layout.draw(x, y)
if not self.bar.horizontal and self.rotate:
translatex, translatey = translatey, translatex
self.drawer.ctx.translate(
(translatex - surface.width) / 2,
(translatey - surface.height) / 2,
)
self.drawer.ctx.set_source(surface.pattern)
self.drawer.ctx.paint()
self.drawer.ctx.restore()
self.draw_at_default_position()
def _get_layout_names(self):
"""
Returns a sequence of tuples of layout name and lowercased class name
strings for each available layout.
"""
layouts = itertools.chain(
self.qtile.config.layouts,
(layout for group in self.qtile.config.groups for layout in group.layouts),
)
return set((layout.name, layout.__class__.__name__.lower()) for layout in layouts)
def _update_icon_paths(self):
self.icon_paths = []
# We allow user to override icon search path
self.icon_paths.extend(os.path.expanduser(path) for path in self.custom_icon_paths)
# We also look in ~/.icons/ and ~/.local/share/icons
self.icon_paths.append(os.path.expanduser("~/.icons"))
self.icon_paths.append(os.path.expanduser("~/.local/share/icons"))
# Default icons are in libqtile/resources/layout-icons.
# If using default config without any custom icons,
# this path will be used.
root = os.sep.join(os.path.abspath(__file__).split(os.sep)[:-2])
self.icon_paths.append(os.path.join(root, "resources", "layout-icons"))
def find_icon_file_path(self, layout_name):
for icon_path in self.icon_paths:
for extension in ["png", "svg"]:
icon_filename = f"layout-{layout_name}.{extension}"
icon_file_path = os.path.join(icon_path, icon_filename)
if os.path.isfile(icon_file_path):
return icon_file_path
def _setup_images(self):
"""
Loads layout icons.
"""
new_height = (self.bar.size - 2) * self.scale
for names in self._get_layout_names():
layout_name = names[0]
# Python doesn't have an ordered set but we can use a dictionary instead
# First key is the layout's name (which may have been set by the user),
# the second is the class name. If these are the same (i.e. the user hasn't
# set a name) then there is only one key in the dictionary.
layouts = dict.fromkeys(names)
for layout in layouts.keys():
icon_file_path = self.find_icon_file_path(layout)
if icon_file_path:
break
else:
logger.warning('No icon found for layout "%s"', layout_name)
icon_file_path = self.find_icon_file_path("unknown")
img = Img.from_path(icon_file_path)
img.resize(height=new_height)
img_length = img.width if self.bar.horizontal else img.height
if img_length > self.img_length:
self.img_length = img_length
self.surfaces[layout_name] = img
self.icons_loaded = True
def finalize(self):
self.remove_hooks()
base._TextBox.finalize(self)