From d1c2f65297c69d528c966bec7f3aab1349f9a2b6 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 11 Jun 2025 15:38:29 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20chore(dock):=20update=20config?= =?UTF-8?q?=20and=20dock.json,=20add=20pinned=20apps=20to=20dock=20instanc?= =?UTF-8?q?e,=20connect=20drag=20signals?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.json | 4 +- config/dock.json | 17 +- dock.py | 1040 ---------------------------------- modules/dock/applications.py | 248 ++++---- modules/dock/components.py | 35 +- modules/dock/main.py | 36 +- 6 files changed, 225 insertions(+), 1155 deletions(-) delete mode 100644 dock.py diff --git a/config/config.json b/config/config.json index 603bca0..ea8cca1 100644 --- a/config/config.json +++ b/config/config.json @@ -36,8 +36,8 @@ "prefix_restart_inspector": "SUPER CTRL ALT", "suffix_restart_inspector": "B", "wallpapers_dir": "/home/sankalp/Pictures/wallpapers", - "dock_position": "Bottom", - "vertical": false, + "dock_position": "Left", + "vertical": true, "dock_enabled": true, "dock_auto_hide": true, "dock_always_occluded": false, diff --git a/config/dock.json b/config/dock.json index 7564bbf..80767a6 100644 --- a/config/dock.json +++ b/config/dock.json @@ -1,3 +1,18 @@ { - "pinned_apps": [] + "pinned_apps": [ + { + "name": "kitty", + "display_name": "kitty", + "window_class": null, + "executable": "kitty", + "command_line": "kitty" + }, + { + "name": "Visual Studio Code", + "display_name": "Visual Studio Code", + "window_class": "Code", + "executable": "/usr/bin/code", + "command_line": "/usr/bin/code %F" + } + ] } \ No newline at end of file diff --git a/dock.py b/dock.py deleted file mode 100644 index 88a3f77..0000000 --- a/dock.py +++ /dev/null @@ -1,1040 +0,0 @@ -import json -import logging - -import cairo -from fabric.hyprland.widgets import get_hyprland_connection -from fabric.utils import ( - exec_shell_command, - exec_shell_command_async, - get_relative_path, - idle_add, - remove_handler, -) -from fabric.utils.helpers import get_desktop_applications -from fabric.widgets.box import Box -from fabric.widgets.button import Button -from fabric.widgets.eventbox import EventBox -from fabric.widgets.image import Image -from fabric.widgets.revealer import Revealer -from gi.repository import Gdk, GLib, Gtk - -import config.data as data -from modules.corners import MyCorner -from utils.icon_resolver import IconResolver -from utils.occlusion import check_occlusion -from widgets.wayland import WaylandWindow as Window - - -def read_config(): - """Read and return the full configuration from the JSON file, handling missing file.""" - config_path = get_relative_path("../config/dock.json") - try: - with open(config_path, "r") as file: - config_data = json.load(file) - - if ( - "pinned_apps" in config_data - and config_data["pinned_apps"] - and isinstance(config_data["pinned_apps"][0], str) - ): - all_apps = get_desktop_applications() - app_map = {app.name: app for app in all_apps if app.name} - - old_pinned = config_data["pinned_apps"] - config_data["pinned_apps"] = [] - - for app_id in old_pinned: - app = app_map.get(app_id) - if app: - app_data_obj = { - "name": app.name, - "display_name": app.display_name, - "window_class": app.window_class, - "executable": app.executable, - "command_line": app.command_line, - } - config_data["pinned_apps"].append(app_data_obj) - else: - config_data["pinned_apps"].append({"name": app_id}) - - except (FileNotFoundError, json.JSONDecodeError): - config_data = {"pinned_apps": []} - return config_data - - -def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface: - alloc = widget.get_allocation() - surface = cairo.ImageSurface( - cairo.Format.ARGB32, - alloc.width, - alloc.height, - ) - cr = cairo.Context(surface) - cr.set_source_rgba(255, 255, 255, 0) - cr.rectangle(0, 0, alloc.width, alloc.height) - cr.fill() - widget.draw(cr) - return surface - - -class Dock(Window): - _instances = [] - - def __init__(self, integrated_mode: bool = False, **kwargs): - self.integrated_mode = integrated_mode - self.icon_size = 20 if self.integrated_mode else data.DOCK_ICON_SIZE - self.effective_occlusion_size = 36 + self.icon_size - - anchor_to_set: str - revealer_transition_type: str - - self.actual_dock_is_horizontal: bool - main_box_orientation_val: Gtk.Orientation - main_box_h_align_val: str - dock_wrapper_orientation_val: Gtk.Orientation - - if not self.integrated_mode: - self.actual_dock_is_horizontal = not data.VERTICAL - - if self.actual_dock_is_horizontal: - anchor_to_set = "bottom" - revealer_transition_type = "slide-up" - main_box_orientation_val = Gtk.Orientation.VERTICAL - main_box_h_align_val = "center" - dock_wrapper_orientation_val = Gtk.Orientation.HORIZONTAL - else: - if data.BAR_POSITION == "Left": - anchor_to_set = "right" - revealer_transition_type = "slide-left" - elif data.BAR_POSITION == "Right": - anchor_to_set = "left" - revealer_transition_type = "slide-right" - else: - anchor_to_set = "right" - revealer_transition_type = "slide-left" - - main_box_orientation_val = Gtk.Orientation.HORIZONTAL - main_box_h_align_val = "end" if anchor_to_set == "right" else "start" - dock_wrapper_orientation_val = Gtk.Orientation.VERTICAL - - super().__init__( - name="dock-window", - layer="top", - anchor=anchor_to_set, - margin="0px 0px 0px 0px", - exclusivity="none", - **kwargs, - ) - Dock._instances.append(self) - else: - self.actual_dock_is_horizontal = True - dock_wrapper_orientation_val = Gtk.Orientation.HORIZONTAL - - anchor_to_set = "bottom" - revealer_transition_type = "slide-up" - main_box_orientation_val = Gtk.Orientation.VERTICAL - main_box_h_align_val = "center" - - self.config = read_config() - self.conn = get_hyprland_connection() - self.icon_resolver = IconResolver() - self.pinned = self.config.get("pinned_apps", []) - self.config_path = get_relative_path("../config/dock.json") - self.app_map = {} - self._all_apps = get_desktop_applications() - self.app_identifiers = self._build_app_identifiers_map() - - self.hide_id = None - self._arranger_handler = None - self._drag_in_progress = False - self.always_occluded = ( - data.DOCK_ALWAYS_OCCLUDED if not self.integrated_mode else False - ) - self.is_mouse_over_dock_area = False - self._prevent_occlusion = False - - self.view = Box(name="viewport", spacing=4) - self.wrapper = Box( - name="dock", - children=[self.view], - style_classes=["left"] if data.BAR_POSITION == "Right" else [], - ) - - self.wrapper.set_orientation(dock_wrapper_orientation_val) - self.view.set_orientation(dock_wrapper_orientation_val) - - if self.integrated_mode: - self.wrapper.add_style_class("integrated") - else: - if dock_wrapper_orientation_val == Gtk.Orientation.VERTICAL: - self.wrapper.add_style_class("vertical") - else: - self.wrapper.remove_style_class("vertical") - - match data.DOCK_THEME: - case "Pills": - self.wrapper.add_style_class("pills") - case "Dense": - self.wrapper.add_style_class("dense") - case "Edge": - self.wrapper.add_style_class("edge") - case _: - self.wrapper.add_style_class("pills") - - if not self.integrated_mode: - self.dock_eventbox = EventBox() - self.dock_eventbox.add(self.wrapper) - self.dock_eventbox.connect("enter-notify-event", self._on_dock_enter) - self.dock_eventbox.connect("leave-notify-event", self._on_dock_leave) - - self.corner_left = Box() - self.corner_right = Box() - self.corner_top = Box() - self.corner_bottom = Box() - - if self.actual_dock_is_horizontal: - self.corner_left = Box( - name="dock-corner-left", - orientation=Gtk.Orientation.VERTICAL, - h_align="start", - children=[ - Box(v_expand=True, v_align="fill"), - MyCorner("bottom-right"), - ], - ) - self.corner_right = Box( - name="dock-corner-right", - orientation=Gtk.Orientation.VERTICAL, - h_align="end", - children=[ - Box(v_expand=True, v_align="fill"), - MyCorner("bottom-left"), - ], - ) - self.dock_full = Box( - name="dock-full", - orientation=Gtk.Orientation.HORIZONTAL, - h_expand=True, - h_align="fill", - children=[self.corner_left, self.dock_eventbox, self.corner_right], - ) - else: - if anchor_to_set == "right": - self.corner_top = Box( - name="dock-corner-top", - orientation=Gtk.Orientation.HORIZONTAL, - v_align="start", - children=[ - Box(h_expand=True, h_align="fill"), - MyCorner("bottom-right"), - ], - ) - self.corner_bottom = Box( - name="dock-corner-bottom", - orientation=Gtk.Orientation.HORIZONTAL, - v_align="end", - children=[ - Box(h_expand=True, h_align="fill"), - MyCorner("top-right"), - ], - ) - else: - self.corner_top = Box( - name="dock-corner-top", - orientation=Gtk.Orientation.HORIZONTAL, - v_align="start", - children=[ - MyCorner("bottom-left"), - Box(h_expand=True, h_align="fill"), - ], - ) - self.corner_bottom = Box( - name="dock-corner-bottom", - orientation=Gtk.Orientation.HORIZONTAL, - v_align="end", - children=[ - MyCorner("top-left"), - Box(h_expand=True, h_align="fill"), - ], - ) - - self.dock_full = Box( - name="dock-full", - orientation=Gtk.Orientation.VERTICAL, - v_expand=True, - v_align="fill", - children=[self.corner_top, self.dock_eventbox, self.corner_bottom], - ) - - self.dock_revealer = Revealer( - name="dock-revealer", - transition_type=revealer_transition_type, - transition_duration=250, - child_revealed=False, - child=self.dock_full, - ) - - self.hover_activator = EventBox() - - self.hover_activator.set_size_request( - -1 if self.actual_dock_is_horizontal else 1, - 1 if self.actual_dock_is_horizontal else -1, - ) - self.hover_activator.connect("enter-notify-event", self._on_hover_enter) - self.hover_activator.connect("leave-notify-event", self._on_hover_leave) - - self.main_box = Box( - orientation=main_box_orientation_val, - children=[self.hover_activator, self.dock_revealer] - if data.BAR_POSITION != "Right" - else [self.dock_revealer, self.hover_activator], - h_align=main_box_h_align_val, - ) - self.add(self.main_box) - - if data.DOCK_THEME in ["Edge", "Dense"]: - for corner in [ - self.corner_left, - self.corner_right, - self.corner_top, - self.corner_bottom, - ]: - corner.set_visible(False) - - if not data.DOCK_ENABLED or data.BAR_POSITION in ["Top", "Bottom"]: - self.set_visible(False) - - if self.always_occluded: - self.dock_full.add_style_class("occluded") - - self.view.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, - [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)], - Gdk.DragAction.MOVE, - ) - self.view.drag_dest_set( - Gtk.DestDefaults.ALL, - [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)], - Gdk.DragAction.MOVE, - ) - self.view.connect("drag-data-get", self.on_drag_data_get) - self.view.connect("drag-data-received", self.on_drag_data_received) - self.view.connect("drag-begin", self.on_drag_begin) - self.view.connect("drag-end", self.on_drag_end) - - if self.conn.ready: - self.update_dock() - if not self.integrated_mode: - GLib.timeout_add(250, self.check_occlusion_state) - else: - self.conn.connect("event::ready", self.update_dock) - if not self.integrated_mode: - self.conn.connect( - "event::ready", - lambda *args: GLib.timeout_add(250, self.check_occlusion_state), - ) - - for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"): - self.conn.connect(f"event::{ev}", self.update_dock) - - if not self.integrated_mode: - self.conn.connect("event::workspace", self.check_hide) - - GLib.timeout_add_seconds(1, self.check_config_change) - - def _build_app_identifiers_map(self): - identifiers = {} - for app in self._all_apps: - if app.name: - identifiers[app.name.lower()] = app - if app.display_name: - identifiers[app.display_name.lower()] = app - if app.window_class: - identifiers[app.window_class.lower()] = app - if app.executable: - identifiers[app.executable.split("/")[-1].lower()] = app - if app.command_line: - identifiers[app.command_line.split()[0].split("/")[-1].lower()] = app - return identifiers - - def _normalize_window_class(self, class_name): - if not class_name: - return "" - normalized = class_name.lower() - suffixes = [".bin", ".exe", ".so", "-bin", "-gtk"] - for suffix in suffixes: - if normalized.endswith(suffix): - normalized = normalized[: -len(suffix)] - return normalized - - def _classes_match(self, class1, class2): - if not class1 or not class2: - return False - norm1 = self._normalize_window_class(class1) - norm2 = self._normalize_window_class(class2) - return norm1 == norm2 - - def on_drag_begin(self, widget, drag_context): - self._drag_in_progress = True - Gtk.drag_set_icon_surface(drag_context, createSurfaceFromWidget(widget)) - - def _on_hover_enter(self, *args): - if self.integrated_mode: - return - self.is_mouse_over_dock_area = True - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = None - self.dock_revealer.set_reveal_child(True) - if not self.always_occluded: - self.dock_full.remove_style_class("occluded") - - def _on_hover_leave(self, *args): - if self.integrated_mode: - return - self.is_mouse_over_dock_area = False - self.delay_hide() - - def _on_dock_enter(self, widget, event): - if self.integrated_mode: - return True - self.is_mouse_over_dock_area = True - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = None - self.dock_revealer.set_reveal_child(True) - if not self.always_occluded: - self.dock_full.remove_style_class("occluded") - return True - - def _on_dock_leave(self, widget, event): - if self.integrated_mode: - return True - if event.detail == Gdk.NotifyType.INFERIOR: - return False - - self.is_mouse_over_dock_area = False - self.delay_hide() - - if self.always_occluded: - self.dock_full.add_style_class("occluded") - return True - - def find_app(self, app_identifier): - if not app_identifier: - return None - if isinstance(app_identifier, dict): - for key in [ - "window_class", - "executable", - "command_line", - "name", - "display_name", - ]: - if key in app_identifier and app_identifier[key]: - app = self.find_app_by_key(app_identifier[key]) - if app: - return app - return None - return self.find_app_by_key(app_identifier) - - def find_app_by_key(self, key_value): - if not key_value: - return None - normalized_id = str(key_value).lower() - if normalized_id in self.app_identifiers: - return self.app_identifiers[normalized_id] - for app in self._all_apps: - if app.name and normalized_id in app.name.lower(): - return app - if app.display_name and normalized_id in app.display_name.lower(): - return app - if app.window_class and normalized_id in app.window_class.lower(): - return app - if app.executable and normalized_id in app.executable.lower(): - return app - if app.command_line and normalized_id in app.command_line.lower(): - return app - return None - - def update_app_map(self): - self._all_apps = get_desktop_applications() - self.app_map = {app.name: app for app in self._all_apps if app.name} - self.app_identifiers = self._build_app_identifiers_map() - - def create_button(self, app_identifier, instances): - desktop_app = self.find_app(app_identifier) - icon_img = None - display_name = None - - if desktop_app: - icon_img = desktop_app.get_icon_pixbuf(size=self.icon_size) - display_name = desktop_app.display_name or desktop_app.name - - id_value = ( - app_identifier["name"] - if isinstance(app_identifier, dict) - else app_identifier - ) - - if not icon_img: - icon_img = self.icon_resolver.get_icon_pixbuf(id_value, self.icon_size) - - if not icon_img: - icon_img = self.icon_resolver.get_icon_pixbuf( - "application-x-executable-symbolic", self.icon_size - ) - if not icon_img: - icon_img = self.icon_resolver.get_icon_pixbuf( - "image-missing", self.icon_size - ) - - items = [Image(pixbuf=icon_img)] - tooltip = display_name or (id_value if isinstance(id_value, str) else "Unknown") - if not display_name and instances and instances[0].get("title"): - tooltip = instances[0]["title"] - - button = Button( - child=Box( - name="dock-icon", orientation="v", h_align="center", children=items - ), - on_clicked=lambda *a: self.handle_app( - app_identifier, instances, desktop_app - ), - tooltip_text=tooltip, - name="dock-app-button", - ) - button.app_identifier = app_identifier - button.desktop_app = desktop_app - button.instances = instances - if instances: - button.add_style_class("instance") - - button.drag_source_set( - Gdk.ModifierType.BUTTON1_MASK, - [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)], - Gdk.DragAction.MOVE, - ) - button.connect("drag-begin", self.on_drag_begin) - button.connect("drag-end", self.on_drag_end) - button.drag_dest_set( - Gtk.DestDefaults.ALL, - [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)], - Gdk.DragAction.MOVE, - ) - button.connect("drag-data-get", self.on_drag_data_get) - button.connect("drag-data-received", self.on_drag_data_received) - button.connect("enter-notify-event", self._on_child_enter) - return button - - def handle_app(self, app_identifier, instances, desktop_app=None): - if not instances: - if not desktop_app: - desktop_app = self.find_app(app_identifier) - if desktop_app: - launch_success = desktop_app.launch() - if not launch_success: - if desktop_app.command_line: - exec_shell_command_async(f"nohup {desktop_app.command_line} &") - elif desktop_app.executable: - exec_shell_command_async(f"nohup {desktop_app.executable} &") - else: - cmd_to_run = None - if isinstance(app_identifier, dict): - if ( - "command_line" in app_identifier - and app_identifier["command_line"] - ): - cmd_to_run = app_identifier["command_line"] - elif ( - "executable" in app_identifier and app_identifier["executable"] - ): - cmd_to_run = app_identifier["executable"] - elif "name" in app_identifier and app_identifier["name"]: - cmd_to_run = app_identifier["name"] - elif isinstance(app_identifier, str): - cmd_to_run = app_identifier - if cmd_to_run: - exec_shell_command_async(f"nohup {cmd_to_run} &") - else: - focused = self.get_focused() - idx = next( - (i for i, inst in enumerate(instances) if inst["address"] == focused), - -1, - ) - next_inst = instances[(idx + 1) % len(instances)] - exec_shell_command( - f"hyprctl dispatch focuswindow address:{next_inst['address']}" - ) - - def _on_child_enter(self, widget, event): - if self.integrated_mode: - return False - self.is_mouse_over_dock_area = True - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = None - return False - - def delay_hide(self): - if self.integrated_mode: - return - if self.hide_id: - GLib.source_remove(self.hide_id) - self.hide_id = GLib.timeout_add(250, self.hide_dock_if_not_hovered) - - def hide_dock_if_not_hovered(self): - if self.integrated_mode: - return False - self.hide_id = None - if ( - not self.is_mouse_over_dock_area - and not self._drag_in_progress - and not self._prevent_occlusion - ): - if self.always_occluded: - self.dock_revealer.set_reveal_child(False) - else: - occlusion_region = ( - ("bottom", self.effective_occlusion_size) - if self.actual_dock_is_horizontal - else ("right", self.effective_occlusion_size) - ) - if check_occlusion(occlusion_region) or not self.view.get_children(): - self.dock_revealer.set_reveal_child(False) - return False - - def check_hide(self, *args): - if self.integrated_mode: - return - if ( - self.is_mouse_over_dock_area - or self._drag_in_progress - or self._prevent_occlusion - ): - return - - clients = self.get_clients() - current_ws = self.get_workspace() - ws_clients = [w for w in clients if w["workspace"]["id"] == current_ws] - - if not self.always_occluded: - if not ws_clients: - if not self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(True) - self.dock_full.remove_style_class("occluded") - elif any( - not w.get("floating") and not w.get("fullscreen") for w in ws_clients - ): - self.check_occlusion_state() - else: - if not self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(True) - self.dock_full.remove_style_class("occluded") - else: - if self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(False) - self.dock_full.add_style_class("occluded") - - def update_dock(self, *args): - self.update_app_map() - arranger_handler = getattr(self, "_arranger_handler", None) - if arranger_handler: - remove_handler(arranger_handler) - clients = self.get_clients() - - running_windows = {} - for c in clients: - window_id = None - if class_name := c.get("initialClass", "").lower(): - window_id = class_name - elif class_name := c.get("class", "").lower(): - window_id = class_name - elif title := c.get("title", "").lower(): - possible_name = title.split(" - ")[0].strip() - if possible_name and len(possible_name) > 1: - window_id = possible_name - else: - window_id = title - if not window_id: - window_id = "unknown-app" - running_windows.setdefault(window_id, []).append(c) - normalized_id = self._normalize_window_class(window_id) - if normalized_id != window_id: - running_windows.setdefault(normalized_id, []).extend( - running_windows[window_id] - ) - - pinned_buttons = [] - used_window_classes = set() - - for app_data_item in self.pinned: - app = self.find_app(app_data_item) - instances = [] - matched_class = None - possible_identifiers = [] - - if isinstance(app_data_item, dict): - for key in [ - "window_class", - "executable", - "command_line", - "name", - "display_name", - ]: - if key in app_data_item and app_data_item[key]: - possible_identifiers.append(app_data_item[key].lower()) - elif isinstance(app_data_item, str): - possible_identifiers.append(app_data_item.lower()) - - if app: - if app.window_class: - possible_identifiers.append(app.window_class.lower()) - if app.executable: - possible_identifiers.append(app.executable.split("/")[-1].lower()) - if app.command_line: - cmd_parts = app.command_line.split() - if cmd_parts: - possible_identifiers.append(cmd_parts[0].split("/")[-1].lower()) - if app.name: - possible_identifiers.append(app.name.lower()) - if app.display_name: - possible_identifiers.append(app.display_name.lower()) - - possible_identifiers = list(set(possible_identifiers)) - - for identifier in possible_identifiers: - if identifier in running_windows: - instances = running_windows[identifier] - matched_class = identifier - break - normalized = self._normalize_window_class(identifier) - if normalized in running_windows: - instances = running_windows[normalized] - matched_class = normalized - break - for window_class_key in running_windows: - if len(identifier) >= 3 and identifier in window_class_key: - instances = running_windows[window_class_key] - matched_class = window_class_key - break - if matched_class: - break - - if matched_class: - used_window_classes.add(matched_class) - used_window_classes.add(self._normalize_window_class(matched_class)) - - pinned_buttons.append(self.create_button(app_data_item, instances)) - - open_buttons = [] - for class_name, instances in running_windows.items(): - if class_name not in used_window_classes: - app = None - app = self.app_identifiers.get(class_name) - if not app: - norm_class = self._normalize_window_class(class_name) - app = self.app_identifiers.get(norm_class) - if not app: - app = self.find_app_by_key(class_name) - if not app and instances and instances[0].get("title"): - title = instances[0].get("title", "") - potential_name = title.split(" - ")[0].strip() - if len(potential_name) > 2: - app = self.find_app_by_key(potential_name) - - if app: - app_data_obj = { - "name": app.name, - "display_name": app.display_name, - "window_class": app.window_class, - "executable": app.executable, - "command_line": app.command_line, - } - identifier = app_data_obj - else: - identifier = class_name - open_buttons.append(self.create_button(identifier, instances)) - - children = pinned_buttons - separator_orientation = ( - Gtk.Orientation.VERTICAL - if self.view.get_orientation() == Gtk.Orientation.HORIZONTAL - else Gtk.Orientation.HORIZONTAL - ) - if pinned_buttons and open_buttons: - children += [ - Box( - orientation=separator_orientation, - v_expand=False, - h_expand=False, - h_align="center", - v_align="center", - name="dock-separator", - ) - ] - children += open_buttons - - self.view.children = children - if not self.integrated_mode: - idle_add(self._update_size) - self._drag_in_progress = False - if not self.integrated_mode: - self.check_occlusion_state() - - def _update_size(self): - if self.integrated_mode: - return False - width, _ = self.view.get_preferred_width() - self.set_size_request(width, -1) - return False - - def get_clients(self): - try: - return json.loads(self.conn.send_command("j/clients").reply.decode()) - except json.JSONDecodeError: - return [] - - def get_focused(self): - try: - return json.loads( - self.conn.send_command("j/activewindow").reply.decode() - ).get("address", "") - except json.JSONDecodeError: - return "" - - def get_workspace(self): - try: - return json.loads( - self.conn.send_command("j/activeworkspace").reply.decode() - ).get("id", 0) - except json.JSONDecodeError: - return 0 - - def check_occlusion_state(self): - if self.integrated_mode: - return False - - if ( - self.is_mouse_over_dock_area - or self._drag_in_progress - or self._prevent_occlusion - ): - if not self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(True) - if not self.always_occluded: - self.dock_full.remove_style_class("occluded") - return True - - if self.always_occluded: - if self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(False) - self.dock_full.add_style_class("occluded") - return True - - occlusion_region = ( - ("bottom", self.effective_occlusion_size) - if self.actual_dock_is_horizontal - else ("right", self.effective_occlusion_size) - ) - is_occluded_by_window = check_occlusion(occlusion_region) - is_empty = not self.view.get_children() - - if is_occluded_by_window or is_empty: - if self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(False) - self.dock_full.add_style_class("occluded") - else: - if not self.dock_revealer.get_reveal_child(): - self.dock_revealer.set_reveal_child(True) - self.dock_full.remove_style_class("occluded") - - return True - - def _find_drag_target(self, widget): - children = self.view.get_children() - while widget is not None and widget not in children: - widget = widget.get_parent() if hasattr(widget, "get_parent") else None - return widget - - def on_drag_data_get(self, widget, drag_context, data_obj, info, time): - target = self._find_drag_target( - widget.get_parent() if isinstance(widget, Box) else widget - ) - if target is not None: - index = self.view.get_children().index(target) - data_obj.set_text(str(index), -1) - - def on_drag_data_received(self, widget, drag_context, x, y, data_obj, info, time): - target = self._find_drag_target( - widget.get_parent() if isinstance(widget, Box) else widget - ) - if target is None: - return - try: - source_index = int(data_obj.get_text()) - except (TypeError, ValueError): - return - - children = self.view.get_children() - try: - target_index = children.index(target) - except ValueError: - return - - if source_index != target_index: - separator_index = -1 - for i, child_item_loop in enumerate(children): - if child_item_loop.get_name() == "dock-separator": - separator_index = i - break - cross_section_drag = separator_index != -1 and ( - (source_index < separator_index and target_index > separator_index) - or (source_index > separator_index and target_index < separator_index) - ) - - child_item_to_move = children.pop(source_index) - children.insert(target_index, child_item_to_move) - self.view.children = children - self.update_pinned_apps(skip_update=not cross_section_drag) - if cross_section_drag: - GLib.idle_add(self.update_dock) - - def on_drag_end(self, widget, drag_context): - if not self._drag_in_progress: - return - - def process_drag_end(): - display = Gdk.Display.get_default() - _, x, y, _ = display.get_pointer() - - # Get the widget's allocation to check if drag ended outside - alloc = self.view.get_allocation() - widget_x = alloc.x - widget_y = alloc.y - widget_width = alloc.width - widget_height = alloc.height - - # Check if pointer is outside the dock area - if not ( - widget_x <= x <= widget_x + widget_width - and widget_y <= y <= widget_y + widget_height - ): - app_id_dragged = widget.app_identifier - instances_dragged = widget.instances - - # 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 - - 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 - if not self.integrated_mode: - self.check_occlusion_state() - - GLib.idle_add(process_drag_end) - - def check_config_change(self): - new_config = read_config() - if not self.integrated_mode: - new_always_occluded = data.DOCK_ALWAYS_OCCLUDED - if self.always_occluded != new_always_occluded: - self.always_occluded = new_always_occluded - self.check_occlusion_state() - - if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []): - self.config = new_config - self.pinned = self.config.get("pinned_apps", []) - self.update_app_map() - self.update_dock() - return True - - def update_pinned_apps_file(self): - config_path = get_relative_path("../config/dock.json") - try: - with open(config_path, "w") as file: - json.dump(self.config, file, indent=4) - return True - except Exception as e: - logging.error(f"Failed to write dock config: {e}") - return False - - def update_pinned_apps(self, skip_update=False): - pinned_children_data = [] - for child_widget in self.view.get_children(): - if child_widget.get_name() == "dock-separator": - break - if hasattr(child_widget, "app_identifier"): - if hasattr(child_widget, "desktop_app") and child_widget.desktop_app: - app = child_widget.desktop_app - app_data_obj = { - "name": app.name, - "display_name": app.display_name, - "window_class": app.window_class, - "executable": app.executable, - "command_line": app.command_line, - } - pinned_children_data.append(app_data_obj) - else: - pinned_children_data.append(child_widget.app_identifier) - - self.config["pinned_apps"] = pinned_children_data - self.pinned = pinned_children_data - file_updated = self.update_pinned_apps_file() - if file_updated and not skip_update: - self.update_dock() - - @staticmethod - def notify_config_change(): - for dock_instance in Dock._instances: - GLib.idle_add(dock_instance.check_config_change_immediate) - - def check_config_change_immediate(self): - new_config = read_config() - - if not self.integrated_mode: - previous_always_occluded = self.always_occluded - self.always_occluded = data.DOCK_ALWAYS_OCCLUDED - - if previous_always_occluded != self.always_occluded: - self.check_occlusion_state() - - if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []): - self.config = new_config - self.pinned = self.config.get("pinned_apps", []) - self.update_app_map() - self.update_dock() - return False - - @staticmethod - def update_visibility(visible): - for dock in Dock._instances: - dock.set_visible(visible) - if visible: - GLib.idle_add(dock.check_occlusion_state) - else: - if ( - hasattr(dock, "dock_revealer") - and dock.dock_revealer.get_reveal_child() - ): - dock.dock_revealer.set_reveal_child(False) diff --git a/modules/dock/applications.py b/modules/dock/applications.py index b2431a1..ab61a18 100644 --- a/modules/dock/applications.py +++ b/modules/dock/applications.py @@ -11,6 +11,7 @@ from gi.repository import Gdk, GLib, Gtk import config.data as data from utils.icon_resolver import IconResolver + def read_config(): """Read and return the full configuration from the JSON file, handling missing file.""" config_path = get_relative_path("../../config/dock.json") @@ -47,15 +48,31 @@ def read_config(): config_data = {"pinned_apps": []} return config_data + +def createSurfaceFromWidget(widget: Gtk.Widget) -> cairo.ImageSurface: + alloc = widget.get_allocation() + surface = cairo.ImageSurface( + cairo.Format.ARGB32, + alloc.width, + alloc.height, + ) + cr = cairo.Context(surface) + cr.set_source_rgba(255, 255, 255, 0) + cr.rectangle(0, 0, alloc.width, alloc.height) + cr.fill() + widget.draw(cr) + return surface + + class Applications(Box): - def __init__(self, orientation_val="h", **kwargs): + def __init__(self, orientation_val="h", dock_instance=None, **kwargs): super().__init__( - name="applications-dock", - orientation=orientation_val, - spacing=4, - **kwargs + name="applications-dock", orientation=orientation_val, spacing=4, **kwargs ) + # Store reference to the dock instance + self.dock_instance = dock_instance + self.config = read_config() self.conn = get_hyprland_connection() self.icon_resolver = IconResolver() @@ -65,10 +82,10 @@ class Applications(Box): self._all_apps = get_desktop_applications() self.app_identifiers = self._build_app_identifiers_map() self._drag_in_progress = False - + # Initialize with empty content self.update_applications() - + # Connect to Hyprland events for live updates if self.conn.ready: self.update_applications() @@ -78,10 +95,10 @@ class Applications(Box): # Connect to window events to update in real-time for ev in ("activewindow", "openwindow", "closewindow", "changefloatingmode"): self.conn.connect(f"event::{ev}", lambda *args: self.update_applications()) - + # Set up periodic refresh for config changes GLib.timeout_add_seconds(1, self.check_config_change) - + def _build_app_identifiers_map(self): app_map = {} for app in self._all_apps: @@ -92,25 +109,31 @@ class Applications(Box): if app.name: app_map[app.name.lower()] = app return app_map - + def find_app_by_key(self, key): if not key: return None key_lower = key.lower() if isinstance(key, str) else "" return self.app_identifiers.get(key_lower) - + def find_app(self, app_identifier): if not app_identifier: return None if isinstance(app_identifier, dict): - for key in ["window_class", "executable", "command_line", "name", "display_name"]: + for key in [ + "window_class", + "executable", + "command_line", + "name", + "display_name", + ]: if key in app_identifier and app_identifier[key]: app = self.find_app_by_key(app_identifier[key]) if app: return app return None return self.find_app_by_key(app_identifier) - + def create_button(self, app_identifier, instances): desktop_app = self.find_app(app_identifier) icon_img = None @@ -165,23 +188,23 @@ class Applications(Box): [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)], Gdk.DragAction.MOVE, ) - + # Add drag destination capabilities button.drag_dest_set( Gtk.DestDefaults.ALL, [Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0)], Gdk.DragAction.MOVE, ) - + # Connect drag signals button.connect("drag-begin", self.on_drag_begin) button.connect("drag-data-get", self.on_drag_data_get) button.connect("drag-data-received", self.on_drag_data_received) button.connect("drag-end", self.on_drag_end) button.connect("button-press-event", self.on_button_press) - + return button - + def handle_app(self, app_identifier, instances, desktop_app=None): if not instances: if not desktop_app: @@ -215,7 +238,7 @@ class Applications(Box): address = instances[0].get("address") if address: exec_shell_command(f"hyprctl dispatch focuswindow address:{address}") - + def update_pinned_apps_file(self): config_path = get_relative_path("../../config/dock.json") try: @@ -225,7 +248,7 @@ class Applications(Box): except Exception as e: logging.error(f"Failed to write dock config: {e}") return False - + def check_config_change(self): new_config = read_config() if new_config.get("pinned_apps", []) != self.config.get("pinned_apps", []): @@ -234,20 +257,20 @@ class Applications(Box): self.update_app_map() self.update_applications() return True - + def update_app_map(self): self._all_apps = get_desktop_applications() self.app_identifiers = self._build_app_identifiers_map() - + def _normalize_window_class(self, class_name): # Implementation from dock.py return class_name.lower() - + def update_applications(self): # Clear existing children for child in self.get_children(): self.remove(child) - + # Get running windows from Hyprland clients = [] try: @@ -256,7 +279,7 @@ class Applications(Box): clients = json.loads(clients_data.decode("utf-8")) except Exception as e: logging.error(f"Failed to get clients: {e}") - + # Group clients by window class running_windows = {} for client in clients: @@ -265,11 +288,11 @@ class Applications(Box): if class_name not in running_windows: running_windows[class_name] = [] running_windows[class_name].append(client) - + # Create pinned app buttons pinned_buttons = [] used_window_classes = set() - + for app_data_item in self.pinned: app = self.find_app(app_data_item) instances = [] @@ -277,12 +300,18 @@ class Applications(Box): possible_identifiers = [] if isinstance(app_data_item, dict): - for key in ["window_class", "executable", "command_line", "name", "display_name"]: + for key in [ + "window_class", + "executable", + "command_line", + "name", + "display_name", + ]: if key in app_data_item and app_data_item[key]: possible_identifiers.append(app_data_item[key].lower()) elif isinstance(app_data_item, str): possible_identifiers.append(app_data_item.lower()) - + # Try to match with running windows for class_name, window_instances in running_windows.items(): for identifier in possible_identifiers: @@ -292,18 +321,18 @@ class Applications(Box): break if matched_class: break - + if matched_class: used_window_classes.add(matched_class) - + pinned_buttons.append(self.create_button(app_data_item, instances)) - + # Create buttons for non-pinned running apps open_buttons = [] for class_name, instances in running_windows.items(): if class_name not in used_window_classes: app = self.find_app_by_key(class_name) - + if app: app_data_obj = { "name": app.name, @@ -315,19 +344,19 @@ class Applications(Box): identifier = app_data_obj else: identifier = class_name - + open_buttons.append(self.create_button(identifier, instances)) - + # Add all buttons to the dock children = pinned_buttons - + if pinned_buttons and open_buttons: separator_orientation = ( Gtk.Orientation.VERTICAL if self.get_orientation() == Gtk.Orientation.HORIZONTAL else Gtk.Orientation.HORIZONTAL ) - + children.append( Box( orientation=separator_orientation, @@ -338,19 +367,19 @@ class Applications(Box): name="dock-separator", ) ) - + children.extend(open_buttons) - + # Add all children to the dock for child in children: self.add(child) - + # Show or hide based on whether there are any applications if len(children) > 0: self.show_all() else: self.hide() - + return True def on_button_press(self, widget, event): @@ -358,18 +387,21 @@ class Applications(Box): if event.button == 3: # Right mouse button app_identifier = widget.app_identifier instances = widget.instances - + # Check if app is already pinned is_pinned = False for pinned_app in self.pinned: - if (isinstance(app_identifier, dict) and isinstance(pinned_app, dict) and - app_identifier.get("name") == pinned_app.get("name")): + if ( + isinstance(app_identifier, dict) + and isinstance(pinned_app, dict) + and app_identifier.get("name") == pinned_app.get("name") + ): is_pinned = True break elif app_identifier == pinned_app: is_pinned = True break - + # If not pinned, add to pinned apps if not is_pinned: if hasattr(widget, "desktop_app") and widget.desktop_app: @@ -384,7 +416,7 @@ class Applications(Box): self.pinned.append(app_data_obj) else: self.pinned.append(app_identifier) - + self.config["pinned_apps"] = self.pinned self.update_pinned_apps_file() self.update_applications() @@ -393,17 +425,11 @@ class Applications(Box): def on_drag_begin(self, widget, drag_context): self._drag_in_progress = True - Gtk.drag_set_icon_surface(drag_context, self.create_surface_from_widget(widget)) - - def create_surface_from_widget(self, widget): - # Create a surface from the widget for drag icon - allocation = widget.get_allocation() - surface = cairo.ImageSurface( - cairo.FORMAT_ARGB32, allocation.width, allocation.height - ) - ctx = cairo.Context(surface) - widget.draw(ctx) - return surface + Gtk.drag_set_icon_surface(drag_context, createSurfaceFromWidget(widget)) + + # Prevent dock from hiding during drag + if self.dock_instance: + self.dock_instance.prevent_hiding(True) def on_drag_data_get(self, widget, drag_context, data_obj, info, time): # Find the index of the button being dragged @@ -417,61 +443,75 @@ class Applications(Box): source_index = int(data_obj.get_text()) except (TypeError, ValueError): return - + children = self.get_children() target_index = children.index(widget) - - if source_index != target_index: - # Reorder the pinned apps - if source_index < len(self.pinned) and target_index < len(self.pinned): - item = self.pinned.pop(source_index) - self.pinned.insert(target_index, item) - self.config["pinned_apps"] = self.pinned - self.update_pinned_apps_file() - self.update_applications() - def on_drag_end(self, widget, drag_context): - if not hasattr(self, "_drag_in_progress") or not self._drag_in_progress: - return - - # Get pointer position - display = widget.get_display() - seat = display.get_default_seat() - pointer = seat.get_pointer() - screen, x, y = pointer.get_position() - - # Get widget position and size - widget_alloc = widget.get_allocation() - widget_x, widget_y = widget.translate_coordinates(self, 0, 0) - if widget_x is None: # Widget might not be in the same window - widget_x, widget_y = 0, 0 - widget_width, widget_height = widget_alloc.width, widget_alloc.height - - # Check if pointer is outside the dock area - if not (widget_x <= x <= widget_x + widget_width and - widget_y <= y <= widget_y + widget_height): - app_id_dragged = widget.app_identifier - instances_dragged = widget.instances - - # Remove pinned app if it's in the pinned list - 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 + if source_index != target_index: + # Find separator index to determine pinned vs unpinned sections + separator_index = -1 + for i, child in enumerate(children): + if child.get_name() == "dock-separator": + separator_index = i break - if app_index_dragged >= 0: - self.pinned.pop(app_index_dragged) + # Check if we're dragging from unpinned to pinned section + cross_section_drag = separator_index != -1 and ( + (source_index > separator_index and target_index < separator_index) + ) + + # If dragging from unpinned to pinned, add to pinned apps + if cross_section_drag: + source_widget = children[source_index] + app_identifier = source_widget.app_identifier + + # Add to pinned apps if not already pinned + if hasattr(source_widget, "desktop_app") and source_widget.desktop_app: + app = source_widget.desktop_app + app_data_obj = { + "name": app.name, + "display_name": app.display_name, + "window_class": app.window_class, + "executable": app.executable, + "command_line": app.command_line, + } + self.pinned.insert(target_index, app_data_obj) + else: + self.pinned.insert(target_index, app_identifier) + self.config["pinned_apps"] = self.pinned self.update_pinned_apps_file() self.update_applications() - elif instances_dragged: - address = instances_dragged[0].get("address") - if address: - exec_shell_command(f"hyprctl dispatch focuswindow address:{address}") + else: + # Regular reordering within the same section + if source_index < separator_index and target_index < separator_index: + # Reorder within pinned section + item = self.pinned.pop(source_index) + self.pinned.insert(target_index, item) + self.config["pinned_apps"] = self.pinned + self.update_pinned_apps_file() + self.update_applications() + elif source_index > separator_index and target_index > separator_index: + # Reorder within unpinned section - just reorder the widgets + # since unpinned apps aren't stored in config + child_to_move = children.pop(source_index) + children.insert(target_index, child_to_move) + + # Update the UI + for child in self.get_children(): + self.remove(child) + for child in children: + self.add(child) + self.show_all() + + def on_drag_end(self, widget, drag_context): + if not self._drag_in_progress: + return + + # Process drag end... self._drag_in_progress = False + + # Allow dock to hide again + if self.dock_instance: + self.dock_instance.prevent_hiding(False) diff --git a/modules/dock/components.py b/modules/dock/components.py index f5dc442..105450b 100644 --- a/modules/dock/components.py +++ b/modules/dock/components.py @@ -14,11 +14,22 @@ from modules.dock.applications import Applications class DockComponents(Box): - def __init__(self, orientation_val="h", **kwargs): + def __init__(self, orientation_val="h", dock_instance=None, **kwargs): super().__init__( - name="system-tray", orientation=orientation_val, spacing=4, **kwargs + name="dock-components", + orientation=orientation_val, + spacing=8, + **kwargs ) - + + self.dock_instance = dock_instance + + # Create applications with dock reference + self.applications = Applications( + orientation_val=orientation_val, + dock_instance=dock_instance + ) + # Initialize component visibility from data self.component_visibility = data.DOCK_COMPONENTS_VISIBILITY @@ -45,9 +56,21 @@ class DockComponents(Box): name="tray", orientation="h" if not data.VERTICAL else "v", ) - self.applications = Applications( - orientation_val=orientation_val - ) + + # Add methods to connect drag signals + self.connect_drag_signals() + + def connect_drag_signals(self): + # Method to connect drag signals from applications to parent + pass + + def set_drag_callback(self, on_drag_begin_cb, on_drag_end_cb): + """Set callbacks for drag operations from applications component""" + # Connect to applications drag signals + self.applications.connect("drag-begin", + lambda widget, context: on_drag_begin_cb()) + self.applications.connect("drag-end", + lambda widget, context: on_drag_end_cb()) # Add components based on position if data.DOCK_POSITION == "Right": diff --git a/modules/dock/main.py b/modules/dock/main.py index 8a51183..d17fec2 100644 --- a/modules/dock/main.py +++ b/modules/dock/main.py @@ -12,7 +12,6 @@ from utils.wayland import WaylandWindow as Window from modules.dock.components import DockComponents - class Dock(Window): _instances = [] @@ -107,7 +106,14 @@ class Dock(Window): # Create components using DockComponents self.components = DockComponents( - orientation_val="h" if not data.VERTICAL else "v" + orientation_val="h" if not data.VERTICAL else "v", + dock_instance=self + ) + + # Connect drag signals from applications component + self.components.set_drag_callback( + on_drag_begin_cb=self.on_app_drag_begin, + on_drag_end_cb=self.on_app_drag_end ) # Add components based on position @@ -454,3 +460,29 @@ class Dock(Window): and dock.dock_revealer.get_reveal_child() ): dock.dock_revealer.set_reveal_child(False) + + def on_app_drag_begin(self): + """Called when application drag begins""" + self._drag_in_progress = True + # Ensure dock is visible during drag + self.dock_revealer.set_reveal_child(True) + if not self.always_occluded: + self.dock_full.remove_style_class("occluded") + + def on_app_drag_end(self): + """Called when application drag ends""" + self._drag_in_progress = False + # Check if we should hide the dock + GLib.idle_add(self.check_occlusion_state) + + def prevent_hiding(self, prevent=True): + """Prevent the dock from hiding""" + self._prevent_occlusion = prevent + if prevent: + # Force dock to be visible + self.dock_revealer.set_reveal_child(True) + if not self.always_occluded: + self.dock_full.remove_style_class("occluded") + else: + # Check if we should hide + GLib.idle_add(self.check_occlusion_state)