Replace window realm frame decorations with nicer label decorations

This commit is contained in:
2024-05-06 18:25:56 -04:00
parent ee57f6de95
commit 661e293434
16 changed files with 517 additions and 792 deletions

View File

@ -162,7 +162,7 @@
<file>ui/realms/realmManager.js</file>
<file>ui/realms/realmSearchProvider.js</file>
<file>ui/realms/realmSwitcher.js</file>
<file>ui/realms/realmWindowFrame.js</file>
<file>ui/realms/realmLabels.js</file>
<file>ui/realms/realmWindowMenu.js</file>
</gresource>
</gresources>

View File

@ -2959,6 +2959,14 @@ export const AppIcon = GObject.registerClass({
iconParams['createIcon'] = this._createIcon.bind(this);
iconParams['setSizeManually'] = true;
this.icon = new IconGrid.BaseIcon(app.get_name(), iconParams);
if (Main.realmManager.appIconLabelsEnabled()) {
const realmLabel = Main.realmManager.createRealmLabelForApp(app);
if (realmLabel) {
this.icon._box.insert_child_at_index(realmLabel, 0);
}
}
this._iconContainer.add_child(this.icon);
this._dot = new St.Widget({

431
js/ui/realms/realmLabels.js Normal file
View File

@ -0,0 +1,431 @@
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;
}
})

View File

@ -1,15 +1,11 @@
import Clutter from 'gi://Clutter'
import Gio from 'gi://Gio'
import Meta from 'gi://Meta'
import Shell from 'gi://Shell'
import St from 'gi://St'
import * as Main from '../main.js';
import * as RealmIndicator from './realmIndicator.js';
import * as RealmSwitcher from './realmSwitcher.js';
import * as Lightbox from '../lightbox.js';
import * as RealmSearchProvider from './realmSearchProvider.js';
import * as RealmWindowFrame from './realmWindowFrame.js';
import * as RealmLabels from './realmLabels.js';
export const RealmManager = class {
constructor() {
@ -39,17 +35,20 @@ export const RealmManager = class {
});
this._switchAnimation = new RealmSwitcher.ContextSwitchAnimationController(this._realmIndicator);
this.labelManager = new RealmLabels.RealmLabelManager();
}
if (Main.overview._overview) {
this._searchResults = Main.overview._overview.controls._searchController._searchResults;
this._searchProvider = new RealmSearchProvider.RealmSearchProvider();
this._searchProvider.createResultDisplay(this._searchResults);
this._searchResults._registerProvider(this._searchProvider);
createRealmLabelForApp(app) {
let realmName = app.get_realm_name();
if (this.labelManager.appIconLabelsEnabled() && realmName) {
return this.labelManager.createAppIconLabelForRealm(realmName);
} else {
log("Not creating search provider because Main.overview._overview does not exist");
return null;
}
}
this._frameManager = new RealmWindowFrame.WindowFrameManager();
appIconLabelsEnabled() {
return this.labelManager.appIconLabelsEnabled();
}
animateSwitch(from, to, onComplete) {
@ -61,5 +60,4 @@ export const RealmManager = class {
if (!popup.show(binding.is_reversed(), binding.get_name(), binding.get_mask()))
popup.fadeAndDestroy();
}
};

View File

@ -1,332 +0,0 @@
import Clutter from 'gi://Clutter';
import Cogl from 'gi://Cogl';
import GObject from 'gi://GObject';
import Meta from 'gi://Meta';
import Shell from 'gi://Shell';
import St from 'gi://St';
export const WindowFrameManager = class WindowFrameManager {
constructor() {
this._realms = Shell.Realms.get_default();
let frames = this._realms.window_frames();
this._frame_effects = [];
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));
frames.connect('realm-frame-colors-changed', this._onFrameColorsChanged.bind(this));
this.trackWindows();
}
_onContextWindowMoved(workspaceManager, window) {
let actor = window.get_compositor_private();
if (actor) {
this.handleWindow(actor);
}
return Clutter.EVENT_PROPAGATE;
}
_handleWindowMap(shellwm, actor) {
this.handleWindow(actor);
return Clutter.EVENT_PROPAGATE;
}
_onContextRemoved(workspaceManager, id) {
this.trackWindows();
}
_onFrameColorsChanged(realms) {
this.trackWindows();
}
trackWindows() {
var actors = global.get_window_actors();
actors.forEach(a => this.handleWindow(a));
}
handleWindow(actor) {
let win = actor.metaWindow;
let win_id = win.get_stable_sequence();
let effect = this._frame_effects[win_id];
let frames = this._realms.window_frames();
if (frames.has_frame(win) && frames.is_frame_enabled(win)) {
let color = frames.color_for_window(win);
if (effect) {
effect.setColor(color);
} else {
let label = frames.label_for_window(win);
effect = new RealmFrameEffect(actor, color, label);
this._frame_effects[win_id] = effect;
}
} else if (effect) {
effect.removeEffect(actor);
this._frame_effects[win_id] = null;
}
}
}
const RealmFrameEffect = GObject.registerClass(
class RealmFrameEffect extends Clutter.Effect {
_init(actor, color, label_text) {
super._init();
this._frame_width = 2;
this._pipeline = null;
this._color = color;
this._label_on_top = true;
this._label = null;
this._label_text = label_text;
if (label_text) {
this._updateLabel(actor.metaWindow);
}
this._sizeChangedId = actor.metaWindow.connect('size-changed', window => {
this._updateLabel(window);
});
actor.add_effect(this);
}
removeEffect(actor) {
if (this._label) {
actor.remove_child(this._label);
this._label = null;
}
if (this._sizeChangedId) {
let win = actor.metaWindow;
win.disconnect(this._sizeChangedId);
this._sizeChangedId = 0;
}
actor.remove_effect(this);
}
_createLabel(actor, label_text) {
let label = new St.Label({
style_class: 'realm-frame-label',
z_position: 1.0,
});
label.set_text(' '+label_text+' ');
actor.add_child(label);
return label;
}
_updateLabel(window) {
if (!this._label_text) {
return;
}
if (window.is_fullscreen()) {
if (this._label) {
let actor = window.get_compositor_private();
actor.remove_child(this._label);
this._label = null;
}
} else if (!this._label) {
let actor = window.get_compositor_private();
this._label = this._createLabel(actor, this._label_text);
}
if (this._label) {
this._updateLabelPosition(window);
this._updateLabelColor();
}
}
_updateLabelPosition(window) {
if (!this._label_height) {
// If we scale the text, the reported size of the label will not be the value we need so
// save the initial value.
this._label_height = this._label.get_height();
}
let maximized = window.is_fullscreen() === true || // Fullscreen
[Meta.MaximizeFlags.BOTH, Meta.MaximizeFlags.VERTICAL].includes(window.get_maximized()); // Maximized
this._label_on_top = !maximized;
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;
if (window.get_client_type() === Meta.WindowClientType.WAYLAND) {
let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
if (scaleFactor !== 1) {
offsetX = offsetX / scaleFactor;
this._label.set_style(`font-size: ${12 / scaleFactor}pt;`);
}
offsetX -= 1;
offsetY -= 4;
}
// If label is on top and there is enough space above title bar move position up by label height
if (this._label_on_top && this._label_height <= offsetY) {
offsetY -= this._label_height;
} else if (maximized) {
offsetX = 0;
offsetY = 0;
}
this._label.set_position(offsetX, offsetY);
}
_updateLabelColor() {
let fg = new Clutter.Color({
red: 0,
green: 0,
blue: 0,
alpha: 96,
});
let bg = this._color.copy();
if (this._label_on_top) {
bg.alpha = 100;
} else {
bg.alpha = 200;
}
let clutter_text = this._label.get_clutter_text();
clutter_text.set_color(fg);
clutter_text.set_background_color(bg);
}
setColor(color) {
if (this._color && this._color.equal(color)) {
return;
}
this._color = color;
this.setPipelineColor();
if (this._label) {
this._updateLabelColor();
}
}
setPipelineColor() {
if (!this._color || !this._pipeline) {
return;
}
let s = this._color.to_string();
let cogl_color = new Cogl.Color();
cogl_color.init_from_4f(
this._color.red / 255.0,
this._color.green / 255.0,
this._color.blue / 255.0,
0xc4 / 255.0);
this._pipeline.set_color(cogl_color);
}
_calculate_frame_box(window, allocation) {
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;
let top = offsetY - 3;
let bottom = offsetY - 1;
let left = offsetX - 3;
let right = offsetX - 1;
let wayland = window.get_client_type() == Meta.WindowClientType.WAYLAND;
if (wayland) {
bottom += 4;
}
let fw = this._frame_width;
switch (window.get_maximized()) {
case Meta.MaximizeFlags.BOTH:
top += fw;
right += fw;
bottom += fw - (wayland ? 5 : 0);
left += fw;
break;
case Meta.MaximizeFlags.HORIZONTAL:
right += fw;
left += fw;
break;
case Meta.MaximizeFlags.VERTICAL:
top += fw;
bottom += fw;
break;
}
if (window.is_fullscreen()) {
top += 3;
right += 2;
bottom -= (wayland ? 3 : 0);
left += 3;
}
if (!wayland && !window.decorated && !window.is_fullscreen() && (window.get_maximized() !== Meta.MaximizeFlags.BOTH)) {
bottom += 4;
}
let x = left;
let y = top + fw;
let w = allocation.get_width() - (right + left);
let h = allocation.get_height() - (bottom + top + fw);
return [x, y, w, h];
}
draw_rect(node, x, y, width, height) {
const box = new Clutter.ActorBox();
box.set_origin(x, y);
box.set_size(width, height);
node.add_rectangle(box);
}
draw_hline(node, x, y, width, width_factor = 1) {
this.draw_rect(node, x, y, width, this._frame_width * width_factor);
}
draw_vline(node, x, y, height, width_factor = 1) {
this.draw_rect(node, x, y, this._frame_width * width_factor, height);
}
vfunc_paint_node(node, ctx) {
let actor = this.get_actor();
const actorNode = new Clutter.ActorNode(actor, -1);
node.add_child(actorNode);
if (!this._pipeline) {
let framebuffer = ctx.get_framebuffer();
let coglContext = framebuffer.get_context();
this._pipeline = Cogl.Pipeline.new(coglContext);
this.setPipelineColor();
}
const pipelineNode = new Clutter.PipelineNode(this._pipeline);
pipelineNode.set_name('Realm Frame');
node.add_child(pipelineNode);
let [x, y, width, height] = this._calculate_frame_box(actor.metaWindow, actor.get_allocation_box());
// Top
this.draw_hline(pipelineNode, x, y, width, 2);
// Right
this.draw_vline(pipelineNode, x + width, y, height);
// Bottom
this.draw_hline(pipelineNode, x, y + height, width);
// Left
this.draw_vline(pipelineNode, x, y, height);
}
});

View File

@ -1,7 +1,7 @@
import GObject from 'gi://GObject';
import Shell from 'gi://Shell';
import * as PopupMenu from '../popupMenu.js';
import * as Main from '../main.js';
function _windowAppId(window) {
const tracker = Shell.WindowTracker.get_default();
@ -79,22 +79,20 @@ function windowContextRealmName(window) {
}
}
export function enableFrameItem(window) {
const realms = Shell.Realms.get_default();
const frames = realms.window_frames();
if (!frames.has_frame(window)) {
export function enableLabelItem(window) {
const labelManager = Main.realmManager.labelManager;
if (!labelManager.windowNeedsLabel(window)) {
return null;
}
let enabled = frames.is_frame_enabled(window);
let item = new PopupMenu.PopupMenuItem("Display colored window frame");
let enabled = labelManager.getWindowLabelEnabled(window);
let item = new PopupMenu.PopupMenuItem("Display realm label on window");
if (enabled) {
item.setOrnament(PopupMenu.Ornament.CHECK);
}
item.connect('activate', () => {
let realms = Shell.Realms.get_default();
const frames = realms.window_frames();
frames.set_frame_enabled(window, !enabled);
Main.realmManager.labelManager.setWindowLabelEnabled(window, !enabled);
});
return item;

View File

@ -29,7 +29,7 @@ export class WindowMenu extends PopupMenu.PopupMenu {
this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem(s));
item = RealmWindowMenu.enableFrameItem(window);
item = RealmWindowMenu.enableLabelItem(window);
if (item) {
this.addMenuItem(item);
}