Files
SDG-DRIFT/Ax_Shell/modules/dock.py
T
2026-06-03 21:26:54 +02:00

858 lines
37 KiB
Python

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()