Files
ai-control/cinnamon-extension/ai-control-popup@local/extension.js
T
marcus hinz bbb06e0814 Popup auf SNI-Desktops: Activate-Koordinaten (Anchor::Click) + Cinnamon-Extension
- SNI-Activate-Koordinaten gehen als Anchor::Click durch; Cinnamon liefert
  echte Werte, bei (0,0) bleibt der Cursor-Fallback (KDE/XFCE).
- Neue Cinnamon-Extension ai-control-popup@local: platziert das Popup unter
  Wayland am Zeiger (Work-Area-geklemmt, set_position ist dort wirkungslos)
  und gibt ihm unter X11 den initialen Fokus (Muffins Focus-Stealing-
  Prevention verwirft das present() der App).
- Extension in deb/rpm nach /usr/share/cinnamon/extensions paketiert.
- README: Cinnamon-Abschnitte und Installer-Teststand aktualisiert.

Verifiziert in der VM-Flotte: Cinnamon Wayland + X11, XFCE, KDE (Wayland),
GNOME (Wayland).
2026-07-21 11:24:18 +02:00

70 lines
2.4 KiB
JavaScript

/* 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;
}
}