Commit 9cb4493cbb758c69c81c8f8403d3ca0dac54a461 Parent: 2849fe1f4524e0cb88b2e8ce62cb786262d59922 Author: mcol <mcol@posteo.net> Date: 2021-09-12 20:33:49 +0100 Committer: mcol <mcol@posteo.net> Committed: 2021-09-12 20:33:49 +0100 Move graphical notifications to dedicated plugin
notification.py Deleted
@@ -1,450 +0,0 @@ -""" -Qtile plugin that acts as a notification server and draws notification windows. - -Usage: - - import notification - - notifier = notification.Server() - - keys.extend([EzKey(k, v) for k, v in { - 'M-<grave>': notifier.lazy_prev, - 'M-S-<grave>': notifier.lazy_next, - 'C-<space>': notifier.lazy_close, - }.items()]) - -""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING - -from libqtile import configurable, hook, images, pangocffi, qtile -from libqtile.lazy import lazy -from libqtile.log_utils import logger -from libqtile.notify import notifier, ClosedReason -from libqtile.popup import Popup - -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Tuple - - from cairocffi import ImageSurface - - from libqtile.core.manager import Qtile - try: - from libqtile.notify import Notification - except ImportError: # no dbus_next - Notification = Any # type: ignore - - -class Server(configurable.Configurable): - """ - This class provides a full graphical notification manager for the - org.freedesktop.Notifications service implemented in libqtile.notify. - - The format option determines what text is shown on the popup windows, and supports - markup and new line characters e.g. '<b>{summary}</b>\n{body}'. Available - placeholders are summary, body and app_name. - - Foreground and background colours can be specified either as tuples/lists of 3 - strings, corresponding to low, normal and critical urgencies, or just a single - string which will then be used for all urgencies. The timeout and border options can - be set in the same way. - - The max_windows option limits how many popup windows can be drawn at a time. When - more notifications are recieved while the maximum number are already drawn, - notifications are queued and displayed when existing notifications are closed. - - TODO: - - text overflow - - select screen / follow mouse/keyboard focus - - critical notifications to replace any visible non-critical notifs immediately? - - hints: image-path, desktop-entry (for icon) - - hints: Server parameters set for single notification? - - hints: progress value e.g. int:value:42 with drawing - - """ - defaults = [ - ('x', 32, 'x position on screen to start drawing notifications.'), - ('y', 64, 'y position on screen to start drawing notifications.'), - ('width', 192, 'Width of notifications.'), - ('height', 64, 'Height of notifications.'), - ('format', '{summary}\n{body}', 'Text format.'), - ( - 'foreground', - ('#ffffff', '#ffffff', '#ffffff'), - 'Foreground colour of notifications, in ascending order of urgency.', - ), - ( - 'background', - ('#111111', '#111111', '#111111'), - 'Background colour of notifications, in ascending order of urgency.', - ), - ( - 'border', - ('#111111', '#111111', '#111111'), - 'Border colours in ascending order of urgency. Or None for none.', - ), - ( - 'timeout', - (5000, 5000, 0), - 'Millisecond timeout duration, in ascending order of urgency.', - ), - ('opacity', 1.0, 'Opacity of notifications.'), - ('border_width', 4, 'Line width of drawn borders.'), - ('corner_radius', None, 'Corner radius for round corners, or None.'), - ('font', 'sans', 'Font used in notifications.'), - ('font_size', 14, 'Size of font.'), - ('fontshadow', None, 'Color for text shadows, or None for no shadows.'), - ('text_alignment', 'left', 'Text alignment: left, center or right.'), - ('horizontal_padding', None, 'Padding at sides of text.'), - ('vertical_padding', None, 'Padding at top and bottom of text.'), - ('line_spacing', 4, 'Space between lines.'), - ( - 'overflow', - 'truncate', - 'How to deal with too much text: more_width, more_height, or truncate.', - ), - ('max_windows', 2, 'Maximum number of windows to show at once.'), - ('gap', 12, 'Vertical gap between popup windows.'), - ('sticky_history', True, 'Disable timeout when browsing history.'), - ('icon_size', 36, 'Pixel size of any icons.'), - ('fullscreen', 'show', 'What to do when in fullscreen: show, hide, or queue.'), - ('screen', 'focus', 'How to select a screen: focus, mouse, or an int.'), - ] - capabilities = {'body', 'body-markup', 'actions'} - # specification: https://developer.gnome.org/notification-spec/ - - def __init__(self, **config) -> None: - configurable.Configurable.__init__(self, **config) - self.add_defaults(Server.defaults) - self._hidden: List[Popup] = [] - self._shown: List[Popup] = [] - self._queue: List[Notification] = [] - self._positions: List[Tuple[int, int]] = [] - self._scroll_popup: Optional[Popup] = None - self._current_id: int = 0 - self._notif_id: Optional[int] = None - self._paused: bool = False - self._icons: Dict[str, Tuple[ImageSurface, int]] = {} - - self._make_attr_list('foreground') - self._make_attr_list('background') - self._make_attr_list('timeout') - self._make_attr_list('border') - - qtile.call_soon(asyncio.create_task, self._configure()) - - def __getattr__(self, name: str) -> Any: - """ - Using this, we can get e.g. Server.lazy_close which is the equivalent of - lazy.function(Server.close) but more convenient for setting keybindings. - """ - if name.startswith('lazy_'): - return lazy.function(getattr(self, name[5:])) - return configurable.Configurable.__getattr__(self, name) - - def _make_attr_list(self, attr: str) -> None: - """ - Turns '#000000' into ('#000000', '#000000', '#000000') - """ - value = getattr(self, attr) - if not isinstance(value, (tuple, list)): - setattr(self, attr, (value,) * 3) - - async def _configure(self) -> None: - """ - This method needs to be called to set up the Server with the Qtile manager and - create the required popup windows. - """ - if self.horizontal_padding is None: - self.horizontal_padding = self.font_size // 2 - if self.vertical_padding is None: - self.vertical_padding = self.font_size // 2 - - popup_config = { - "x": self.x, - "y": self.y, - "width": self.width, - "height": self.height, - } - - for opt in Popup.defaults: - key = opt[0] - if hasattr(self, key): - value = getattr(self, key) - if isinstance(value, (tuple, list)): - popup_config[key] = value[1] - else: - popup_config[key] = value - - for win in range(self.max_windows): - popup = Popup(qtile, **popup_config) - popup.win.process_button_click = self._process_button_click(popup) - popup.notif = None - self._hidden.append(popup) - self._positions.append( - ( - self.x, - self.y + win * (self.height + 2 * self.border_width + self.gap) - ) - ) - - await notifier.register( - self._notify, Server.capabilities, on_close=self._on_close - ) - logger.info("Notification server started up successfully") - - def _process_button_click(self, popup: Popup) -> Callable: - def _(x: int, y: int, button: int) -> None: - if button == 1: - self._act(popup) - self._close(popup, reason=ClosedReason.dismissed) - if button == 3: - self._close(popup, reason=ClosedReason.dismissed) - return _ - - def _notify(self, notif: Notification) -> None: - """ - This method is registered with the NotificationManager to handle notifications - received via dbus. They will either be drawn now or queued to be drawn soon. - """ - if self._paused: - self._queue.append(notif) - return - - if qtile.current_window and qtile.current_window.fullscreen: - if self.fullscreen != 'show': - if self.fullscreen == 'queue': - if self._unfullscreen not in hook.subscriptions: - hook.subscribe.float_change(self._unfullscreen) - self._queue.append(notif) - return - - if notif.replaces_id: - for popup in self._shown: - if notif.replaces_id == popup.notif.replaces_id: - self._shown.remove(popup) - self._send(notif, popup) - self._reposition() - return - - if self._hidden: - self._send(notif, self._hidden.pop()) - else: - self._queue.append(notif) - - def _on_close(self, nid: int) -> None: - for popup in self._shown: - self._close(popup, nid=nid, reason=ClosedReason.method) - - def _unfullscreen(self) -> None: - """ - Begin displaying of queue notifications after leaving fullscreen. - """ - if not qtile.current_window.fullscreen: - hook.unsubscribe.float_change(self._unfullscreen) - self._renotify() - - def _renotify(self) -> None: - """ - If we hold off temporarily on sending notifications and accumulate a queue, we - should use this to send the queue through self._notify again. - """ - queue = self._queue.copy() - self._queue.clear() - while queue: - self._notify(queue.pop(0)) - - def _send( - self, notif: Notification, popup: Popup, timeout: Optional[int] = None - ) -> None: - """ - Draw the desired notification using the specified Popup instance. - """ - text = self._get_text(notif) - - if "urgency" in notif.hints: - urgency = notif.hints["urgency"].value - else: - urgency = 1 - - self._current_id += 1 - popup.id = self._current_id # Used for closing the popup - popup.notif = notif # Used for finding the visible popup's notif for actions - if popup not in self._shown: - self._shown.append(popup) - popup.x, popup.y = self._get_coordinates() - popup.place() - icon = self._load_icon(notif) - popup.unhide() - - popup.background = self.background[urgency] - popup.foreground = self.foreground[urgency] - popup.clear() - - if icon: - popup.draw_image( - icon[0], - self.horizontal_padding, - 1 + (self.height - icon[1]) / 2, - ) - popup.horizontal_padding += self.icon_size + self.horizontal_padding / 2 - - for num, line in enumerate(text.split('\n')): - popup.text = line - y = self.vertical_padding + num * (popup.layout.height + self.line_spacing) - popup.draw_text(y=y) - if self.border_width: - popup.set_border(self.border[urgency]) - popup.draw() - if icon: - popup.horizontal_padding = self.horizontal_padding - - if timeout is None: - if notif.timeout is None or notif.timeout < 0: - timeout = self.timeout[urgency] - else: - timeout = notif.timeout - elif timeout < 0: - timeout = self.timeout[urgency] - if timeout > 0: - qtile.call_later(timeout / 1000, self._close, popup, self._current_id) - - def _get_text(self, notif: Notification) -> str: - summary = '' - body = '' - app_name = '' - if notif.summary: - summary = pangocffi.markup_escape_text(notif.summary) - if notif.body: - body = pangocffi.markup_escape_text(notif.body) - if notif.app_name: - app_name = pangocffi.markup_escape_text(notif.app_name) - return self.format.format(summary=summary, body=body, app_name=app_name) - - def _get_coordinates(self) -> Tuple[int, int]: - x, y = self._positions[len(self._shown) - 1] - if isinstance(self.screen, int): - screen = qtile.screens[self.screen] - elif self.screen == 'focus': - screen = qtile.current_screen - elif self.screen == 'mouse': - screen = qtile.find_screen(*qtile.mouse_position) - return x + screen.x, y + screen.y - - def _close(self, popup: Popup, nid: Optional[int] = None, reason=1) -> None: - """ - Close the specified Popup instance. - """ - if popup in self._shown: - if nid is not None and popup.id != nid: - return - self._shown.remove(popup) - if self._scroll_popup is popup: - self._scroll_popup = None - self._notif_id = None - popup.hide() - if self._queue and not self._paused: - self._send(self._queue.pop(0), popup) - else: - self._hidden.append(popup) - notifier._service.NotificationClosed(popup.notif.id, reason) - self._reposition() - - def _act(self, popup: Popup) -> None: - """ - Execute the actions specified by the notification visible on a clicked popup. - """ - # Currently this always invokes default action - #actions = {i: l for i, l in zip(notif.actions[:-1:2], notif.actions[1::2])} - if popup.notif.actions: - notifier._service.ActionInvoked(popup.notif.id, popup.notif.actions[0]) - - def _reposition(self) -> None: - for index, shown in enumerate(self._shown): - shown.x, shown.y = self._positions[index] - shown.place() - - def _load_icon(self, notif: Notification) -> Optional[Tuple[ImageSurface, int]]: - if not notif.app_icon: - return None - if notif.app_icon in self._icons: - return self._icons.get(notif.app_icon) - try: - img = images.Img.from_path(notif.app_icon) - if img.width > img.height: - img.resize(width=self.icon_size) - else: - img.resize(height=self.icon_size) - surface, _ = images._decode_to_image_surface( - img.bytes_img, img.width, img.height - ) - self._icons[notif.app_icon] = surface, surface.get_height() - except (FileNotFoundError, images.LoadingError, IsADirectoryError) as e: - logger.exception(e) - self._icons[notif.app_icon] = None - return self._icons[notif.app_icon] - - def close(self, _qtile=None) -> None: - """ - Close the oldest of all visible popup windows. - """ - if self._shown: - self._close(self._shown[0]) - - def close_all(self, _qtile=None) -> None: - """ - Close all popup windows. - """ - self._queue.clear() - while self._shown: - self._close(self._shown[0]) - - def prev(self, _qtile=None) -> None: - """ - Display the previous notification in the history. - """ - if notifier.notifications: - if self._scroll_popup is None: - if self._hidden: - self._scroll_popup = self._hidden.pop(0) - else: - self._scroll_popup = self._shown[0] - self._notif_id = len(notifier.notifications) - if self._notif_id > 0: - self._notif_id -= 1 - self._send( - notifier.notifications[self._notif_id], - self._scroll_popup, - 0 if self.sticky_history else None, - ) - - def next(self, _qtile=None) -> None: - """ - Display the next notification in the history. - """ - if self._scroll_popup: - if self._notif_id < len(notifier.notifications) - 1: - self._notif_id += 1 - if self._scroll_popup in self._shown: - self._shown.remove(self._scroll_popup) - self._send( - notifier.notifications[self._notif_id], - self._scroll_popup, - 0 if self.sticky_history else None, - ) - - def pause(self, _qtile=None) -> None: - """ - Pause display of notifications on screen. Notifications will be queued and - presented as usual when this is called again. - """ - if self._paused: - self._paused = False - self._renotify() - else: - self._paused = True - while self._shown: - self._close(self._shown[0])
config.py Modified
@@ -47,7 +47,6 @@ else: from x11 import keys_backend, term, wmname # noqa: F401 - # Colours theme = dict( foreground='#CFCCD6', @@ -98,6 +97,9 @@ my_keys.extend(keys_group) my_keys.extend(keys_scratchpad) +reload("fullscreen_state") +from fullscreen_state import toggle_fullscreen_state + def float_to_front(qtile: Qtile) -> None: """ Bring all floating windows of the group to front """ @@ -110,6 +112,7 @@ # Window management ([mod, 'control'], 'q', lazy.window.kill(), "Close window"), ([mod], 'f', lazy.window.toggle_fullscreen(), "Toggle fullscreen"), + ([mod], 'g', lazy.function(toggle_fullscreen_state), "Toggle fullscreen state"), ([mod, 'shift'], 'space', lazy.window.toggle_floating(), "Toggle floating"), ([mod], 'space', lazy.function(float_to_front), "Move floating windows to the front"), ([mod], 'j', lazy.function(traverse.down), "Traverse down"), @@ -478,6 +481,7 @@ left=bar.Gap(outer_gaps), right=bar.Gap(outer_gaps), wallpaper="~/pictures/Wallpapers/qtile-wallpaper.png", + wallpaper_mode="fill", ), Screen( bottom=bar.Bar( @@ -500,33 +504,26 @@ ] -# Notification server -if True: - reload("notification") - import notification - -# Remove defunct callbacks left when reloading the config - import libqtile.notify - libqtile.notify.notifier.callbacks.clear() - libqtile.notify.notifier.close_callbacks.clear() - - notifier = notification.Server( - background=colours[12], - foreground=background, - x=50, - y=50, - width=320, - height=100, - font_size=18, - font='TamzenForPowerline Bold', - ) - - my_keys.extend([ - ([mod], 'grave', notifier.lazy_prev, "Previous notification"), - ([mod, 'shift'], 'grave', notifier.lazy_next, "Next notification"), - (['control'], 'space', notifier.lazy_close, "Close notification"), - ]) +# Graphical notifications +reload("graphical_notifications") +from graphical_notifications import Notifier + +notifier = Notifier( + background=colours[12], + foreground=background, + x=50, + y=50, + width=320, + height=100, + font_size=18, + font='TamzenForPowerline Bold', +) +my_keys.extend([ + ([mod], 'grave', lazy.function(notifier.prev), "Previous notification"), + ([mod, 'shift'], 'grave', lazy.function(notifier.next), "Next notification"), + (['control'], 'space', lazy.function(notifier.close), "Close notification"), +]) # Config variables