diff --git a/README.md b/README.md index 4f37482..33ce160 100644 --- a/README.md +++ b/README.md @@ -64,11 +64,11 @@ Releases carry a `.deb`, an `.rpm`, and an AppImage. - **deb / rpm** — recommended. Both also install the GNOME Shell extension and the KWin script that provide the tray/popup integration on GNOME and KDE Wayland (into `/usr/share/gnome-shell/extensions/` and `/usr/share/kwin/scripts/`). - **AppImage** — runs on desktops with a standard tray (KDE, XFCE, Cinnamon). On GNOME it refuses to start and points to the deb/rpm: the tray there needs the shell extension, which an installation-free AppImage cannot provide. -Tray integration by desktop: GNOME via the bundled shell extension, KDE Wayland via StatusNotifierItem plus the bundled KWin script for popup placement, KDE X11/XFCE/Cinnamon via StatusNotifierItem directly. +Tray integration by desktop: GNOME via the bundled shell extension, KDE Wayland via StatusNotifierItem plus the bundled KWin script for popup placement, KDE X11/XFCE/Cinnamon via StatusNotifierItem directly. On Cinnamon **Wayland** sessions a bundled Cinnamon extension additionally places the popup at the tray icon (a Wayland client cannot position its own window); Cinnamon X11 needs no extension. ### Installer tests -The installers are hand-tested per release. Current state: Fedora 44 via rpm on GNOME, KDE Plasma, Cinnamon, and XFCE (VM test fleet); Ubuntu 26.04 (GNOME), Kubuntu (KDE Plasma, Wayland) and Linux Mint (Cinnamon) via deb in earlier rounds. Two known issues from the current round are open: on Cinnamon the tray popup opens at the top-left corner instead of at the tray icon, and on the X11 desktops (Cinnamon, XFCE) clicking outside the popup does not close it. macOS re-test of the current build is in progress. +The installers are hand-tested per release. Current state: Fedora 44 via rpm on GNOME, KDE Plasma, Cinnamon, and XFCE (VM test fleet); Ubuntu 26.04 (GNOME), Kubuntu (KDE Plasma, Wayland) and Linux Mint (Cinnamon) via deb in earlier rounds. The two open findings from that round are resolved: popup placement on Cinnamon Wayland now comes from the bundled Cinnamon extension, and click-outside-to-close verified fine on XFCE and Cinnamon. macOS re-test of the current build is in progress. ### Windows @@ -109,6 +109,7 @@ Deleting a project is scoped in three stages, each preceded by a preview of the │ ├── capabilities/ per-window ACL (main/popup/terminal/panel) │ └── icons/ app and tray icons ├── gnome-shell-extension/ tray/popup integration for GNOME +├── cinnamon-extension/ popup placement for Cinnamon Wayland ├── kwin-script/ popup placement for KDE Wayland ├── dev.sh development mode (tauri dev) ├── build.sh release build for the current OS (see below) diff --git a/cinnamon-extension/ai-control-popup@local/extension.js b/cinnamon-extension/ai-control-popup@local/extension.js new file mode 100644 index 0000000..73a88d4 --- /dev/null +++ b/cinnamon-extension/ai-control-popup@local/extension.js @@ -0,0 +1,69 @@ +/* Cinnamon-Extension (CJS, Muffin/Meta-0) + * ai-control-popup@local + * + * Das Tray-Icon kommt unter Cinnamon über SNI (xapp), die App zeigt das + * Popup selbst. Die Extension ergänzt, was der Client nicht kann: + * - Wayland: set_position aus dem Client ist wirkungslos — die Extension + * schiebt das frisch gemappte Popup an den Mauszeiger (der beim Klick auf + * dem Icon steht), mit derselben Klemm-Logik wie position_popup in app.rs. + * - X11: Muffins Focus-Stealing-Prevention verwirft das present() der App — + * ohne initialen Fokus schließt das Blur-Verstecken nie. Die Extension + * fokussiert das Popup beim Map aus dem Compositor heraus (platziert wird + * client-seitig über die SNI-Activate-Koordinaten). + */ + +const Meta = imports.gi.Meta; + +const WIN_TITLE = 'ai-control-popup'; + +let mapId = 0; + +function _place(win) { + const [px, py] = global.get_pointer(); + const rect = win.get_frame_rect(); + // Work-Area statt Monitor: die Panel-Struts bleiben frei, das Popup sitzt + // über der Leiste statt darauf. + const wa = win.get_work_area_current_monitor(); + const maxX = wa.x + wa.width - rect.width; + const maxY = wa.y + wa.height - rect.height; + const x = Math.min(Math.max(px - rect.width, wa.x), Math.max(maxX, wa.x)); + const y = Math.min(Math.max(py, wa.y), Math.max(maxY, wa.y)); + win.move_frame(true, x, y); +} + +// Beim Mappen unsichtbar schalten und vor dem nächsten Redraw positionieren — +// der erste sichtbare Frame sitzt schon am Zielort (Muster wie in der +// GNOME-Extension, dort über global.compositor.get_laters()). +function _placeBeforeRedraw(actor, win) { + actor.remove_all_transitions(); + actor.opacity = 0; + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + const rect = win.get_frame_rect(); + // Noch keine echte Größe -> nächster Redraw, bis positionierbar. + if (!rect || rect.width <= 1) return true; + _place(win); + actor.opacity = 255; + return false; + }); +} + +function init(_metadata) {} + +function enable() { + mapId = global.window_manager.connect('map', (_wm, actor) => { + const win = actor.meta_window; + if (!win || win.get_title() !== WIN_TITLE) return; + if (Meta.is_wayland_compositor()) { + _placeBeforeRedraw(actor, win); + } else { + win.activate(global.get_current_time()); + } + }); +} + +function disable() { + if (mapId) { + global.window_manager.disconnect(mapId); + mapId = 0; + } +} diff --git a/cinnamon-extension/ai-control-popup@local/metadata.json b/cinnamon-extension/ai-control-popup@local/metadata.json new file mode 100644 index 0000000..2f39dbd --- /dev/null +++ b/cinnamon-extension/ai-control-popup@local/metadata.json @@ -0,0 +1,7 @@ +{ + "uuid": "ai-control-popup@local", + "name": "ai-control popup placement", + "description": "ai-control-Tray-Popup: platziert es unter Wayland am Mauszeiger und gibt ihm unter X11 den initialen Fokus (Focus-Stealing-Prevention).", + "cinnamon-version": ["6.0", "6.2", "6.4", "6.6"], + "version": 1 +} diff --git a/src-tauri/src/app.rs b/src-tauri/src/app.rs index 2190e41..b7adfb8 100644 --- a/src-tauri/src/app.rs +++ b/src-tauri/src/app.rs @@ -61,19 +61,26 @@ pub(crate) fn show_popup(app: &tauri::AppHandle, anchor: Anchor) { }; let _ = w.set_position(tauri::PhysicalPosition::new(x, y)); } - // SNI (KDE/XFCE/Cinnamon): Activate liefert keine brauchbaren - // Koordinaten, aber der Zeiger sitzt beim Klick auf dem Icon. - Anchor::Cursor => { + // SNI (KDE/XFCE/Cinnamon): Activate-Koordinaten, wenn der Host welche + // mitgibt (Cinnamon); sonst Zeigerposition — der Zeiger sitzt beim + // Klick auf dem Icon. Unter Wayland liefert cursor_position (0,0). + Anchor::Click { x, y } => { + #[cfg(not(target_os = "linux"))] + let _ = (x, y); #[cfg(target_os = "linux")] { // Gelöst wird die Sperre vom ersten Focused(true) im Event-Handler. if let Some(g) = win.try_state::() { g.0.store(true, std::sync::atomic::Ordering::SeqCst); } - match win.cursor_position() { - Ok(p) => position_popup(&w, p.x as i32, p.y as i32), - Err(_) => { - let _ = w.center(); + if (x, y) != (0, 0) { + position_popup(&w, x, y); + } else { + match win.cursor_position() { + Ok(p) => position_popup(&w, p.x as i32, p.y as i32), + Err(_) => { + let _ = w.center(); + } } } } diff --git a/src-tauri/src/platform/linux/tray_sni.rs b/src-tauri/src/platform/linux/tray_sni.rs index 01df5a4..9657b5e 100644 --- a/src-tauri/src/platform/linux/tray_sni.rs +++ b/src-tauri/src/platform/linux/tray_sni.rs @@ -1,7 +1,7 @@ //! Eigenes StatusNotifierItem (ksni) statt Tauris libappindicator-Tray: //! libappindicator kann nur ein Menü, wir wollen Linksklick → Popup-Fenster. -//! Activate liefert keine brauchbaren Koordinaten — der Zeiger sitzt beim -//! Klick auf dem Icon, daher Anchor::Cursor. +//! Die Activate-Koordinaten gehen als Anchor::Click durch; ob sie brauchbar +//! sind, hängt vom SNI-Host ab (Cinnamon ja, KDE 0,0). use crate::platform::{Anchor, TrayCallbacks}; @@ -21,8 +21,8 @@ impl ksni::Tray for AiControlTray { fn icon_pixmap(&self) -> Vec { self.icon.clone() } - fn activate(&mut self, _x: i32, _y: i32) { - (self.cb.show)(&self.app, Anchor::Cursor); + fn activate(&mut self, x: i32, y: i32) { + (self.cb.show)(&self.app, Anchor::Click { x, y }); } fn menu(&self) -> Vec> { use ksni::menu::StandardItem; diff --git a/src-tauri/src/platform/mod.rs b/src-tauri/src/platform/mod.rs index 939b28f..4295ef7 100644 --- a/src-tauri/src/platform/mod.rs +++ b/src-tauri/src/platform/mod.rs @@ -55,9 +55,10 @@ pub(crate) enum Anchor { /// entscheidet die Plattform: Menüleiste oben → Popup darunter (macOS), /// Taskbar unten → Popup darüber (Windows). IconRect { rect: tauri::Rect, popup_below: bool }, - /// SNI/ksni liefert keine Koordinaten — der Zeiger steht beim Klick auf dem - /// Icon (KDE/XFCE/Cinnamon). - Cursor, + /// SNI/ksni (KDE/XFCE/Cinnamon): Klick-Koordinaten aus Activate. Cinnamon + /// liefert echte Werte; wo der Host keine mitgibt (0,0), fällt app.rs auf + /// die Zeigerposition zurück — der Zeiger steht beim Klick auf dem Icon. + Click { x: i32, y: i32 }, /// Positionierung übernimmt der Compositor bzw. die Shell-Extension /// (GNOME, KDE-Wayland/KWin). Managed, diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 468235d..a849634 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -33,6 +33,8 @@ "/usr/share/gnome-shell/extensions/ai-control-popup@local/extension.js": "../gnome-shell-extension/ai-control-popup@local/extension.js", "/usr/share/gnome-shell/extensions/ai-control-popup@local/metadata.json": "../gnome-shell-extension/ai-control-popup@local/metadata.json", "/usr/share/gnome-shell/extensions/ai-control-popup@local/trayLinux.png": "icons/trayLinux.png", + "/usr/share/cinnamon/extensions/ai-control-popup@local/extension.js": "../cinnamon-extension/ai-control-popup@local/extension.js", + "/usr/share/cinnamon/extensions/ai-control-popup@local/metadata.json": "../cinnamon-extension/ai-control-popup@local/metadata.json", "/usr/share/kwin/scripts/ai-control-popup/metadata.json": "../kwin-script/ai-control-popup/metadata.json", "/usr/share/kwin/scripts/ai-control-popup/contents/code/main.js": "../kwin-script/ai-control-popup/contents/code/main.js" } @@ -42,6 +44,8 @@ "/usr/share/gnome-shell/extensions/ai-control-popup@local/extension.js": "../gnome-shell-extension/ai-control-popup@local/extension.js", "/usr/share/gnome-shell/extensions/ai-control-popup@local/metadata.json": "../gnome-shell-extension/ai-control-popup@local/metadata.json", "/usr/share/gnome-shell/extensions/ai-control-popup@local/trayLinux.png": "icons/trayLinux.png", + "/usr/share/cinnamon/extensions/ai-control-popup@local/extension.js": "../cinnamon-extension/ai-control-popup@local/extension.js", + "/usr/share/cinnamon/extensions/ai-control-popup@local/metadata.json": "../cinnamon-extension/ai-control-popup@local/metadata.json", "/usr/share/kwin/scripts/ai-control-popup/metadata.json": "../kwin-script/ai-control-popup/metadata.json", "/usr/share/kwin/scripts/ai-control-popup/contents/code/main.js": "../kwin-script/ai-control-popup/contents/code/main.js" }