432 lines
14 KiB
JavaScript
432 lines
14 KiB
JavaScript
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;
|
|
}
|
|
})
|