diff --git a/README.md b/README.md index 379a9b4..45bb8c3 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,23 @@ You can also toggle most settings from the **Settings window** (`Super + I`). See [Configuration Guide](docs/configuration.md) for full reference. +## Desktop Widgets + +Drop a `.py` file in `config/desktop/` and it appears on your desktop instantly: + +```bash +cp examples/desktop/example.py config/desktop/example.py +``` + +Edit positions in `config/desktop.toml` or drag widgets in edit mode (right-click → Edit Widgets). + +```bash +nvim config/desktop/example.py # live reload, no restart needed +rm config/desktop/example.py # widget disappears within 2s +``` + +See [Desktop Widgets Guide](docs/desktop_widgets.md) for full docs and examples. + ## Documentation | Doc | What's inside | @@ -131,6 +148,7 @@ See [Configuration Guide](docs/configuration.md) for full reference. | [Styling Guide](docs/styling.md) | Matugen colors, CSS customization | | [Architecture Overview](docs/architecture.md) | App Switcher C-backend, Spotlight engine, Services | | [Spotlight Plugins](docs/plugins.md) | Install/customize plugins, write your own | +| [Desktop Widgets](docs/desktop_widgets.md) | Custom desktop widgets, user widget system | | [FAQs & Tips](docs/faqs_tips.md) | Troubleshooting, shortcuts, live reload | ## Custom Mods diff --git a/docs/architecture.md b/docs/architecture.md index d83eec8..3f372d0 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -19,6 +19,7 @@ Entry point is `start.py`, delegates to `src/main.py`. `main.py` instantiates - **OSD**: Volume/brightness overlays (`src/window/osd/`) - **Settings**: Configuration GUI (`src/window/settings/`) - **Wallpaper Picker**: Wallpaper browser and management (`src/window/wallpaper/`) +- **Desktop Widgets**: User-defined widgets on the desktop background (`src/window/desktop/`) ## 2. Spotlight Search Engine @@ -87,7 +88,35 @@ macOS-style menu bar in the top panel. - Python daemon (`src/window/globalmenu/service.py`) listens for exported menus and renders them as Fabric widgets in the panel -## 5. State Management & Services +## 5. Desktop Widgets + +Desktop widgets render GTK widgets directly on the wallpaper background. + +### Widget System + +- **Registry** (`src/window/desktop/registry.py`): Stores widget classes, sizes, and default positions keyed by string id. +- **Built-in widgets** (`src/window/desktop/*.py`): Date, weather, calendar, CPU/RAM info. Each module registers itself at import time. +- **User widgets** (`config/desktop/*.py`): Drop-in `.py` files that call `DesktopWidgetRegistry.register()`. Loaded dynamically at startup and watched for live reload. +- **Position Manager** (`src/window/desktop/main.py`): Reads widget positions from `config/desktop.toml` (per-monitor, fractional coords). Merges with defaults from registry. Drag/drop saves overrides. +- **Desktop Window** (`src/window/desktop/main.py`): `DesktopWidgetWindow` uses a `Gtk.Fixed` container to position widgets at fractional screen coordinates. Handles edit mode (drag and drop), fade animations, and file monitoring. + +### Lifecycle + +``` +Startup: + 1. Import built-in widget modules → register widgets + 2. Scan config/desktop/*.py → import & register user widgets + 3. Read config/desktop.toml → get positions (or populate defaults) + 4. Create DesktopWidgetWindow per monitor → render widgets + +Runtime: + File monitor on config/desktop/ → detect add/delete + → unregister stale widgets → re-import new widgets → rebuild + 2-second poll fallback for missed file monitor events + Drag/drop in edit mode → save position to desktop.toml +``` + +## 6. State Management & Services Services in `src/services/` monitor system state asynchronously and emit signals: diff --git a/docs/configuration.md b/docs/configuration.md index afeffb8..cb8fe78 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -9,6 +9,8 @@ Modus is configured via TOML files in the `config/` directory, plus a graphical | `config.toml` | Main settings (features, widgets, paths) | Restart | | `mods.toml` | Custom panel buttons (menus, shell commands) | Instant | | `dock.toml` | Pinned dock apps | Restart | +| `desktop.toml` | Desktop widget positions (per-monitor) | Restart | +| `config/desktop/*.py` | User desktop widgets | Instant (live reload) | ## Settings Window @@ -156,3 +158,59 @@ pinned = [ ``` Use the exact `.desktop` file name or application ID. + +--- + +## Desktop Widgets (`config/desktop/` + `desktop.toml`) + +Desktop widgets render directly on your wallpaper. Drop a `.py` file in `config/desktop/` and it appears live. + +### Adding a Widget + +```bash +cp examples/desktop/example.py config/desktop/example.py +``` + +The widget file calls `DesktopWidgetRegistry.register()` with a key, class, size, and default position: + +```python +DesktopWidgetRegistry.register("my_widget", MyWidget, (174, 174), (0.5, 0.5)) +``` + +### Positioning + +Positions live in `config/desktop.toml`, one entry per monitor: + +```toml +[[0]] +key = "my_widget" +px = 0.5 +py = 0.5 +``` + +- `0` = monitor ID +- `px`, `py` = 0.0–1.0 fractions of screen + +Drag widgets in edit mode (right-click desktop → Edit Widgets) to reposition. + +### Removing a Widget + +Delete the `.py` file from `config/desktop/`: + +```bash +rm config/desktop/example.py +``` + +Widget disappears within 2 seconds. The entry is also removed from `desktop.toml`. + +### Built-in Widgets + +| Key | Description | +|-----|-------------| +| `date` | Date display | +| `weather` | Weather info | +| `calendar` | Monthly calendar | +| `cpu_info` | CPU usage | +| `ram_info` | RAM usage | + +See [Desktop Widgets Guide](desktop_widgets.md) for full documentation and examples. diff --git a/docs/desktop_widgets.md b/docs/desktop_widgets.md new file mode 100644 index 0000000..0c8c6d1 --- /dev/null +++ b/docs/desktop_widgets.md @@ -0,0 +1,210 @@ +# Desktop Widgets Guide + +Modus displays widgets directly on your desktop background. Drop a `.py` file in `config/desktop/` and it appears live — no restart needed. + +## Quick Start + +Copy the example widget: + +```bash +cp examples/desktop/example.py config/desktop/example.py +``` + +The widget appears on your desktop immediately. Edit positions by dragging in edit mode (right-click desktop → Edit Widgets) or by editing `config/desktop.toml`. + +## How It Works + +1. On startup, Modus scans `config/desktop/` for `.py` files. +2. Each file is imported. It calls `DesktopWidgetRegistry.register()` to register a widget class with a key, size, and default position. +3. `config/desktop.toml` stores per-monitor positions (`px`, `py` as 0.0–1.0 fractions). +4. A file monitor + 2-second poll watches `config/desktop/` — adding or removing files live-reloads widgets. + +## Writing a Desktop Widget + +### Minimal Example + +File: `config/desktop/example.py` + +```python +from fabric.widgets.box import Box +from fabric.widgets.label import Label + +from window.desktop.registry import DesktopWidgetRegistry + + +class ExampleClockWidget(Box): + def __init__(self, **kwargs): + super().__init__( + name="example-clock-widget", + orientation="v", + h_expand=True, + v_expand=True, + h_align="center", + v_align="center", + **kwargs, + ) + self.label = Label( + name="example-clock-label", + label="Hello Desktop", + justification="center", + ) + self.add(self.label) + + +DesktopWidgetRegistry.register( + "example", # unique key (used in desktop.toml) + ExampleClockWidget, # GTK widget class + (174, 174), # size (width, height) in pixels + (0.45, 0.45), # default position (px, py) as 0.0–1.0 fractions +) +``` + +### `DesktopWidgetRegistry.register()` Parameters + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `key` | `str` | Yes | Unique identifier (e.g., `"my_widget"`). Used in `desktop.toml`. | +| `widget_cls` | `class` | Yes | GTK widget class (must inherit from a Fabric/GTK widget). | +| `size` | `tuple[int, int]` | Yes | `(width, height)` in pixels. | +| `default_position` | `tuple[float, float]` | No | `(px, py)` as 0.0–1.0 screen fractions. Default `(0.0, 0.0)`. | + +### Widget Rules + +- Widget must be a GTK widget (inherit from `fabric.widgets.box.Box`, `fabric.widgets.label.Label`, etc.) +- Use `name=` for CSS styling (maps to `#example-clock-widget` in CSS) +- Set `h_expand=True` / `v_expand=True` for flexible sizing +- Call `DesktopWidgetRegistry.register()` at module level (not inside a function) +- The `key` must be unique across all widgets + +## Built-in Widgets + +| Key | Widget | Default Position | +|-----|--------|-----------------| +| `date` | Date display | Top-left `(0.0, 0.0)` | +| `weather` | Weather info | Top-left `(0.097, 0.0)` | +| `calendar` | Monthly calendar | Top-left `(0.191, 0.001)` | +| `cpu_info` | CPU usage | Bottom-right `(0.810, 0.827)` | +| `ram_info` | RAM usage | Bottom-right `(0.905, 0.827)` | + +## Positioning + +### Edit Mode (Drag and Drop) + +1. Right-click desktop → **Edit Widgets** +2. Drag widgets to new positions +3. Right-click → **Done Edit** +4. Positions save to `config/desktop.toml` + +### Config File + +Edit `config/desktop.toml` directly: + +```toml +[[0]] +key = "my_widget" +px = 0.5 +py = 0.5 +``` + +- `0` = monitor ID (0 for primary) +- `px`, `py` = position as 0.0–1.0 fractions of screen + +### Default Position + +Set in your widget file's `register()` call. Used when no entry exists in `desktop.toml`: + +```python +DesktopWidgetRegistry.register("my_widget", MyWidget, (174, 174), (0.3, 0.7)) +``` + +## Lifecycle + +| Action | What Happens | +|--------|-------------| +| Add `.py` file to `config/desktop/` | Widget appears on desktop (live) | +| Delete `.py` file from `config/desktop/` | Widget disappears (within 2s) | +| Edit `.py` file in `config/desktop/` | Widget rebuilds on next file change | +| Drag widget in edit mode | Position saves to `desktop.toml` | +| Edit `desktop.toml` manually | Position updates on restart | + +## Styling Widgets + +Use the widget's `name` parameter for CSS targeting: + +```python +self.label = Label(name="my-widget-label", label="Hello") +``` + +In your CSS (`src/styles/`), target with: + +```css +#my-widget-label { + font-size: 24px; + color: @theme_text_color; +} + +#my-widget-label:selected { + color: @accent_color; +} +``` + +## Advanced: Using Services + +Widgets can access Modus services for live data: + +```python +from fabric.utils import GLib, invoke_repeater +from fabric.widgets.box import Box +from fabric.widgets.label import Label + +from window.desktop.registry import DesktopWidgetRegistry + + +class LiveClockWidget(Box): + def __init__(self, **kwargs): + super().__init__( + name="live-clock", + orientation="v", + h_align="center", + v_align="center", + **kwargs, + ) + self.label = Label(name="clock-label", label="--:--") + self.add(self.label) + self._timer_id = invoke_repeater(1000, self._update) + + def _update(self) -> bool: + import datetime + now = datetime.datetime.now() + self.label.set_label(now.strftime("%H:%M")) + return True + + def destroy(self): + if hasattr(self, "_timer_id") and self._timer_id: + GLib.source_remove(self._timer_id) + super().destroy() + + +DesktopWidgetRegistry.register("live_clock", LiveClockWidget, (174, 174), (0.5, 0.0)) +``` + +## Troubleshooting + +### Widget not appearing? + +- Check the file is in `config/desktop/` (not a subdirectory) +- Check `journalctl --user -f -t modus` for `[DesktopWidgets]` errors +- Ensure the file calls `DesktopWidgetRegistry.register()` at module level +- Ensure the `key` is unique (not conflicting with built-in widgets) + +### Widget still showing after deleting the file? + +The file monitor + poll should clean up within 2 seconds. If not: +1. Check `journalctl --user -f -t modus` for `[DesktopWidgets] poll found stale:` messages +2. Restart Modus: `uv run start` + +### Position resets after restart? + +- `desktop.toml` stores your custom positions +- If you delete `desktop.toml`, positions revert to defaults from `register()` calls +- Ensure `config/desktop.toml` is writable diff --git a/docs/faqs_tips.md b/docs/faqs_tips.md index 5eb914b..a6dce97 100644 --- a/docs/faqs_tips.md +++ b/docs/faqs_tips.md @@ -49,6 +49,25 @@ Update `weather_location` in `config.toml` (e.g., `"London, UK"` or - The venv lives at `config/plugins/.venvs//` - Delete that directory to force re-install on next load +### 8. Desktop widget not appearing? + +- Check the file is in `config/desktop/` (not a subdirectory) +- Ensure it calls `DesktopWidgetRegistry.register()` at module level +- Check `journalctl --user -f -t modus` for `[DesktopWidgets]` errors +- Ensure the widget `key` is unique (not `"date"`, `"weather"`, etc.) + +### 9. Desktop widget still showing after deleting the file? + +The cleanup runs within 2 seconds via file monitor + poll fallback. If it persists: +- Check `journalctl --user -f -t modus` for stale widget messages +- Restart Modus: `uv run start` + +### 10. Desktop widget position resets after restart? + +- Positions are stored in `config/desktop.toml` +- If you delete `desktop.toml`, positions revert to defaults from `register()` calls +- Ensure `config/desktop.toml` is writable + --- ## Pro Tips @@ -56,6 +75,10 @@ Update `weather_location` in `config.toml` (e.g., `"London, UK"` or - **Live Reload for Mods**: `config/mods.toml` is monitored for changes. Edits apply instantly — no restart needed +- **Desktop Widgets**: Drop `.py` files in `config/desktop/` for instant widgets. + Edit `config/desktop.toml` or drag in edit mode to reposition. + Remove by deleting the `.py` file — disappears within 2 seconds. + - **Pinning Apps**: Use exact `.desktop` filename in `dock.toml` (e.g., `"org.gnome.Nautilus"`, not `"Files"`) diff --git a/examples/desktop/example.py b/examples/desktop/example.py new file mode 100644 index 0000000..81b330f --- /dev/null +++ b/examples/desktop/example.py @@ -0,0 +1,31 @@ +from fabric.widgets.box import Box +from fabric.widgets.label import Label + +from window.desktop.registry import DesktopWidgetRegistry + + +class ExampleClockWidget(Box): + def __init__(self, **kwargs): + super().__init__( + name="example-widget", + orientation="v", + h_expand=True, + v_expand=True, + h_align="center", + v_align="center", + **kwargs, + ) + self.label = Label( + name="examplelabel", + label="Hello Desktop", + justification="center", + ) + self.add(self.label) + + +DesktopWidgetRegistry.register( + "example", + ExampleClockWidget, + (174, 174), + (0.45, 0.45), +) diff --git a/src/services/desktop_widget.py b/src/services/desktop_widget.py index 58334b6..36c5a2d 100644 --- a/src/services/desktop_widget.py +++ b/src/services/desktop_widget.py @@ -73,9 +73,7 @@ class DesktopWidgetService(Service): return True def remove(self, monitor_id: int, key: str) -> bool: - removed = position_manager.remove(monitor_id, key) - if not removed: - return False + position_manager.remove_override(monitor_id, key) win = self._windows.get(monitor_id) if win: win.remove_widget(key) diff --git a/src/window/desktop/calendar.py b/src/window/desktop/calendar.py index dfe1251..7ffc097 100644 --- a/src/window/desktop/calendar.py +++ b/src/window/desktop/calendar.py @@ -143,4 +143,6 @@ class DesktopCalendarContainer(Box): ) -DesktopWidgetRegistry.register("calendar", DesktopCalendarContainer, (174, 174)) +DesktopWidgetRegistry.register( + "calendar", DesktopCalendarContainer, (174, 174), (0.19115, 0.00095) +) diff --git a/src/window/desktop/date.py b/src/window/desktop/date.py index 2aad087..631a859 100644 --- a/src/window/desktop/date.py +++ b/src/window/desktop/date.py @@ -49,4 +49,4 @@ class DesktopDateContainer(Box): ) -DesktopWidgetRegistry.register("date", DesktopDateContainer, (174, 174)) +DesktopWidgetRegistry.register("date", DesktopDateContainer, (174, 174), (0.0, 0.0)) diff --git a/src/window/desktop/main.py b/src/window/desktop/main.py index 143dd99..a1f845d 100644 --- a/src/window/desktop/main.py +++ b/src/window/desktop/main.py @@ -1,9 +1,10 @@ import importlib import os import pkgutil +import sys import tomlkit -from fabric.utils import Gdk, GLib, Gtk, logger +from fabric.utils import Gdk, Gio, GLib, Gtk, logger from fabric.widgets.box import Box from fabric.widgets.wayland import WaylandWindow @@ -22,19 +23,54 @@ for _, _name, _ in pkgutil.iter_modules([_dir]): # Config-backed position manager (stores percentage-based positions) # -_DEFAULT_POSITIONS = [ - {"key": "date", "px": 0.0, "py": 0.0}, - {"key": "weather", "px": 0.0974, "py": 0.0}, - {"key": "calendar", "px": 0.19271, "py": 0.0}, - {"key": "cpu_info", "px": 0.81042, "py": 0.82732}, - {"key": "ram_info", "px": 0.90521, "py": 0.82732}, -] - +_CONFIG_DESKTOP_DIR = os.path.join( + os.path.dirname(toml_file("desktop.toml")), "desktop" +) _TARGET = Gtk.TargetEntry.new("text/plain", Gtk.TargetFlags.SAME_APP, 0) +_user_widget_modules: dict[str, str] = {} +_user_key_to_file: dict[str, str] = {} + + +def _load_user_widgets() -> set[str]: + """Import every .py in config/desktop/. Returns set of loaded keys.""" + if not os.path.isdir(_CONFIG_DESKTOP_DIR): + return set() + loaded_keys: set[str] = set() + for fname in os.listdir(_CONFIG_DESKTOP_DIR): + if not fname.endswith(".py") or fname.startswith("_"): + continue + module_name = fname[:-3] + full_path = os.path.join(_CONFIG_DESKTOP_DIR, fname) + try: + before_keys = set(DesktopWidgetRegistry._widgets) + spec = importlib.util.spec_from_file_location( + f"user_widget_{module_name}", full_path + ) + mod = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = mod + spec.loader.exec_module(mod) + _user_widget_modules[module_name] = spec.name + new_keys = set(DesktopWidgetRegistry._widgets) - before_keys + for k in new_keys: + _user_key_to_file[k] = module_name + DesktopWidgetRegistry.register_user(k) + loaded_keys.update(new_keys) + except Exception as e: + logger.error(f"[DesktopWidgets] failed to load user widget {fname}: {e}") + return loaded_keys + + +_load_user_widgets() + + class PositionManager: - """Manages widget positions stored in desktop.toml as fractional coords.""" + """Merges widget defaults from registry with user overrides in desktop.toml. + + config/desktop/*.py widgets register themselves with a default position. + desktop.toml stores drag-and-drop overrides per monitor. + """ _file = toml_file("desktop.toml") @@ -61,36 +97,49 @@ class PositionManager: def get_widgets(cls, monitor_id: int) -> list[dict]: state = cls._read() mid_str = str(monitor_id) - if mid_str not in state: - state[mid_str] = list(_DEFAULT_POSITIONS) + if mid_str not in state or not state[mid_str]: + state[mid_str] = [] + for key in DesktopWidgetRegistry._widgets: + pos = DesktopWidgetRegistry.get_default_position(key) + px, py = pos if pos else (0.0, 0.0) + state[mid_str].append({"key": key, "px": px, "py": py}) cls._write(state) - return state[mid_str] + else: + registered = set(DesktopWidgetRegistry._widgets) + before = len(state[mid_str]) + state[mid_str] = [e for e in state[mid_str] if e["key"] in registered] + for key in registered - {e["key"] for e in state[mid_str]}: + pos = DesktopWidgetRegistry.get_default_position(key) + px, py = pos if pos else (0.0, 0.0) + state[mid_str].append({"key": key, "px": px, "py": py}) + if len(state[mid_str]) != before: + cls._write(state) + return list(state[mid_str]) @classmethod def save_position(cls, monitor_id: int, key: str, px: float, py: float) -> None: state = cls._read() mid_str = str(monitor_id) if mid_str not in state: - state[mid_str] = list(_DEFAULT_POSITIONS) + state[mid_str] = [] for entry in state[mid_str]: if entry["key"] == key: entry["px"] = round(max(0.0, min(1.0, px)), 5) entry["py"] = round(max(0.0, min(1.0, py)), 5) break else: - state[mid_str].append({"key": key, "px": px, "py": py}) + state[mid_str].append({"key": key, "px": round(px, 5), "py": round(py, 5)}) cls._write(state) @classmethod - def remove(cls, monitor_id: int, key: str) -> bool: + def remove_override(cls, monitor_id: int, key: str) -> None: + """Remove a drag/drop override so the widget reverts to its default position.""" state = cls._read() mid_str = str(monitor_id) if mid_str not in state: - return False - before = len(state[mid_str]) + return state[mid_str] = [e for e in state[mid_str] if e["key"] != key] cls._write(state) - return len(state[mid_str]) < before position_manager = PositionManager() @@ -157,6 +206,9 @@ class DesktopWidgetWindow(WaylandWindow): self.connect("size-allocate", self._on_size_allocate) self.connect("button-press-event", self._on_window_button_press) self._setup_drag_dest() + self._setup_file_monitor() + self._last_user_files: set[str] = set() + GLib.timeout_add(2000, self._poll_user_widgets) GLib.timeout_add(50, self._initial_build) @@ -202,6 +254,71 @@ class DesktopWidgetWindow(WaylandWindow): def _on_fade_out_finished(self, animator) -> None: self._fixed.set_opacity(0.0) + # fallback poll — catches deletions that Gio file monitor misses + + def _poll_user_widgets(self) -> bool: + if not os.path.isdir(_CONFIG_DESKTOP_DIR): + return True + current = { + f[:-3] + for f in os.listdir(_CONFIG_DESKTOP_DIR) + if f.endswith(".py") and not f.startswith("_") + } + if current != self._last_user_files: + self._last_user_files = current + stale = {k for k, f in _user_key_to_file.items() if f not in current} + if stale: + logger.info(f"[DesktopWidgets] poll found stale: {stale}") + for key in stale: + mod_name = _user_key_to_file.pop(key, None) + if mod_name: + sys.modules.pop(f"user_widget_{mod_name}", None) + _user_widget_modules.pop(mod_name, None) + DesktopWidgetRegistry.unregister_user_keys(stale) + _load_user_widgets() + self.rebuild() + return True + + # file monitor — live-rebuild when config/desktop/ changes + + def _setup_file_monitor(self) -> None: + if not os.path.isdir(_CONFIG_DESKTOP_DIR): + os.makedirs(_CONFIG_DESKTOP_DIR, exist_ok=True) + config_dir = Gio.File.new_for_path(_CONFIG_DESKTOP_DIR) + monitor = config_dir.monitor_directory(Gio.FileMonitorFlags.NONE, None) + monitor.connect("changed", self._on_config_changed) + self._config_monitor = monitor + + def _on_config_changed(self, _monitor, _file, _other, event_type) -> None: + logger.info(f"[DesktopWidgets] file monitor event: {event_type}") + if event_type in ( + Gio.FileMonitorEvent.CREATED, + Gio.FileMonitorEvent.DELETED, + Gio.FileMonitorEvent.CHANGED, + ): + GLib.idle_add(self._on_widget_dir_changed) + + def _on_widget_dir_changed(self) -> bool: + current_files = { + f[:-3] + for f in os.listdir(_CONFIG_DESKTOP_DIR) + if f.endswith(".py") and not f.startswith("_") + } + stale_keys = {k for k, f in _user_key_to_file.items() if f not in current_files} + logger.info( + f"[DesktopWidgets] dir changed: current={current_files}, " + f"stale={stale_keys}, tracked={set(_user_key_to_file.keys())}" + ) + for key in stale_keys: + mod_name = _user_key_to_file.pop(key, None) + if mod_name: + sys.modules.pop(f"user_widget_{mod_name}", None) + _user_widget_modules.pop(mod_name, None) + DesktopWidgetRegistry.unregister_user_keys(stale_keys) + _load_user_widgets() + self.rebuild() + return False + # right-click context menu def _on_window_button_press(self, widget, event: Gdk.EventButton) -> bool: @@ -212,11 +329,13 @@ class DesktopWidgetWindow(WaylandWindow): def _show_context_menu(self, event: Gdk.EventButton) -> None: menu = Gtk.Menu() + label_text = "Done Edit" if self._edit_mode else "Edit Widgets" edit_item = Gtk.CheckMenuItem(label=label_text) edit_item.set_active(self._edit_mode) edit_item.connect("toggled", self._on_edit_toggled) menu.append(edit_item) + menu.show_all() menu.popup_at_pointer(event) diff --git a/src/window/desktop/registry.py b/src/window/desktop/registry.py index e7172a7..25bf9ef 100644 --- a/src/window/desktop/registry.py +++ b/src/window/desktop/registry.py @@ -1,11 +1,33 @@ class DesktopWidgetRegistry: _widgets = {} _sizes = {} + _default_positions = {} + _user_keys: set[str] = set() @classmethod - def register(cls, key: str, widget_cls, size: tuple[int, int]): + def register( + cls, + key: str, + widget_cls, + size: tuple[int, int], + default_position: tuple[float, float] | None = None, + ): cls._widgets[key] = widget_cls cls._sizes[key] = size + if default_position is not None: + cls._default_positions[key] = default_position + + @classmethod + def register_user(cls, key: str) -> None: + cls._user_keys.add(key) + + @classmethod + def unregister_user_keys(cls, keys: set[str]) -> None: + for key in keys: + cls._widgets.pop(key, None) + cls._sizes.pop(key, None) + cls._default_positions.pop(key, None) + cls._user_keys.discard(key) @classmethod def get_widget_class(cls, key: str): @@ -14,3 +36,7 @@ class DesktopWidgetRegistry: @classmethod def get_widget_size(cls, key: str) -> tuple[int, int]: return cls._sizes.get(key, (174, 174)) + + @classmethod + def get_default_position(cls, key: str) -> tuple[float, float] | None: + return cls._default_positions.get(key) diff --git a/src/window/desktop/system.py b/src/window/desktop/system.py index a0b3f1b..5b13ae9 100644 --- a/src/window/desktop/system.py +++ b/src/window/desktop/system.py @@ -168,5 +168,9 @@ class DesktopCpuInfoWidget(SystemInfoBase): return True -DesktopWidgetRegistry.register("ram_info", DesktopRamInfoWidget, (174, 174)) -DesktopWidgetRegistry.register("cpu_info", DesktopCpuInfoWidget, (174, 174)) +DesktopWidgetRegistry.register( + "ram_info", DesktopRamInfoWidget, (174, 174), (0.90521, 0.82732) +) +DesktopWidgetRegistry.register( + "cpu_info", DesktopCpuInfoWidget, (174, 174), (0.81042, 0.82732) +) diff --git a/src/window/desktop/weather.py b/src/window/desktop/weather.py index da90773..917766c 100644 --- a/src/window/desktop/weather.py +++ b/src/window/desktop/weather.py @@ -129,4 +129,6 @@ class DesktopWeatherWidget(Box): super().destroy() -DesktopWidgetRegistry.register("weather", DesktopWeatherWidget, (174, 174)) +DesktopWidgetRegistry.register( + "weather", DesktopWeatherWidget, (174, 174), (0.0974, 0.0) +)