feat(src/window): switch configuration from JSON to TOML and refactor pinned apps handling
This commit is contained in:
7 files changed
+333
-41
No files matched your search
@@ -0,0 +1,241 @@
|
||||
from pathlib import Path
|
||||
|
||||
import tomlkit
|
||||
|
||||
from fabric.utils import GLib, logger
|
||||
from fabric.widgets.box import Box
|
||||
from fabric.widgets.button import Button
|
||||
from fabric.widgets.label import Label
|
||||
from gi.repository import Gio
|
||||
|
||||
from shared.window.applet_window import AppletWindow
|
||||
from utils.functions import run_command, thread
|
||||
from utils.utils import setup_cursor_hover, svg_file
|
||||
|
||||
MODS_CONFIG_PATH = (
|
||||
Path(__file__).resolve().parent.parent.parent.parent.parent / "config" / "mods.toml"
|
||||
)
|
||||
RELOAD_DELAY_MS = 200
|
||||
|
||||
|
||||
def dropdown_divider():
|
||||
return Box(
|
||||
children=[Box(name="dropdown-divider", h_expand=True)],
|
||||
name="dropdown-divider-box",
|
||||
h_align="fill",
|
||||
h_expand=True,
|
||||
)
|
||||
|
||||
|
||||
class ModPopup(AppletWindow):
|
||||
def __init__(
|
||||
self, mod_name, options, parent_window, pointing_to, source_button, **kwargs
|
||||
):
|
||||
self._mod_name = mod_name
|
||||
self._source_button = source_button
|
||||
|
||||
option_widgets = []
|
||||
for opt in sorted(options, key=lambda o: o.get("order", 99)):
|
||||
if opt.get("divider"):
|
||||
option_widgets.append(dropdown_divider())
|
||||
|
||||
btn = Button(
|
||||
name="dropdown-option",
|
||||
child=Label(label=opt["label"], name="dropdown-option-label"),
|
||||
h_align="fill",
|
||||
h_expand=True,
|
||||
)
|
||||
cmd = opt["on-clicked"]
|
||||
btn.connect("clicked", lambda *_, c=cmd: self._run_command(c))
|
||||
setup_cursor_hover(btn, "pointer")
|
||||
option_widgets.append(btn)
|
||||
|
||||
options_box = Box(
|
||||
name="dropdown-options",
|
||||
orientation="vertical",
|
||||
spacing=0,
|
||||
children=option_widgets,
|
||||
)
|
||||
|
||||
main_box = Box(name="dropdown-menu", children=[options_box])
|
||||
|
||||
super().__init__(
|
||||
parent=parent_window,
|
||||
pointing_to=pointing_to,
|
||||
name="dropdown-menu",
|
||||
title="modus-dropdown",
|
||||
layer="overlay",
|
||||
visible=False,
|
||||
**kwargs,
|
||||
)
|
||||
self.children = [main_box]
|
||||
self.connect("notify::visible", self._on_visible_changed)
|
||||
|
||||
def _on_visible_changed(self, *_):
|
||||
if self.get_visible():
|
||||
self._source_button.add_style_class("active")
|
||||
else:
|
||||
self._source_button.remove_style_class("active")
|
||||
|
||||
def _run_command(self, cmd):
|
||||
self.close_applet()
|
||||
thread(run_command, cmd.split(), timeout=30)
|
||||
|
||||
def toggle(self, pointing_to=None):
|
||||
if pointing_to:
|
||||
self.set_pointing_to(pointing_to)
|
||||
super().toggle()
|
||||
|
||||
|
||||
class CustomMods(Box):
|
||||
def __init__(self, parent_window, **kwargs):
|
||||
self._parent = parent_window
|
||||
self._monitors = []
|
||||
self._reload_pending = False
|
||||
self._mods_data = {}
|
||||
self._mod_buttons = {}
|
||||
self._mod_popups = {}
|
||||
super().__init__(name="custom-mods", orientation="h", spacing=0, **kwargs)
|
||||
self._load_mods()
|
||||
self._setup_monitor()
|
||||
|
||||
def _load_mods(self):
|
||||
try:
|
||||
if not MODS_CONFIG_PATH.exists():
|
||||
return
|
||||
with open(MODS_CONFIG_PATH) as f:
|
||||
data = tomlkit.load(f)
|
||||
except Exception as e:
|
||||
logger.error(f"[CustomMods] Failed to parse {MODS_CONFIG_PATH}: {e}")
|
||||
return
|
||||
|
||||
mods = {}
|
||||
for mod_name, val in data.get("Mods", {}).items():
|
||||
icon = val.get("icon", "")
|
||||
icon_size = val.get("icon-size", 18)
|
||||
order = val.get("order", 99)
|
||||
on_clicked = val.get("on-clicked")
|
||||
options = val.get("options", [])
|
||||
mods[mod_name] = {
|
||||
"name": mod_name,
|
||||
"icon": icon,
|
||||
"icon_size": icon_size,
|
||||
"order": order,
|
||||
"on-clicked": on_clicked,
|
||||
"options": options,
|
||||
}
|
||||
|
||||
self._rebuild(mods)
|
||||
|
||||
def _on_btn_press(self, btn, event, mod):
|
||||
popup = self._mod_popups.get(mod["name"])
|
||||
|
||||
if event.button == 3:
|
||||
if popup:
|
||||
popup.toggle(pointing_to=btn)
|
||||
return True
|
||||
|
||||
if event.button == 1:
|
||||
if popup and popup._is_open:
|
||||
popup.toggle()
|
||||
return True
|
||||
|
||||
cmd = mod.get("on-clicked")
|
||||
if cmd:
|
||||
thread(run_command, cmd.split(), timeout=30)
|
||||
return True
|
||||
|
||||
if popup:
|
||||
popup.toggle(pointing_to=btn)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def _rebuild(self, mods):
|
||||
for child in list(self.get_children()):
|
||||
self.remove(child)
|
||||
|
||||
for popup in self._mod_popups.values():
|
||||
if popup:
|
||||
popup.destroy()
|
||||
self._mod_popups.clear()
|
||||
self._mod_buttons.clear()
|
||||
self._mods_data.clear()
|
||||
|
||||
sorted_mods = sorted(mods.values(), key=lambda m: m.get("order", 99))
|
||||
|
||||
for mod in sorted_mods:
|
||||
icon_widget = svg_file(mod["icon"], size=mod["icon_size"])
|
||||
btn = Button(name="global-menu", child=icon_widget)
|
||||
setup_cursor_hover(btn, "pointer")
|
||||
|
||||
popup = None
|
||||
if mod["options"]:
|
||||
popup = ModPopup(
|
||||
mod_name=mod["name"],
|
||||
options=mod["options"],
|
||||
parent_window=self._parent,
|
||||
pointing_to=btn,
|
||||
source_button=btn,
|
||||
)
|
||||
|
||||
btn.connect(
|
||||
"button-press-event",
|
||||
lambda b, e, m=mod: self._on_btn_press(b, e, m),
|
||||
)
|
||||
|
||||
self.add(btn)
|
||||
self._mod_buttons[mod["name"]] = btn
|
||||
self._mod_popups[mod["name"]] = popup
|
||||
self._mods_data[mod["name"]] = mod
|
||||
|
||||
self.show_all()
|
||||
|
||||
def _setup_monitor(self):
|
||||
try:
|
||||
for path in [MODS_CONFIG_PATH, MODS_CONFIG_PATH.parent]:
|
||||
if not path.exists():
|
||||
continue
|
||||
gio_file = Gio.File.new_for_path(str(path))
|
||||
monitor = gio_file.monitor_file(Gio.FileMonitorFlags.NONE, None)
|
||||
monitor.connect("changed", self._on_file_changed)
|
||||
self._monitors.append(monitor)
|
||||
except Exception as e:
|
||||
logger.error(f"[CustomMods] Failed to setup monitor: {e}")
|
||||
|
||||
def _on_file_changed(self, monitor, gfile, other_file, event_type):
|
||||
if event_type == Gio.FileMonitorEvent.DELETED and gfile.get_path() == str(
|
||||
MODS_CONFIG_PATH
|
||||
):
|
||||
self._schedule_reload()
|
||||
return
|
||||
valid_events = [
|
||||
Gio.FileMonitorEvent.CHANGES_DONE_HINT,
|
||||
Gio.FileMonitorEvent.CHANGED,
|
||||
Gio.FileMonitorEvent.CREATED,
|
||||
]
|
||||
if event_type in valid_events and not self._reload_pending:
|
||||
path = gfile.get_path()
|
||||
if path and path != str(MODS_CONFIG_PATH):
|
||||
return
|
||||
self._schedule_reload()
|
||||
|
||||
def _schedule_reload(self):
|
||||
self._reload_pending = True
|
||||
GLib.timeout_add(RELOAD_DELAY_MS, self._reload_config)
|
||||
|
||||
def _reload_config(self):
|
||||
self._reload_pending = False
|
||||
self._load_mods()
|
||||
return False
|
||||
|
||||
def destroy(self):
|
||||
for monitor in self._monitors:
|
||||
monitor.cancel()
|
||||
for popup in self._mod_popups.values():
|
||||
if popup:
|
||||
popup.destroy()
|
||||
self._mod_popups.clear()
|
||||
self._mod_buttons.clear()
|
||||
self._mods_data.clear()
|
||||
super().destroy()
|
||||
@@ -0,0 +1,20 @@
|
||||
from fabric.widgets.button import Button
|
||||
|
||||
from services.google_lens import GoogleLens
|
||||
from utils.functions import thread
|
||||
from utils.utils import setup_cursor_hover, svg_file
|
||||
|
||||
|
||||
class GoogleLensIndicator(Button):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(name="panel-button", **kwargs)
|
||||
self._lens = GoogleLens.get_initial()
|
||||
|
||||
self.icon = svg_file("google-lens.svg", size=16)
|
||||
self.add(self.icon)
|
||||
|
||||
setup_cursor_hover(self, "pointer")
|
||||
self.connect("clicked", self.on_clicked)
|
||||
|
||||
def on_clicked(self, *_):
|
||||
thread(self._lens.search)
|
||||
@@ -15,6 +15,8 @@ from window.controlcenter.main import ModusControlCenter
|
||||
from window.notification.notification_center import NotificationCenter
|
||||
from window.panel.components.enhanced_system_tray import apply_enhanced_system_tray
|
||||
from window.panel.components.globalmenu import GlobalMenu
|
||||
from window.panel.components.custom_mods import CustomMods
|
||||
from window.panel.components.google_lens_indicator import GoogleLensIndicator
|
||||
from window.panel.components.indicators import (
|
||||
BatteryIndicator,
|
||||
BluetoothIndicator,
|
||||
@@ -109,6 +111,8 @@ class Panel(Window):
|
||||
)
|
||||
setup_cursor_hover(self.datetime_btn, "pointer")
|
||||
|
||||
self.google_lens_indicator = GoogleLensIndicator()
|
||||
self.custom_mods = CustomMods(parent_window=self)
|
||||
self.workspace_indicator = WorkspaceIndicator()
|
||||
self.recording_indicator = RecordingIndicator()
|
||||
|
||||
@@ -235,6 +239,10 @@ class Panel(Window):
|
||||
|
||||
# Right
|
||||
right_children = []
|
||||
if config_data.get("custom_mods", True):
|
||||
right_children.append(self.custom_mods)
|
||||
if config_data.get("google_lens", True):
|
||||
right_children.append(self.google_lens_indicator)
|
||||
if config_data.get("workspace_indicator", True):
|
||||
right_children.append(self.workspace_indicator)
|
||||
|
||||
@@ -268,6 +276,8 @@ class Panel(Window):
|
||||
keys = {
|
||||
"imac_button",
|
||||
"global_menu",
|
||||
"google_lens",
|
||||
"custom_mods",
|
||||
"workspace_indicator",
|
||||
"systray",
|
||||
"battery",
|
||||
@@ -328,6 +338,8 @@ class Panel(Window):
|
||||
# Destroy components
|
||||
for component in [
|
||||
self.globalmenu,
|
||||
self.google_lens_indicator,
|
||||
self.custom_mods,
|
||||
self.workspace_indicator,
|
||||
self.recording_indicator,
|
||||
self.indicators,
|
||||
|
||||
Reference in new issue
Block a user