Source code for libqtile.widget.currentlayout

# Copyright (c) 2011 Florian Mounier
# Copyright (c) 2011 Kenji_Takahashi
# Copyright (c) 2012 roger
# Copyright (c) 2012, 2014 Tycho Andersen
# Copyright (c) 2012 Maximilian Köhl
# Copyright (c) 2013 Craig Barnes
# Copyright (c) 2014 Sean Vig
# Copyright (c) 2014 Adi Sieker
#
# 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 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.actual_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.actual_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": if self.bar.horizontal: height = self.bar.height if self.icon_first: # padding - icon - padding - text - padding x = self.actual_padding + self.img_length + self.actual_padding translatex -= base._TextBox.calculate_length(self) - self.actual_padding else: # padding - text - padding - icon - padding x = self.actual_padding translatex += base._TextBox.calculate_length(self) - self.actual_padding elif self.rotate: height = self.bar.width if self.icon_first: # padding - icon - padding - text - padding x = self.actual_padding + self.img_length + self.actual_padding translatey -= base._TextBox.calculate_length(self) - self.actual_padding else: # padding - text - padding - icon - padding x = self.actual_padding translatey += base._TextBox.calculate_length(self) - self.actual_padding else: x = 0 if self.icon_first: # padding - icon - padding - text - padding height = self.actual_padding + self.img_length + self.actual_padding translatey -= base._TextBox.calculate_length(self) - self.actual_padding else: # padding - text - padding - icon - padding height = self.actual_padding translatey += base._TextBox.calculate_length(self) - self.actual_padding # neutralize all math in the layout.draw() below # to simulate starting height from zero height = (height * 2) + self.layout.height - 2 self.layout.draw(x, int(height / 2 - self.layout.height / 2) + 1) 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. """ width = (self.bar.width - 2) * self.scale if not self.bar.horizontal else None height = (self.bar.height - 2) * self.scale if self.bar.horizontal else None 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(width=width, height=height) if img.width > self.img_length: self.img_length = img.width self.surfaces[layout_name] = img self.icons_loaded = True def finalize(self): self.remove_hooks() base._TextBox.finalize(self)