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 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, monitor_id: int = 0, integrated_mode: bool = False, **kwargs): self.monitor_id = monitor_id 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 self.always_show = data.DOCK_ALWAYS_SHOW if not self.integrated_mode else False 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="auto" if self.always_show else "none", monitor=monitor_id, **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" if not self.integrated_mode: match data.BAR_POSITION: case "Top": self.set_margin("-8px 0px 0px 0px") case "Bottom": self.set_margin("0px 0px 0px 0px") case "Left": self.set_margin("0px 0px 0px -8px") case "Right": self.set_margin("0px -8px 0px 0px") case _: self.set_margin("0px 0px 0px 0px") 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.is_mouse_over_dock_area = False self._prevent_occlusion = False self._forced_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) # Hide normal dock when it should be embedded in the bar OR when dock is disabled should_be_embedded = (data.BAR_POSITION == "Bottom") or (data.PANEL_THEME == "Panel" and data.BAR_POSITION in ["Top", "Bottom"]) if should_be_embedded or not data.DOCK_ENABLED: self.set_visible(False) if self.always_show: 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(500, 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)) # Listen to window events to update dock when apps open/close self.conn.connect("event::openwindow", self.update_dock) self.conn.connect("event::closewindow", self.update_dock) if not self.integrated_mode: self.conn.connect("event::workspace", self.check_hide) GLib.timeout_add_seconds(2, 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_show: 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 if self._forced_occlusion: self.dock_revealer.set_reveal_child(False) else: 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_show: 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 if self._forced_occlusion: self.dock_revealer.set_reveal_child(False) else: self.delay_hide() if not self.always_show: 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 not self.always_show: 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 self.always_show: 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 # When forced occlusion is active, only show on hover if self._forced_occlusion: if self.is_mouse_over_dock_area: 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") return True 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_show: self.dock_full.remove_style_class("occluded") return True if self.always_show: 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") 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_show = data.DOCK_ALWAYS_SHOW if self.always_show != new_always_show: self.always_show = new_always_show 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_show = self.always_show self.always_show = data.DOCK_ALWAYS_SHOW if previous_always_show != self.always_show: 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) def force_occlusion(self): """Force dock to hide and act as if always_show is False.""" if self.integrated_mode: return # Save current always_show state self._saved_always_show = self.always_show # Set to False to enable hover behavior self.always_show = False self._forced_occlusion = True if not self.is_mouse_over_dock_area: self.dock_revealer.set_reveal_child(False) def restore_from_occlusion(self): """Restore dock to its previous always_show state.""" if self.integrated_mode: return self._forced_occlusion = False # Restore saved always_show state if hasattr(self, '_saved_always_show'): self.always_show = self._saved_always_show delattr(self, '_saved_always_show') self.check_occlusion_state()