Files
Modus/modules/dock/main.py
T

512 lines
18 KiB
Python

import json
import config.data as data
from fabric.hyprland.widgets import get_hyprland_connection
from fabric.widgets.box import Box
from fabric.widgets.eventbox import EventBox
from fabric.widgets.revealer import Revealer
from gi.repository import Gdk, GLib, Gtk
from modules.corners import MyCorner
from modules.dock.components import DockComponents
from utils.occlusion import check_occlusion
from utils.wayland import WaylandWindow as Window
class Dock(Window):
_instances = []
def __init__(self, **kwargs):
self.icon_size = 28
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
self.actual_dock_is_horizontal = not data.VERTICAL
if self.actual_dock_is_horizontal:
if data.DOCK_POSITION == "Top":
anchor_to_set = "top"
revealer_transition_type = "slide-down"
else: # Bottom
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.DOCK_POSITION == "Left":
anchor_to_set = "left"
revealer_transition_type = "slide-right"
elif data.DOCK_POSITION == "Right":
anchor_to_set = "right"
revealer_transition_type = "slide-left"
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 not data.DOCK_AUTO_HIDE else "none",
**kwargs,
)
Dock._instances.append(self)
self.conn = get_hyprland_connection()
self.hide_id = None
self._arranger_handler = None
self._drag_in_progress = False
self.always_occluded = data.DOCK_ALWAYS_OCCLUDED
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.DOCK_POSITION == "Left"
else ["right"]
if data.DOCK_POSITION == "Right"
else ["top"]
if data.DOCK_POSITION == "Top"
else [],
)
self.wrapper.set_orientation(dock_wrapper_orientation_val)
self.view.set_orientation(dock_wrapper_orientation_val)
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")
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.components = DockComponents(
orientation_val="h" if not data.VERTICAL else "v", dock_instance=self
)
if self.actual_dock_is_horizontal:
self.view.add(self.components)
elif data.DOCK_POSITION == "Left":
self.view.add(self.components)
else: # Right position
self.view.add(self.components)
self.corner_left = Box()
self.corner_right = Box()
self.corner_top = Box()
self.corner_bottom = Box()
if self.actual_dock_is_horizontal:
if data.DOCK_POSITION == "Top":
self.corner_left = Box(
name="dock-corner-left",
orientation=Gtk.Orientation.VERTICAL,
h_align="start",
children=[MyCorner("top-right"), Box(v_expand=True, v_align="fill")],
)
self.corner_right = Box(
name="dock-corner-right",
orientation=Gtk.Orientation.VERTICAL,
h_align="end",
children=[MyCorner("top-left"), Box(v_expand=True, v_align="fill")],
)
else: # Bottom
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",
margin=0,
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()
if self.actual_dock_is_horizontal:
# Bottom dock
self.hover_activator.set_size_request(-1, 1)
else:
# Vertical dock (Left or Right)
self.hover_activator.set_size_request(1, -1)
self.hover_activator.connect("enter-notify-event", self._on_hover_enter)
self.hover_activator.connect("leave-notify-event", self._on_hover_leave)
if self.actual_dock_is_horizontal:
if data.DOCK_POSITION == "Top":
# Top dock - revealer first, then hover activator
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.dock_revealer, self.hover_activator],
h_align=main_box_h_align_val,
)
else:
# Bottom dock - hover activator first, then revealer
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.hover_activator, self.dock_revealer],
h_align=main_box_h_align_val,
)
else:
# Vertical dock
if data.DOCK_POSITION == "Left":
# Left dock - revealer first, then hover activator
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.dock_revealer, self.hover_activator],
h_align=main_box_h_align_val,
)
else:
# Right dock - hover activator first, then revealer
self.main_box = Box(
orientation=main_box_orientation_val,
children=[self.hover_activator, self.dock_revealer],
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:
self.set_visible(False)
if self.always_occluded:
self.dock_full.add_style_class("occluded")
if self.conn.ready:
GLib.timeout_add(250, self.check_occlusion_state)
else:
self.conn.connect(
"event::ready",
lambda *args: GLib.timeout_add(250, self.check_occlusion_state),
)
self.conn.connect("event::workspace", self.check_hide)
GLib.timeout_add_seconds(1, self.check_config_change)
def _on_hover_enter(self, *args):
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):
self.is_mouse_over_dock_area = False
self.delay_hide()
def _on_dock_enter(self, widget, event):
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 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 delay_hide(self):
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):
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:
if self.actual_dock_is_horizontal:
occlusion_region = (
("top", self.effective_occlusion_size)
if data.DOCK_POSITION == "Top"
else ("bottom", self.effective_occlusion_size)
)
else:
occlusion_region = (
("left", self.effective_occlusion_size)
if data.DOCK_POSITION == "Left"
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.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 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.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 not data.DOCK_AUTO_HIDE:
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
if self.actual_dock_is_horizontal:
occlusion_region = (
("top", self.effective_occlusion_size)
if data.DOCK_POSITION == "Top"
else ("bottom", self.effective_occlusion_size)
)
else:
occlusion_region = (
("left", self.effective_occlusion_size)
if data.DOCK_POSITION == "Left"
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 _update_size(self):
width, _ = self.view.get_preferred_width()
self.set_size_request(width, -1)
return False
def check_config_change(self):
new_always_occluded = data.DOCK_ALWAYS_OCCLUDED
if self.always_occluded != new_always_occluded:
self.always_occluded = new_always_occluded
self.check_occlusion_state()
return True
def check_config_change_immediate(self):
previous_always_occluded = self.always_occluded
self.always_occluded = data.DOCK_ALWAYS_OCCLUDED
if previous_always_occluded != self.always_occluded:
self.check_occlusion_state()
return False
@staticmethod
def notify_config_change():
for dock_instance in Dock._instances:
GLib.idle_add(dock_instance.check_config_change_immediate)
@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 on_app_drag_begin(self):
self._drag_in_progress = True
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):
self._drag_in_progress = False
GLib.idle_add(self.check_occlusion_state)
def prevent_hiding(self, prevent=True):
self._prevent_occlusion = prevent
if prevent:
self.dock_revealer.set_reveal_child(True)
if not self.always_occluded:
self.dock_full.remove_style_class("occluded")
else:
GLib.idle_add(self.check_occlusion_state)