import Clutter from 'gi://Clutter'; import GObject from 'gi://GObject'; import Meta from 'gi://Meta'; import Shell from 'gi://Shell'; import GLib from 'gi://GLib'; import Gio from 'gi://Gio'; import St from 'gi://St'; const LABEL_FADE_TIMEOUT = 1500; const LABEL_FADEIN_TIME = 250; const CITADEL_SETTINGS_SCHEMA = 'com.subgraph.citadel'; const LABEL_COLOR_LIST_KEY = 'label-color-list'; const REALM_LABEL_COLORS_KEY = 'realm-label-colors'; const REALM_LABEL_SHOW_CITADEL_KEY = 'realm-label-show-citadel'; const REALM_LABEL_SHOW_ALL_KEY = 'realm-label-show-all'; const REALM_LABEL_SHOW_APP_ICONS = 'realm-label-show-app-icons'; const CITADEL_LABEL_COLOR = 'rgb(200,0,0)'; const CITADEL_REALM_NAME = 'Citadel'; const WindowLabelColors = class WindowLabelColors { constructor(settings) { this._realm_label_colors = new Map(); this._defaultColors = []; const [_ok, color] = Clutter.Color.from_string(CITADEL_LABEL_COLOR); this.citadelColor = color; this._citadelSettings = settings; this.reloadColors(); } reloadColors() { this._loadDefaultColors(); this._loadRealmColors(); } colorForRealmName(name) { if (name === CITADEL_REALM_NAME) { return this.citadelColor; } else if (this._realm_label_colors.has(name)) { return this._realm_label_colors.get(name); } else { let color = this._allocateColor(); this._realm_label_colors.set(name, color); this._storeColors(); return color; } } _storeColors() { let entries = []; for(let [name,color] of this._realm_label_colors) { entries.push(`${name}:${color.to_string()}`); } entries.sort(); this._citadelSettings.set_strv(REALM_LABEL_COLORS_KEY, entries); } _loadDefaultColors() { this._defaultColors = []; let entries = this._citadelSettings.get_strv(LABEL_COLOR_LIST_KEY); entries.forEach(entry => { let [ok,color] = Clutter.Color.from_string(entry); if (ok) { this._defaultColors.push(color); } else { log(`RealmLabels: failed to parse default color entry: ${entry}`) } }); } _loadRealmColors() { this._realm_label_colors.clear(); let entries = this._citadelSettings.get_strv(REALM_LABEL_COLORS_KEY); entries.forEach(entry => { let parts = entry.split(":"); if (parts.length === 2) { let [ok,color] = Clutter.Color.from_string(parts[1]); if (ok) { this._realm_label_colors.set(parts[0], color); } else { log(`RealmLabels: Failed to parse color from realm color entry: ${entry}`); } } else { log(`RealmLabels: Invalid realm color entry: ${entry}`); } }); } _allocateColor() { // 1) No default colors? return a built in color if (this._defaultColors.length == 0) { return Clutter.Color.new(153, 193, 241, 255); } // 2) No default colors? Find first color on default color list that isn't used already let used_colors = Array.from(this._realm_label_colors.values()); for (const color of this._defaultColors) { if (!used_colors.some(c => c.equal(color))) { return color; } } // 3) Choose a random element of the default list let index = Math.floor(Math.random() * this._defaultColors.length); return this._defaultColors[index]; } } export const RealmLabelManager = class RealmLabelManager { constructor() { this._realms = Shell.Realms.get_default(); this._citadelSettings = new Gio.Settings({ schema_id: CITADEL_SETTINGS_SCHEMA }); this._colors = new WindowLabelColors(this._citadelSettings); this._showCitadelLabels = this._citadelSettings.get_boolean(REALM_LABEL_SHOW_CITADEL_KEY); this._showAllLabels = this._citadelSettings.get_boolean(REALM_LABEL_SHOW_ALL_KEY); this._showAppIconLabels = this._citadelSettings.get_boolean(REALM_LABEL_SHOW_APP_ICONS); this._citadelSettings.connect('changed', this._syncSettings.bind(this)); this._window_labels = new Map(); global.window_manager.connect('map', this._handleWindowMap.bind(this)); global.workspace_manager.connect('context-window-moved', this._onContextWindowMoved.bind(this)); global.workspace_manager.connect('context-removed', this._onContextRemoved.bind(this)); this._syncAllWindows(); } _syncSettings() { this._colors.reloadColors(); for (const label of this._window_labels.values()) { let color = this.colorForWindow(label.window); label.setColor(color); } this._showCitadelLabels = this._citadelSettings.get_boolean(REALM_LABEL_SHOW_CITADEL_KEY); this._showAllLabels = this._citadelSettings.get_boolean(REALM_LABEL_SHOW_ALL_KEY); this._showAppIconLabels = this._citadelSettings.get_boolean(REALM_LABEL_SHOW_APP_ICONS); this._syncAllWindows(); } _onContextWindowMoved(workspaceManager, window) { const actor = window.get_compositor_private(); if (actor) { this._syncWindow(actor); } return Clutter.EVENT_PROPAGATE; } _handleWindowMap(shellwm, actor) { this._syncWindow(actor); return Clutter.EVENT_PROPAGATE; } _onContextRemoved(workspaceManager, id) { this._syncAllWindows(); } _syncAllWindows() { const actors = global.get_window_actors(); actors.forEach(a => this._syncWindow(a)); } createAppIconLabelForRealm(realmName) { if (!realmName) { return null; } const color = this._colors.colorForRealmName(realmName); return new RealmAppIconLabel(realmName, color); } appIconLabelsEnabled() { return this._showAppIconLabels; } addLabelToWindow(actor, window) { const win_id = window.get_stable_sequence(); const name = this.realmNameForWindow(window); const color = this.colorForWindow(window); if (name && color) { this._window_labels.set(win_id, new RealmWindowLabel(actor, color, name)); window.connectObject('unmanaged', window => this.removeLabelFromWindow(window), this); } else { log(`RealmLabels: failed to add label to window`); } } removeLabelFromWindow(window) { const win_id = window.get_stable_sequence(); const label = this._window_labels.get(win_id); if (label) { label.destroy(); this._window_labels.delete(win_id); } } _getWindowLabel(window) { const win_id = window.get_stable_sequence(); return this._window_labels.get(win_id); } _syncWindow(actor) { const window = actor.metaWindow; const label = this._getWindowLabel(window); const needsLabel = this.windowNeedsLabel(window); if (label && !needsLabel) { this.removeLabelFromWindow(window); } else if (!label && needsLabel) { this.addLabelToWindow(actor, window); } } // Return 'true' if 'window' should have a label. windowNeedsLabel(window) { // Only show labels on window type 'NORMAL' if (window.get_window_type() !== Meta.WindowType.NORMAL) { return false; } // Only show labels on citadel windows if showCitadelLabels is enabled. if (this._realms.is_citadel_window(window)) { return this._showCitadelLabels; } // Unless showAllLabels is enabled only show label on 'foreign' windows return this._showAllLabels || this._realms.is_foreign_window(window); } realmNameForWindow(window) { if (this._realms.is_citadel_window(window)) { return "Citadel"; } const realm = this._realms.realm_by_window(window); if (realm) { return realm.get_realm_name(); } else { log(`RealmLabels: No realm found for window`); return null; } } colorForWindow(window) { if (this._realms.is_citadel_window(window)) { return this._colors.citadelColor; } const realmName = this.realmNameForWindow(window); if (!realmName) { return null; } const color = this._colors.colorForRealmName(realmName); return color; } getWindowLabelEnabled(window) { const label = this._getWindowLabel(window); return label && label.visible } setWindowLabelEnabled(window, enabled) { const label = this._getWindowLabel(window); if (!label) { return; } if (enabled) { label.show(); } else { label.hide(); } } } const RealmAppIconLabel = GObject.registerClass( class RealmAppIconLabel extends St.Label { constructor(realmName, color) { super({ style_class: 'realm-app-icon-label', x_align: Clutter.ActorAlign.CENTER, }); this.set_text(realmName); this._color = null; this.setColor(color); } setColor(color) { if (this._color && this._color.equal(color)) { return; } if(color) { this._color = color; let c = color.to_string().substring(0, 7); this.set_style(`border-color: ${c};`); } } } ) const RealmWindowLabel = GObject.registerClass( class RealmWindowLabel extends St.Label { constructor(actor, color, realmName) { super({ style_class: 'realm-window-label', }); this.set_text(realmName); this._color = null; this.setColor(color); this.set_reactive(true); this._changedId = actor.metaWindow.connect('size-changed', window => { this._update(window); }); actor.add_child(this); this._actor = actor; this._update(actor.metaWindow); } setColor(color) { if (this._color && this._color.equal(color)) { return; } if(color) { this._color = color; let c = color.to_string().substring(0, 7); this.set_style(`background-color: ${c};`); } } get window() { return this._actor.metaWindow; } destroy() { if (this._timeoutId) { GLib.source_remove(this._timeoutId); this._timeoutId = 0; } this._actor.metaWindow.disconnect(this._changedId); this._actor.remove_child(this); } _easeOpacity(target) { this.ease({ opacity: target, duration: LABEL_FADEIN_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }) } _setDimmedLabel() { this.opacity = 50; this._resetTimer(); } _restoreDimmedLabel() { this._easeOpacity(255); } _updatePosition(window) { let frame_rect = window.get_frame_rect(); let buffer_rect = window.get_buffer_rect(); let offsetX = frame_rect.x - buffer_rect.x; let offsetY = frame_rect.y - buffer_rect.y; this.set_position(offsetX + 4, offsetY + 4); } _update(window) { if (window.is_fullscreen()) { this.hide(); } else { this.show(); this._updatePosition(window); } } _resendEvent(event) { // Window actors receive events on a different path (via an event filter) // than other actors on the stage. By setting (reactive = false) on the // RealmWindowLabel and resending the event, it will be processed by the // window actor. Afterward we set back (reactive = true). this.set_reactive(false); event.put(); this.set_reactive(true); } _resetTimer() { if (this._timeoutId) { GLib.source_remove(this._timeoutId); this._timeoutId = 0; } this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, LABEL_FADE_TIMEOUT, () => { this._timeoutId = 0; this._restoreDimmedLabel(); return GLib.SOURCE_REMOVE; }); } vfunc_enter_event(event) { this._resendEvent(event); super.vfunc_enter_event(event); this._setDimmedLabel(); return Clutter.EVENT_PROPAGATE; } vfunc_leave_event(event) { super.vfunc_leave_event(event); this._restoreDimmedLabel(); return Clutter.EVENT_PROPAGATE; } vfunc_motion_event(event) { this._resendEvent(event); this._setDimmedLabel(); return Clutter.EVENT_PROPAGATE; } vfunc_button_press_event(event) { this._resendEvent(event); return Clutter.EVENT_PROPAGATE; } vfunc_button_release_event(event) { this._resendEvent(event); return Clutter.EVENT_PROPAGATE; } })