From 904dfa2b6b969cb59312c968307e8f8ce738999e Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sat, 7 Jun 2025 23:02:52 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore(dock):=20add=20system=20tr?= =?UTF-8?q?ay=20and=20update=20tray=20visibility=20in=20DockComponents?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 1 + config/data.py | 4 +- config/dock.json | 78 ++++++++++++++++- modules/dock/components.py | 42 ++++----- modules/dock/main.py | 175 ++++++++++++++++++++----------------- modules/dock/tray.py | 170 +++++++++++++++++++++++++++++++++++ modules/dock/workspaces.py | 20 ++--- 7 files changed, 377 insertions(+), 113 deletions(-) create mode 100644 modules/dock/tray.py diff --git a/config/config.json b/config/config.json index 0d60a4f..1eb67cc 100644 --- a/config/config.json +++ b/config/config.json @@ -51,6 +51,7 @@ "dock_battery_visible": true, "dock_controls_visible": true, "dock_indicators_visible": true, + "dock_tray_visible": true, "metrics_visible": { "cpu": true, "ram": true, diff --git a/config/data.py b/config/data.py index 91af475..133f0e6 100644 --- a/config/data.py +++ b/config/data.py @@ -7,7 +7,7 @@ import gi gi.require_version("Gtk", "3.0") -APP_NAME = "modus" +APP_NAME = "modusv1" APP_NAME_CAP = "Modus" @@ -70,6 +70,7 @@ if os.path.exists(CONFIG_FILE): "date_time": config.get("dock_date_time_visible", True), "controls": config.get("dock_controls_visible", True), "indicators": config.get("dock_indicators_visible", True), + "tray": config.get("dock_tray_visible", True), } METRICS_DISKS = config.get("metrics_disks", ["/"]) @@ -96,6 +97,7 @@ else: "date_time": True, "controls": True, "indicators": True, + "tray": True, } METRICS_DISKS = ["/"] diff --git a/config/dock.json b/config/dock.json index 7564bbf..18348f3 100644 --- a/config/dock.json +++ b/config/dock.json @@ -1,3 +1,77 @@ { - "pinned_apps": [] -} \ No newline at end of file + 2 + "pinned_apps": [ + { + "name": "Advanced Network Configuration", + "display_name": "Advanced Network Configuration", + "executable": "nm-connection-editor", + "command_line": "nm-connection-editor", + "icon_name": "preferences-system-network" + }, + { + "name": "Avahi SSH Server Browser", + "display_name": "Avahi SSH Server Browser", + "executable": "/usr/bin/bssh", + "command_line": "/usr/bin/bssh", + "icon_name": "network-wired" + }, + { + "name": "Avahi VNC Server Browser", + "display_name": "Avahi VNC Server Browser", + "executable": "/usr/bin/bvnc", + "command_line": "/usr/bin/bvnc", + "icon_name": "network-wired" + }, + { + "name": "Avahi Zeroconf Browser", + "display_name": "Avahi Zeroconf Browser", + "executable": "/usr/bin/avahi-discover", + "command_line": "/usr/bin/avahi-discover", + "icon_name": "network-wired" + }, + { + "name": "Bluetooth Manager", + "display_name": "Bluetooth Manager", + "executable": "blueman-manager", + "command_line": "blueman-manager", + "icon_name": "blueman" + }, + { + "name": "Brave", + "display_name": "Brave", + "window_class": "brave-browser", + "executable": "brave", + "command_line": "brave %U", + "icon_name": "brave-desktop" + }, + { + "name": "btop++", + "display_name": "btop++", + "executable": "btop", + "command_line": "btop", + "icon_name": "btop" + }, + { + "name": "Bulk Rename", + "display_name": "Bulk Rename", + "executable": "thunar", + "command_line": "thunar --bulk-rename %F", + "icon_name": "org.xfce.thunar" + }, + { + "name": "CMake", + "display_name": "CMake", + "executable": "cmake-gui", + "command_line": "cmake-gui %f", + "icon_name": "CMakeSetup" + }, + { + "name": "Cursor", + "display_name": "Cursor", + "window_class": "cursor", + "executable": "/usr/bin/cursor", + "command_line": "/usr/bin/cursor --no-sandbox %U", + "icon_name": "cursor" + } + ] +} diff --git a/modules/dock/components.py b/modules/dock/components.py index 0a423ee..eae4f34 100644 --- a/modules/dock/components.py +++ b/modules/dock/components.py @@ -1,21 +1,24 @@ -import json import os - +import json import config.data as data from fabric.widgets.box import Box from fabric.widgets.button import Button from fabric.widgets.datetime import DateTime +from fabric.system_tray.widgets import SystemTray from modules.dock.battery import Battery -from modules.dock.controls import Controls -from modules.dock.indicators import Indicators from modules.dock.metrics import Metrics -from modules.dock.workspaces import create_workspace_widget +from modules.dock.controls import Controls +from modules.dock.workspaces import workspace +from modules.dock.indicators import Indicators class DockComponents(Box): def __init__(self, orientation_val="h", **kwargs): super().__init__( - name="dock-components", orientation=orientation_val, spacing=4, **kwargs + name="system-tray", + orientation=orientation_val, + spacing=4, + **kwargs ) # Initialize component visibility from data @@ -23,7 +26,7 @@ class DockComponents(Box): # Create components self.workspaces = Button( - child=create_workspace_widget(), + child=workspace, name="workspaces", ) self.metrics = Metrics() @@ -38,6 +41,9 @@ class DockComponents(Box): ) self.controls = Controls() self.indicators = Indicators() + self.systray = SystemTray( + icon_size=16, name="tray", orientation="h" if not data.VERTICAL else "v" + ) # Add components based on position if data.DOCK_POSITION == "Right": @@ -47,6 +53,7 @@ class DockComponents(Box): self.add(self.battery) self.add(self.metrics) self.add(self.workspaces) + self.add(self.systray) else: # Bottom or Left position self.add(self.workspaces) self.add(self.metrics) @@ -54,6 +61,7 @@ class DockComponents(Box): self.add(self.indicators) self.add(self.battery) self.add(self.date_time) + self.add(self.systray) # Apply initial visibility self.apply_component_props() @@ -66,6 +74,7 @@ class DockComponents(Box): "date_time": self.date_time, "controls": self.controls, "indicators": self.indicators, + "systray": self.systray, } for component_name, widget in components.items(): @@ -80,27 +89,20 @@ class DockComponents(Box): "date_time": self.date_time, "controls": self.controls, "indicators": self.indicators, + "systray": self.systray, } if component_name in components and component_name in self.component_visibility: - self.component_visibility[component_name] = not self.component_visibility[ - component_name - ] - components[component_name].set_visible( - self.component_visibility[component_name] - ) + self.component_visibility[component_name] = not self.component_visibility[component_name] + components[component_name].set_visible(self.component_visibility[component_name]) - config_file = os.path.expanduser( - f"~/.config/{data.APP_NAME}/config/config.json" - ) + config_file = os.path.expanduser(f"~/.config/{data.APP_NAME}/config/config.json") if os.path.exists(config_file): try: with open(config_file, "r") as f: config = json.load(f) - config[f"dock_{component_name}_visible"] = ( - self.component_visibility[component_name] - ) + config[f"dock_{component_name}_visible"] = self.component_visibility[component_name] with open(config_file, "w") as f: json.dump(config, f, indent=4) @@ -109,4 +111,4 @@ class DockComponents(Box): return self.component_visibility[component_name] - return None + return None \ No newline at end of file diff --git a/modules/dock/main.py b/modules/dock/main.py index 6560549..30a96d3 100644 --- a/modules/dock/main.py +++ b/modules/dock/main.py @@ -19,10 +19,10 @@ from fabric.widgets.image import Image from fabric.widgets.revealer import Revealer from gi.repository import Gdk, GLib, Gtk from modules.corners import MyCorner -from modules.dock.components import DockComponents from utils.icon_resolver import IconResolver from utils.occlusion import check_occlusion from utils.wayland import WaylandWindow as Window +from modules.dock.components import DockComponents def read_config(): @@ -91,7 +91,7 @@ class Dock(Window): layer="top", anchor=anchor_to_set, margin="0px 0px 0px 0px", - exclusivity="auto" if not data.DOCK_AUTO_HIDE else "none", + exclusivity="none", **kwargs, ) Dock._instances.append(self) @@ -146,20 +146,25 @@ class Dock(Window): self.dock_eventbox.connect("enter-notify-event", self._on_dock_enter) self.dock_eventbox.connect("leave-notify-event", self._on_dock_leave) - # Create components section + # Define separator orientation separator_orientation = ( Gtk.Orientation.VERTICAL if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL else Gtk.Orientation.HORIZONTAL ) - # Initialize components with proper orientation - self.components = DockComponents(orientation_val=dock_wrapper_orientation_val) + # Create components using DockComponents + self.components = DockComponents(orientation_val="h" if not data.VERTICAL else "v") - # Initialize components with proper orientation - self.components = DockComponents(orientation_val=dock_wrapper_orientation_val) + # Add components based on position + if self.actual_dock_is_horizontal: # Bottom dock + self.view.add(self.components) + elif data.DOCK_POSITION == "Left": + self.view.add(self.components) + else: # Right position + self.view.add(self.components) - # Add components to dock + # Add system tray to dock if data.DOCK_POSITION == "Right": self.view.add( Box( @@ -171,9 +176,7 @@ class Dock(Window): name="dock-separator", ) ) - self.view.add(self.components) else: - self.view.pack_start(self.components, False, False, 0) self.view.add( Box( orientation=separator_orientation, @@ -563,7 +566,6 @@ class Dock(Window): not self.is_mouse_over_dock_area and not self._drag_in_progress and not self._prevent_occlusion - and data.DOCK_AUTO_HIDE ): if self.always_occluded: self.dock_revealer.set_reveal_child(False) @@ -582,7 +584,6 @@ class Dock(Window): self.is_mouse_over_dock_area or self._drag_in_progress or self._prevent_occlusion - or not data.DOCK_AUTO_HIDE ): return @@ -736,33 +737,7 @@ class Dock(Window): ) # Add components based on position - if data.DOCK_POSITION == "Right": - children.extend(pinned_buttons) - if pinned_buttons and open_buttons: - children.append( - Box( - orientation=separator_orientation, - v_expand=False, - h_expand=False, - h_align="center", - v_align="center", - name="dock-separator", - ) - ) - children.extend(open_buttons) - if pinned_buttons or open_buttons: - children.append( - Box( - orientation=separator_orientation, - v_expand=False, - h_expand=False, - h_align="center", - v_align="center", - name="dock-separator", - ) - ) - children.append(self.components) - else: # Bottom or Left position + if self.actual_dock_is_horizontal: # Bottom dock children.append(self.components) if pinned_buttons or open_buttons: children.append( @@ -788,6 +763,58 @@ class Dock(Window): ) ) children.extend(open_buttons) + elif data.DOCK_POSITION == "Left": + children.append(self.components) + if pinned_buttons or open_buttons: + children.append( + Box( + orientation=separator_orientation, + v_expand=False, + h_expand=False, + h_align="center", + v_align="center", + name="dock-separator", + ) + ) + children.extend(pinned_buttons) + if pinned_buttons and open_buttons: + children.append( + Box( + orientation=separator_orientation, + v_expand=False, + h_expand=False, + h_align="center", + v_align="center", + name="dock-separator", + ) + ) + children.extend(open_buttons) + else: # Right position + children.extend(pinned_buttons) + if pinned_buttons and open_buttons: + children.append( + Box( + orientation=separator_orientation, + v_expand=False, + h_expand=False, + h_align="center", + v_align="center", + name="dock-separator", + ) + ) + children.extend(open_buttons) + if pinned_buttons or open_buttons: + children.append( + Box( + orientation=separator_orientation, + v_expand=False, + h_expand=False, + h_align="center", + v_align="center", + name="dock-separator", + ) + ) + children.append(self.components) self.view.children = children idle_add(self._update_size) @@ -821,7 +848,6 @@ class Dock(Window): self.is_mouse_over_dock_area or self._drag_in_progress or self._prevent_occlusion - or not data.DOCK_AUTO_HIDE ): if not self.dock_revealer.get_reveal_child(): self.dock_revealer.set_reveal_child(True) @@ -908,51 +934,40 @@ class Dock(Window): return def process_drag_end(): - # Check if dropped outside the dock by checking the pointer position - display = self.get_display() - seat = display.get_default_seat() - pointer = seat.get_pointer() - window = self.get_window() + if self.get_mapped(): + display = Gdk.Display.get_default() + _, x, y, _ = display.get_pointer() + window = self.get_window() + + # Only proceed if pointer is outside dock window + if window: + win_x, win_y, width, height = window.get_geometry() + if not (win_x <= x <= win_x + width and win_y <= y <= win_y + height): + app_id_dragged = widget.app_identifier + instances_dragged = widget.instances - if window: - screen, x, y = pointer.get_position() # Fix: Unpack all three values - win_x, win_y = window.get_position() - win_w = window.get_width() - win_h = window.get_height() - - # If pointer is outside dock window bounds - if x < win_x or x > win_x + win_w or y < win_y or y > win_y + win_h: - app_id_dragged = widget.app_identifier - instances_dragged = widget.instances - - app_index_dragged = -1 - for i, pinned_app_item in enumerate(self.pinned): - if isinstance(app_id_dragged, dict) and isinstance( - pinned_app_item, dict - ): - if app_id_dragged.get("name") == pinned_app_item.get( - "name" - ): + # Remove pinned app + app_index_dragged = -1 + for i, pinned_app_item in enumerate(self.pinned): + if isinstance(app_id_dragged, dict) and isinstance(pinned_app_item, dict): + if app_id_dragged.get("name") == pinned_app_item.get("name"): + app_index_dragged = i + break + elif app_id_dragged == pinned_app_item: app_index_dragged = i break - elif app_id_dragged == pinned_app_item: - app_index_dragged = i - break - - if app_index_dragged >= 0: - self.pinned.pop(app_index_dragged) - self.config["pinned_apps"] = self.pinned - self.update_pinned_apps_file() - self.update_dock() - elif instances_dragged: - address = instances_dragged[0].get("address") - if address: - exec_shell_command( - f"hyprctl dispatch focuswindow address:{address}" - ) + + if app_index_dragged >= 0: + self.pinned.pop(app_index_dragged) + self.config["pinned_apps"] = self.pinned + self.update_pinned_apps_file() + self.update_dock() + elif instances_dragged: + address = instances_dragged[0].get("address") + if address: + exec_shell_command(f"hyprctl dispatch focuswindow address:{address}") self._drag_in_progress = False - self.check_occlusion_state() GLib.idle_add(process_drag_end) diff --git a/modules/dock/tray.py b/modules/dock/tray.py new file mode 100644 index 0000000..aac9dcd --- /dev/null +++ b/modules/dock/tray.py @@ -0,0 +1,170 @@ +import config.data as data +from gi.repository import Gdk, GdkPixbuf, GLib, Gray, Gtk +from fabric.widgets.box import Box +import logging +import gi + +gi.require_version("Gray", "0.1") + + +logger = logging.getLogger(__name__) + + +class SystemTray(Box): + def __init__( + self, pixel_size: int = 20, refresh_interval: int = 1, **kwargs + ) -> None: + orientation = ( + Gtk.Orientation.HORIZONTAL + if not data.VERTICAL + else Gtk.Orientation.VERTICAL + ) + super().__init__(name="tray", orientation=orientation, spacing=8, **kwargs) + self.enabled = True + super().set_visible(False) + self.pixel_size = pixel_size + self.refresh_interval = refresh_interval + + self.buttons_by_id = {} + self.items_by_id = {} + + self.watcher = Gray.Watcher() + self.watcher.connect("item-added", self.on_watcher_item_added) + + GLib.timeout_add_seconds(self.refresh_interval, self._refresh_all_items) + + def set_visible(self, visible: bool): + self.enabled = visible + self._update_visibility() + + def _update_visibility(self): + has = len(self.get_children()) > 0 + super().set_visible(self.enabled and has) + + def _get_item_pixbuf(self, item: Gray.Item) -> GdkPixbuf.Pixbuf: + try: + pm = Gray.get_pixmap_for_pixmaps(item.get_icon_pixmaps(), self.pixel_size) + if pm: + return pm.as_pixbuf(self.pixel_size, GdkPixbuf.InterpType.HYPER) + + name = item.get_icon_name() + theme = Gtk.IconTheme.new() + path = item.get_icon_theme_path() + if path: + theme.prepend_search_path(path) + return theme.load_icon( + name, self.pixel_size, Gtk.IconLookupFlags.FORCE_SIZE + ) + except GLib.Error as e: + logger.debug(f"Icon load error {e}") + return Gtk.IconTheme.get_default().load_icon( + "image-missing", self.pixel_size, Gtk.IconLookupFlags.FORCE_SIZE + ) + + def _refresh_item_ui(self, identifier: str, item: Gray.Item, button: Gtk.Button): + pixbuf = self._get_item_pixbuf(item) + img = button.get_image() + if isinstance(img, Gtk.Image): + img.set_from_pixbuf(pixbuf) + else: + new = Gtk.Image.new_from_pixbuf(pixbuf) + button.set_image(new) + new.show() + tip = None + if hasattr(item, "get_tooltip_text"): + tip = item.get_tooltip_text() + elif hasattr(item, "get_title"): + tip = item.get_title() + if tip: + button.set_tooltip_text(tip) + else: + button.set_has_tooltip(False) + + def _refresh_all_items(self) -> bool: + for ident, item in self.items_by_id.items(): + btn = self.buttons_by_id.get(ident) + if btn: + self._refresh_item_ui(ident, item, btn) + return True + + def on_watcher_item_added(self, _, identifier: str): + item = self.watcher.get_item_for_identifier(identifier) + if not item: + return + + if identifier in self.buttons_by_id: + self.buttons_by_id[identifier].destroy() + del self.buttons_by_id[identifier] + del self.items_by_id[identifier] + + btn = self.do_bake_item_button(item) + self.buttons_by_id[identifier] = btn + self.items_by_id[identifier] = item + + item.connect( + "notify::icon-pixmaps", + lambda itm, pspec: self._refresh_item_ui(identifier, itm, btn), + ) + item.connect( + "notify::icon-name", + lambda itm, pspec: self._refresh_item_ui(identifier, itm, btn), + ) + + try: + item.connect( + "updated", lambda itm: self._refresh_item_ui(identifier, itm, btn) + ) + except TypeError: + pass + + item.connect( + "removed", lambda itm: self.on_item_instance_removed(identifier, itm) + ) + + self.add(btn) + btn.show_all() + self._update_visibility() + + def do_bake_item_button(self, item: Gray.Item) -> Gtk.Button: + btn = Gtk.Button() + btn.connect("button-press-event", lambda b, e: self.on_button_click(b, item, e)) + img = Gtk.Image.new_from_pixbuf(self._get_item_pixbuf(item)) + btn.set_image(img) + tip = ( + item.get_tooltip_text() + if hasattr(item, "get_tooltip_text") + else getattr(item, "get_title", lambda: None)() + ) + if tip: + btn.set_tooltip_text(tip) + return btn + + def on_item_instance_removed(self, identifier: str, removed_item: Gray.Item): + if self.items_by_id.get(identifier) is removed_item: + btn = self.buttons_by_id.pop(identifier, None) + self.items_by_id.pop(identifier, None) + if btn: + btn.destroy() + self._update_visibility() + + def on_button_click( + self, button: Gtk.Button, item: Gray.Item, event: Gdk.EventButton + ): + if event.button == Gdk.BUTTON_PRIMARY: + try: + item.activate(int(event.x_root), int(event.y_root)) + except Exception as e: + logger.error(f"Activate error: {e}") + elif event.button == Gdk.BUTTON_SECONDARY: + menu = getattr(item, "get_menu", lambda: None)() + if isinstance(menu, Gtk.Menu): + menu.popup_at_widget( + button, Gdk.Gravity.SOUTH_WEST, Gdk.Gravity.NORTH_WEST, event + ) + else: + cm = getattr(item, "context_menu", None) + if cm: + try: + cm(int(event.x_root), int(event.y_root)) + except Exception as e: + logger.error(f"ContextMenu error: {e}") diff --git a/modules/dock/workspaces.py b/modules/dock/workspaces.py index 2d8e7ba..a0d8df2 100644 --- a/modules/dock/workspaces.py +++ b/modules/dock/workspaces.py @@ -5,15 +5,15 @@ from fabric.widgets.label import Label connection = Hyprland() -def create_workspace_widget(): - workspaceData = connection.send_command("j/activeworkspace").reply - activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"] - workspace_label = Label(label=f"{activeWorkspace}") +workspaceData = connection.send_command("j/activeworkspace").reply +activeWorkspace = json.loads(workspaceData.decode("utf-8"))["name"] +workspace = Label(label=f"{activeWorkspace}") - def on_workspace(obj, signal): - nonlocal workspace_label - activeWorkspace = json.loads(signal.data[0]) - workspace_label.set_label(f"{activeWorkspace}") - connection.connect("event::workspace", on_workspace) - return workspace_label +def on_workspace(obj, signal): + global activeWorkspace + activeWorkspace = json.loads(signal.data[0]) + workspace.set_label(f"{activeWorkspace}") + + +connection.connect("event::workspace", on_workspace)