🔧 chore(dock): add system tray and update tray visibility in DockComponents

This commit is contained in:
sankalp committed 2025-06-07 23:02:52 +05:45
1 parent a6f47cc548
commit 904dfa2b6b
7 files changed
+377 -113

No files matched your search

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