SVG-Icons statt ICNS, Popup-Höhe an Monitor begrenzt, GNOME-Shell-Extension für Popup-Positionierung, Bundle-Targets explizit
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
/* GNOME Shell Extension (ESM, GNOME 45+)
|
||||
* ai-control-popup@local
|
||||
* Dünner Relay: Panel-Button -> D-Bus Show() an die ai-control-App, dann das
|
||||
* rahmenlose Popup-Fenster der App unter den Button schieben (Compositor darf das).
|
||||
*/
|
||||
|
||||
import St from 'gi://St';
|
||||
import Gio from 'gi://Gio';
|
||||
import GLib from 'gi://GLib';
|
||||
import Clutter from 'gi://Clutter';
|
||||
import Meta from 'gi://Meta';
|
||||
|
||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||||
import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
|
||||
|
||||
const DBUS_NAME = 'com.aicontrol.Popup';
|
||||
const DBUS_PATH = '/com/aicontrol/Popup';
|
||||
const DBUS_IFACE = 'com.aicontrol.Popup1';
|
||||
const WIN_TITLE = 'ai-control-popup';
|
||||
const ICON_PATH = '/home/marcuh/projects/ai-control/src-tauri/icons/trayLinux.png';
|
||||
|
||||
export default class AiControlPopupExtension extends Extension {
|
||||
enable() {
|
||||
this._proxy = Gio.DBusProxy.new_for_bus_sync(
|
||||
Gio.BusType.SESSION,
|
||||
Gio.DBusProxyFlags.DO_NOT_AUTO_START,
|
||||
null,
|
||||
DBUS_NAME,
|
||||
DBUS_PATH,
|
||||
DBUS_IFACE,
|
||||
null,
|
||||
);
|
||||
|
||||
// Beim Mappen unsichtbar schalten und unmittelbar vor dem nächsten Redraw
|
||||
// positionieren. Der BEFORE_REDRAW-Later läuft garantiert, bevor der erste
|
||||
// Frame gemalt wird — der erste sichtbare Frame sitzt damit schon am Zielort,
|
||||
// kein zentrierter Zwischenframe.
|
||||
this._mapId = global.window_manager.connect('map', (_wm, actor) => {
|
||||
const w = actor.meta_window;
|
||||
if (!w || w.get_title() !== WIN_TITLE) return;
|
||||
actor.remove_all_transitions();
|
||||
actor.opacity = 0;
|
||||
this._placeBeforeRedraw(actor, w);
|
||||
});
|
||||
|
||||
// Panel-Button nur zeigen, solange die App den D-Bus-Namen hält.
|
||||
this._watchId = Gio.bus_watch_name(
|
||||
Gio.BusType.SESSION,
|
||||
DBUS_NAME,
|
||||
Gio.BusNameWatcherFlags.NONE,
|
||||
() => this._addButton(),
|
||||
() => this._removeButton(),
|
||||
);
|
||||
}
|
||||
|
||||
_addButton() {
|
||||
if (this._button) return;
|
||||
// dontCreateMenu=true: wir wollen kein Shell-Menü, nur den Klick relayen.
|
||||
this._button = new PanelMenu.Button(0.0, 'ai-control', true);
|
||||
const icon = new St.Icon({
|
||||
gicon: Gio.icon_new_for_string(ICON_PATH),
|
||||
icon_size: 36,
|
||||
});
|
||||
this._button.add_child(icon);
|
||||
this._button.connect('button-press-event', () => {
|
||||
this._toggle();
|
||||
return Clutter.EVENT_STOP;
|
||||
});
|
||||
Main.panel.addToStatusArea('ai-control-popup', this._button, 0, 'right');
|
||||
}
|
||||
|
||||
_removeButton() {
|
||||
this._button?.destroy();
|
||||
this._button = null;
|
||||
}
|
||||
|
||||
_toggle() {
|
||||
const win = this._findWindow();
|
||||
if (win && win.showing_on_its_workspace()) {
|
||||
this._call('Hide');
|
||||
return;
|
||||
}
|
||||
// Fenster unsichtbar halten, bis es am Platz sitzt (kein Sprung Mitte->Tray).
|
||||
const actor = win ? win.get_compositor_private() : null;
|
||||
if (actor) actor.opacity = 0;
|
||||
this._call('Show');
|
||||
this._positionWhenReady();
|
||||
}
|
||||
|
||||
_call(method) {
|
||||
try {
|
||||
this._proxy.call_sync(method, null, Gio.DBusCallFlags.NONE, -1, null);
|
||||
} catch (e) {
|
||||
logError(e, 'ai-control-popup: D-Bus call failed (App läuft?)');
|
||||
}
|
||||
}
|
||||
|
||||
_position(win) {
|
||||
const [bx] = this._button.get_transformed_position();
|
||||
const bw = this._button.width;
|
||||
const rect = win.get_frame_rect();
|
||||
const wa = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
|
||||
let x = Math.round(bx + bw - rect.width);
|
||||
if (x < wa.x) x = wa.x;
|
||||
// Höhe auf die Arbeitsfläche klemmen, sonst schiebt der Compositor ein zu
|
||||
// hohes Fenster nach oben aus dem Sichtbereich (oberer Teil verschwindet).
|
||||
const h = Math.min(rect.height, wa.height);
|
||||
win.move_resize_frame(true, x, wa.y, rect.width, h);
|
||||
}
|
||||
|
||||
_placeBeforeRedraw(actor, w) {
|
||||
const laters = global.compositor.get_laters();
|
||||
laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
|
||||
const rect = w.get_frame_rect();
|
||||
// Noch keine echte Größe -> nächster Redraw, bis positionierbar.
|
||||
if (!rect || rect.width <= 1) return GLib.SOURCE_CONTINUE;
|
||||
this._position(w);
|
||||
actor.opacity = 255;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
});
|
||||
}
|
||||
|
||||
_positionWhenReady() {
|
||||
if (this._moveId) GLib.source_remove(this._moveId);
|
||||
let waited = 0;
|
||||
let held = 0;
|
||||
// Erst positionieren, wenn das Fenster gemappt ist UND eine echte Größe hat
|
||||
// (frisch gezeigt liefert get_frame_rect kurz 0 -> off-screen beim 1. Klick).
|
||||
// Danach ~400 ms weiter nachziehen, falls der Compositor nachplatziert.
|
||||
let placed = false;
|
||||
this._moveId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 25, () => {
|
||||
const win = this._findWindow();
|
||||
const actor = win ? win.get_compositor_private() : null;
|
||||
const rect = win ? win.get_frame_rect() : null;
|
||||
const ready = win && win.showing_on_its_workspace() && rect && rect.width > 1;
|
||||
if (ready) {
|
||||
this._position(win);
|
||||
// Erst nach dem Positionieren im selben Frame sichtbar schalten.
|
||||
if (!placed) {
|
||||
placed = true;
|
||||
if (actor) actor.opacity = 255;
|
||||
}
|
||||
held += 25;
|
||||
if (held >= 400) {
|
||||
this._moveId = 0;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
}
|
||||
// Noch nicht platziert -> unsichtbar halten.
|
||||
if (actor) actor.opacity = 0;
|
||||
waited += 25;
|
||||
if (waited > 1500) {
|
||||
// Nicht platzierbar: trotzdem sichtbar schalten, nie unsichtbar hängen lassen.
|
||||
if (actor) actor.opacity = 255;
|
||||
this._moveId = 0;
|
||||
return GLib.SOURCE_REMOVE;
|
||||
}
|
||||
return GLib.SOURCE_CONTINUE;
|
||||
});
|
||||
}
|
||||
|
||||
_findWindow() {
|
||||
for (const actor of global.get_window_actors()) {
|
||||
const w = actor.meta_window;
|
||||
if (w && w.get_title() === WIN_TITLE) return w;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
disable() {
|
||||
if (this._watchId) {
|
||||
Gio.bus_unwatch_name(this._watchId);
|
||||
this._watchId = 0;
|
||||
}
|
||||
if (this._moveId) {
|
||||
GLib.source_remove(this._moveId);
|
||||
this._moveId = 0;
|
||||
}
|
||||
if (this._mapId) {
|
||||
global.window_manager.disconnect(this._mapId);
|
||||
this._mapId = 0;
|
||||
}
|
||||
this._removeButton();
|
||||
this._proxy = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"uuid": "ai-control-popup@local",
|
||||
"name": "ai-control Popup",
|
||||
"description": "Panel-Button; relayt den Klick per D-Bus an die ai-control-App und schiebt deren Popup-Fenster unter den Button.",
|
||||
"shell-version": ["48", "49", "50"],
|
||||
"version": 1
|
||||
}
|
||||
Reference in New Issue
Block a user