From dd70139b8f3f0d684433bf39bea2bb4fc8c931f0 Mon Sep 17 00:00:00 2001 From: Leleat Date: Thu, 9 May 2024 19:09:28 +0200 Subject: [PATCH 1/2] FocusHint: Add a focus hint This commits basically integrates https://github.com/Leleat/focus-indicator-prototype into Tiling Assistant. The other extension was just a prototype in cooperation with a GNOME Designer. But it is more polished than the active window hint that is currently in Tiling Assistant. I did some slight refactoring. I also removed the focus indication when using swipe gestures since the animation didn't work quite well (and had a bug during the animation with multiple monitors). This time however, the focus hint will be disabled by default as I don't believe it is that useful of a feature anymore, it deviates a bit too much from the stock GNOME behavior, and it may even be an unexpected behavior from a users perspective, if they don't know what it is or how it gets triggered. This is feature is also less tested as they are a lot of permutations of situations to test (multi-monitor, workspaces on all/ primary display, etc). Additionally, it is put behind the advanced and experimental settings toggle. Some of the settings for the focus hint are not exposed in the UI (like the outline border radius, outline) because I'd like to cut down on the settings. Even putting them behind the advanced toggle doesn't feel good because eventually the advanced settings will grow so big that there is at least one setting for someone out there, which will eventually make the advanced settings 'mandatory'. I kinda regret exposing so many settings in the prefs UI... oh well. Fixes https://github.com/Leleat/Tiling-Assistant/issues/306 Fixes https://github.com/Leleat/Tiling-Assistant/issues/222 --- .../extension.js | 4 + tiling-assistant@leleat-on-github/prefs.js | 14 +- ...ll.extensions.tiling-assistant.gschema.xml | 12 + .../src/common.js | 7 + .../src/dependencies/shell.js | 2 + .../src/extension/focusHint.js | 876 ++++++++++++++++++ .../src/ui/prefs.ui | 92 ++ 7 files changed, 1004 insertions(+), 3 deletions(-) create mode 100644 tiling-assistant@leleat-on-github/src/extension/focusHint.js diff --git a/tiling-assistant@leleat-on-github/extension.js b/tiling-assistant@leleat-on-github/extension.js index 7a33abd..816181c 100644 --- a/tiling-assistant@leleat-on-github/extension.js +++ b/tiling-assistant@leleat-on-github/extension.js @@ -25,6 +25,7 @@ import KeybindingHandler from './src/extension/keybindingHandler.js'; import LayoutsManager from './src/extension/layoutsManager.js'; import ActiveWindowHint from './src/extension/activeWindowHint.js'; import AltTabOverride from './src/extension/altTab.js'; +import FocusHintManager from './src/extension/focusHint.js'; import { Rect } from './src/extension/utility.js'; /** @@ -161,6 +162,7 @@ export default class TilingAssistantExtension extends Extension { this._keybindingHandler = new KeybindingHandler(); this._layoutsManager = new LayoutsManager(); this._activeWindowHintHandler = new ActiveWindowHint(); + this._focusHintManager = new FocusHintManager(); this._altTabOverride = new AltTabOverride(); // Disable native tiling. @@ -241,6 +243,8 @@ export default class TilingAssistantExtension extends Extension { this._layoutsManager = null; this._activeWindowHintHandler.destroy(); this._activeWindowHintHandler = null; + this._focusHintManager.destroy(); + this._focusHintManager = null; this._altTabOverride.destroy(); this._altTabOverride = null; diff --git a/tiling-assistant@leleat-on-github/prefs.js b/tiling-assistant@leleat-on-github/prefs.js index 27a3040..aefa00f 100644 --- a/tiling-assistant@leleat-on-github/prefs.js +++ b/tiling-assistant@leleat-on-github/prefs.js @@ -100,8 +100,7 @@ export default class Prefs extends ExtensionPreferences { 'screen-left-gap', 'screen-right-gap', 'screen-bottom-gap', - 'active-window-hint-border-size', - 'active-window-hint-inner-border-size', + 'focus-hint-outline-size', 'toggle-maximize-tophalf-timer', 'vertical-preview-area', 'horizontal-preview-area' @@ -135,7 +134,7 @@ export default class Prefs extends ExtensionPreferences { */ _bindColorButtons(settings, builder) { const switches = [ - 'active-window-hint-color' + 'focus-hint-color' ]; switches.forEach(key => { @@ -177,6 +176,15 @@ export default class Prefs extends ExtensionPreferences { 'active_window_hint_always_row' ] }, + { + key: 'focus-hint', + rowNames: [ + 'disabled_focus_hint_row', + 'animated_outline_focus_hint_row', + 'animated_upscale_focus_hint_row', + 'static_outline_focus_hint_row' + ] + }, { key: 'default-move-mode', rowNames: [ diff --git a/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml b/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml index 1f1db6e..50c651e 100644 --- a/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml +++ b/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml @@ -20,6 +20,18 @@ 0 + + 0 + + + '' + + + 8 + + + 8 + 1 diff --git a/tiling-assistant@leleat-on-github/src/common.js b/tiling-assistant@leleat-on-github/src/common.js index 89611b2..9d24013 100644 --- a/tiling-assistant@leleat-on-github/src/common.js +++ b/tiling-assistant@leleat-on-github/src/common.js @@ -154,6 +154,13 @@ export class DynamicKeybindings { static FAVORITE_LAYOUT = 4; } +export const FocusHint = Object.freeze({ + DISABLED: 0, + ANIMATED_OUTLINE: 1, + ANIMATED_UPSCALE: 2, + STATIC_OUTLINE: 3 +}); + export class MoveModes { // Order comes from prefs static EDGE_TILING = 0; diff --git a/tiling-assistant@leleat-on-github/src/dependencies/shell.js b/tiling-assistant@leleat-on-github/src/dependencies/shell.js index cd0b07f..507806f 100644 --- a/tiling-assistant@leleat-on-github/src/dependencies/shell.js +++ b/tiling-assistant@leleat-on-github/src/dependencies/shell.js @@ -3,8 +3,10 @@ export { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js'; +export * as AppFavorites from 'resource:///org/gnome/shell/ui/appFavorites.js'; export * as AltTab from 'resource:///org/gnome/shell/ui/altTab.js'; export * as Main from 'resource:///org/gnome/shell/ui/main.js'; +export * as OsdWindow from 'resource:///org/gnome/shell/ui/osdWindow.js'; export * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js'; export * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; export * as SwitcherPopup from 'resource:///org/gnome/shell/ui/switcherPopup.js'; diff --git a/tiling-assistant@leleat-on-github/src/extension/focusHint.js b/tiling-assistant@leleat-on-github/src/extension/focusHint.js new file mode 100644 index 0000000..93bc863 --- /dev/null +++ b/tiling-assistant@leleat-on-github/src/extension/focusHint.js @@ -0,0 +1,876 @@ +import { + Clutter, + Gio, + GLib, + Meta, + Shell, + St +} from '../dependencies/gi.js'; +import { + AppFavorites, + Main, + OsdWindow, + SwitcherPopup +} from '../dependencies/shell.js'; +import * as AltTab from '../dependencies/unexported/altTab.js'; + +import { FocusHint, Settings } from '../common.js'; + +export default class FocusHintManager { + _hint = null; + + constructor(initialWindow) { + // On a fresh install no color is set for the hint yet. Use the bg color + // from the tile preview style by using a temporary widget. + if (Settings.getString('focus-hint-color') === '') { + const widget = new St.Widget({ style_class: 'tile-preview' }); + global.stage.add_child(widget); + + const color = widget.get_theme_node().get_background_color(); + const { red, green, blue } = color; + + Settings.setString('focus-hint-color', `rgb(${red},${green},${blue})`); + + widget.destroy(); + } + + this._settingsChangedId = Settings.changed( + 'focus-hint', + () => this._setHint(), + this + ); + this._setHint(); + + if (this._hint?.shouldIndicate(initialWindow)) + this._hint.indicate(initialWindow); + } + + destroy() { + Settings.disconnect(this._settingsChangedId); + + this._hint?.destroy(); + this._hint = null; + } + + _setHint() { + this._hint?.destroy(); + + switch (Settings.getInt('focus-hint')) { + case FocusHint.ANIMATED_OUTLINE: + this._hint = new AnimatedOutlineHint(); + break; + case FocusHint.ANIMATED_UPSCALE: + this._hint = new AnimatedUpscaleHint(); + break; + case FocusHint.STATIC_OUTLINE: + this._hint = new StaticOutlineHint(); + break; + default: + this._hint = null; + } + } +}; + +class Hint { + _actors = []; + + constructor() { + this._addIdleWatcher(); + this._overrideSwitchToApplication(); + this._overrideSwitcherPopupFinish(); + this._overrideWorkspaceAnimationSwitch(); + this._indicateOnWindowClose(); + } + + destroy() { + if (this._workspaceSwitchTimer) { + GLib.Source.remove(this._workspaceSwitchTimer); + this._workspaceSwitchTimer = 0; + } + + this.resetAnimation(); + + this._stopIndicatingOnWindowClose(); + this._restoreSwitcherPopupFinish(); + this._restoreSwitchToApplication(); + this._restoreWorkspaceAnimationSwitch(); + this._removeIdleWatcher(); + } + + indicate() { + throw new Error('`indicate` not implemented by Hint subclass!'); + } + + resetAnimation() { + this._actors.forEach(actor => actor.destroy()); + this._actors = []; + } + + _addIdleWatcher() { + const idleMonitor = global.backend.get_core_idle_monitor(); + const idleTime = 120 * 1000; + + this._activeWatchId && idleMonitor.remove_watch(this._activeWatchId); + this._activeWatchId = 0; + + this._idleWatchId && idleMonitor.remove_watch(this._idleWatchId); + this._idleWatchId = idleMonitor.add_idle_watch(idleTime, () => { + this._activeWatchId = idleMonitor.add_user_active_watch(() => { + this._activeWatchId = 0; + + const focus = global.display.focus_window; + + if (this.shouldIndicate(focus)) + this.indicate(focus); + }); + }); + } + + _allowedWindowType(type) { + return [ + Meta.WindowType.NORMAL, + Meta.WindowType.DIALOG, + Meta.WindowType.MODAL_DIALOG + ].includes(type); + } + + _indicateOnWindowClose() { + global.display.connectObject( + 'window-created', + (_, metaWindow) => this._onWindowCreated(metaWindow), + this + ); + + global + .get_window_actors() + .forEach(actor => this._onWindowCreated(actor.get_meta_window())); + } + + _onWindowCreated(window) { + if (!this._allowedWindowType(window.get_window_type())) + return; + + window.connectObject( + 'unmanaged', + () => { + window.disconnectObject(this); + + const focus = global.display.focus_window; + + if (focus && this.shouldIndicate(focus)) + this.indicate(focus); + else + this.resetAnimation(); + }, + this + ); + } + + _overrideSwitcherPopupFinish() { + this._originalSwitcherPopupFinish = + SwitcherPopup.SwitcherPopup.prototype._finish; + + const that = this; + + SwitcherPopup.SwitcherPopup.prototype._finish = function (timestamp) { + that._originalSwitcherPopupFinish.call(this, timestamp); + + const newFocus = global.display.focus_window; + + if (that.shouldIndicate(newFocus)) { + if (that._workspaceSwitchTimer) { + GLib.Source.remove(that._workspaceSwitchTimer); + that._workspaceSwitchTimer = 0; + } + + that.indicate(newFocus); + } else { + that.resetAnimation(); + } + }; + } + + _overrideSwitchToApplication() { + for (let i = 1; i < 10; i++) { + const key = `switch-to-application-${i}`; + + if (global.display.remove_keybinding(key)) { + const handler = (_, __, keybinding) => { + if (!Main.sessionMode.hasOverview) + return; + + const [, , , target] = keybinding.get_name().split('-'); + const apps = AppFavorites.getAppFavorites().getFavorites(); + const app = apps[target - 1]; + + if (app) { + const [newFocus] = app.get_windows(); + + Main.overview.hide(); + app.activate(); + + if (this.shouldIndicate(newFocus)) { + if (this._workspaceSwitchTimer) { + GLib.Source.remove(this._workspaceSwitchTimer); + this._workspaceSwitchTimer = 0; + } + + this.indicate(newFocus); + } else { + this.resetAnimation(); + } + } + }; + + global.display.add_keybinding( + key, + new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }), + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + handler + ); + } + } + } + + _overrideWorkspaceAnimationSwitch() { + this._originalWorkspaceAnimationSwitch = + Main.wm._workspaceAnimation.animateSwitch; + + const that = this; + + Main.wm._workspaceAnimation.animateSwitch = function ( + from, + to, + direction, + onComplete + ) { + that._originalWorkspaceAnimationSwitch.call( + this, + from, + to, + direction, + onComplete + ); + + // This is set if the focused window moved to the new workspace + // along with the workspace switch animation. E. g. when using + // Shift + Super + Alt + Arrow_Keys. + if (this.movingWindow) + return; + + // There are 2 different 'focus behaviors' during a workspace + // animation. 1: When the workspace switch is initiated by an app or + // by a window activation/focus (e. g. App Switcher). In this case + // global.display.focus_window gives the correct window for the + // focus hint. 2: When just switching workspaces (e. g. Super + Alt + // + Arrow Key), here the focus switches *after* the animation. So + // delay this code and let it be interrupted by the switcher popup + // or the switch-to-application focus hint. + that._workspaceSwitchTimer = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + 0, + () => { + that._workspaceSwitchTimer = 0; + + const newWorkspace = + global.workspace_manager.get_workspace_by_index(to); + const [newFocus] = AltTab.getWindows(newWorkspace); + + if (that.shouldIndicate(newFocus)) + that.indicate(newFocus); + else + that.resetAnimation(); + } + ); + }; + } + + shouldIndicate(window) { + if (!window || !window.get_compositor_private()) + return false; + + if (!this._allowedWindowType(window.get_window_type())) + return false; + + if ( + window.is_fullscreen() || + window.get_maximized() === Meta.MaximizeFlags.BOTH + ) + return false; + + return true; + } + + _removeIdleWatcher() { + const idleMonitor = global.backend.get_core_idle_monitor(); + + this._activeWatchId && idleMonitor.remove_watch(this._activeWatchId); + this._activeWatchId = 0; + + this._idleWatchId && idleMonitor.remove_watch(this._idleWatchId); + this._idleWatchId = 0; + } + + _restoreSwitcherPopupFinish() { + SwitcherPopup.SwitcherPopup.prototype._finish = + this._originalSwitcherPopupFinish; + + this._originalSwitcherPopupFinish = null; + } + + _restoreSwitchToApplication() { + for (let i = 1; i < 10; i++) { + const key = `switch-to-application-${i}`; + + if (global.display.remove_keybinding(key)) { + Main.wm.addKeybinding( + key, + new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }), + Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, + Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW, + Main.wm._switchToApplication.bind(Main.wm) + ); + } + } + } + + _restoreWorkspaceAnimationSwitch() { + Main.wm._workspaceAnimation.animateSwitch = + this._originalWorkspaceAnimationSwitch; + + this._originalWorkspaceAnimationSwitch = null; + } + + _stopIndicatingOnWindowClose() { + global.display.disconnectObject(this); + + global.get_window_actors().forEach(actor => { + actor.get_meta_window().disconnectObject(this); + }); + } +} + +class AnimatedOutlineHint extends Hint { + _color = ''; + _outlineSize = 0; + _outlineBorderRadius = 0; + + constructor() { + super(); + + this._color = Settings.getString('focus-hint-color'); + this._colorChangeId = Settings.changed('focus-hint-color', () => { + this._color = Settings.getString('focus-hint-color'); + }); + + this._outlineSize = Settings.getInt('focus-hint-outline-size'); + this._outlineSizeChangeId = Settings.changed('focus-hint-outline-size', () => { + this._outlineSize = Settings.getInt('focus-hint-outline-size'); + }); + + this._outlineBorderRadius = Settings.getInt('focus-hint-outline-border-radius'); + this._outlineBorderRadiusChangeId = Settings.changed('focus-hint-outline-border-radius', () => { + this._outlineBorderRadius = Settings.getInt('focus-hint-outline-border-radius'); + }); + } + + destroy() { + Settings.disconnect(this._colorChangeId); + Settings.disconnect(this._outlineSizeChangeId); + Settings.disconnect(this._outlineBorderRadiusChangeId); + + super.destroy(); + } + + indicate(window, workspaceSwitchAnimationDuration = 250) { + this.resetAnimation(); + + if (!this.shouldIndicate(window)) + return; + + const windowActor = window.get_compositor_private(); + const workspaceAnimationWindowClone = + findWindowCloneForWorkspaceAnimation( + windowActor, + !!Main.wm._workspaceAnimation._switchData + ); + const [monitorContainer, workspaceContainer] = createContainers( + window, + workspaceAnimationWindowClone, + workspaceSwitchAnimationDuration + ); + + this._actors.push(monitorContainer); + + const customClone = createWindowClone( + windowActor, + monitorContainer + ); + const outline = this._createOutline(window, monitorContainer); + const { + x: windowFrameX, + y: windowFrameY, + width: windowFrameWidth, + height: windowFrameHeight + } = window.get_frame_rect(); + + workspaceContainer.add_child(outline); + workspaceContainer.add_child(customClone); + + workspaceAnimationWindowClone?.hide(); + + outline.ease({ + x: windowFrameX - monitorContainer.x - this._outlineSize, + y: windowFrameY - monitorContainer.y - this._outlineSize, + width: windowFrameWidth + 2 * this._outlineSize, + height: windowFrameHeight + 2 * this._outlineSize, + delay: workspaceAnimationWindowClone + ? (175 / 250) * workspaceSwitchAnimationDuration + : 0, + duration: 150, + mode: Clutter.AnimationMode.EASE_OUT_BACK, + onComplete: () => { + outline.ease({ + x: windowFrameX - monitorContainer.x, + y: windowFrameY - monitorContainer.y, + width: windowFrameWidth, + height: windowFrameHeight, + duration: 100, + mode: Clutter.AnimationMode.EASE_IN, + onComplete: () => this.resetAnimation() + }); + } + }); + } + + _createOutline(window, monitorContainer) { + const { x, y, width, height } = window.get_frame_rect(); + const outline = new St.Widget({ + style: this._getCssStyle(), + x: x - monitorContainer.x, + y: y - monitorContainer.y, + width, + height + }); + + return outline; + } + + _getCssStyle() { + return ` + background-color: ${this._color}; + border-radius: ${this._outlineBorderRadius}px; + `; + } +} + +class AnimatedUpscaleHint extends Hint { + _scaleAmount = 10; + + indicate(window, workspaceSwitchAnimationDuration = 250) { + this.resetAnimation(); + + if (!this.shouldIndicate(window)) + return; + + const windowActor = window.get_compositor_private(); + const workspaceAnimationWindowClone = + findWindowCloneForWorkspaceAnimation( + windowActor, + !!Main.wm._workspaceAnimation._switchData + ); + const [monitorContainer, workspaceContainer] = createContainers( + window, + workspaceAnimationWindowClone, + workspaceSwitchAnimationDuration + ); + + this._actors.push(monitorContainer); + + const customClone = createWindowClone( + windowActor, + monitorContainer + ); + const { x, y, width, height } = customClone; + + workspaceContainer.add_child(customClone); + + workspaceAnimationWindowClone?.hide(); + windowActor.set_opacity(0); // Hide to prevent double shadows. + + customClone.ease({ + x: x - this._scaleAmount, + y: y - this._scaleAmount, + width: width + 2 * this._scaleAmount, + height: height + 2 * this._scaleAmount, + delay: workspaceAnimationWindowClone + ? (175 / 250) * workspaceSwitchAnimationDuration + : 0, + duration: 100, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + customClone.ease({ + x, + y, + width, + height, + duration: 150, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => this.resetAnimation() + }); + } + }); + } + + resetAnimation() { + global.get_window_actors().forEach(a => a.set_opacity(255)); + super.resetAnimation(); + } +} + +class StaticOutlineHint extends AnimatedOutlineHint { + _outline = null; + _window = null; + + constructor() { + super(); + + this._outline = new St.Widget({ style: this._getCssStyle() }); + global.window_group.add_child(this._outline); + + // Originally, only `notify::focus-window` was used but that had issues + // with popups on Wayland. `restacked` by itself seems to be kinda + // spotty on Wayland for the first window that is opened on a workspace. + global.display.connectObject( + 'restacked', + () => this._updateOutline(), + 'notify::focus-window', + () => this._updateOutline(), + this + ); + + this._updateOutline(); + + Settings.getGioObject().connectObject( + 'changed::focus-hint-color', + () => this._updateOutline(), + 'changed::focus-hint-outline-size', + () => this._updateOutline(), + 'changed::focus-hint-outline-border-radius', + () => this._updateOutline(), + this + ); + } + + destroy() { + Settings.getGioObject().disconnectObject(this); + + this._cancelGeometryUpdate(); + + this._outline.destroy(); + this._outline = null; + + this._window?.disconnectObject(this); + this._window = null; + + global.display.disconnectObject(this); + + GLib.Source.remove(this._resetTimer); + + super.destroy(); + } + + /** + * This is really only used for the indication when changing workspaces... + * + * @param {Window} window - + * @param {number} workspaceSwitchAnimationDuration - + */ + indicate(window, workspaceSwitchAnimationDuration = 250) { + this.resetAnimation(); + + if (!this.shouldIndicate(window)) + return; + + const animatingWorkspaceSwitch = + !!Main.wm._workspaceAnimation._switchData; + + // Only need to use an animation to indicate the focus when switching + // workspaces. In the other cases, there is the static `this._outline`. + if (!animatingWorkspaceSwitch) + return; + + const windowActor = window.get_compositor_private(); + const workspaceAnimationWindowClone = + findWindowCloneForWorkspaceAnimation( + windowActor, + animatingWorkspaceSwitch + ); + const [monitorContainer, workspaceContainer] = createContainers( + window, + workspaceAnimationWindowClone, + workspaceSwitchAnimationDuration + ); + + this._actors.push(monitorContainer); + + const customClone = createWindowClone( + windowActor, + monitorContainer + ); + const outline = this._createOutline(window, monitorContainer); + + workspaceContainer.add_child(outline); + workspaceContainer.add_child(customClone); + + workspaceAnimationWindowClone?.hide(); + + this._resetTimer = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + workspaceSwitchAnimationDuration, + () => { + this.resetAnimation(); + this._resetTimer = 0; + } + ); + } + + _cancelGeometryUpdate() { + if (this._laterID) { + global.compositor.get_laters().remove(this._laterID); + this._laterID = 0; + } + } + + _createOutline(window, monitorContainer) { + const { x, y, width, height } = window.get_frame_rect(); + const outline = new St.Widget({ + style: this._getCssStyle(), + x: x - monitorContainer.x - this._outlineSize, + y: y - monitorContainer.y - this._outlineSize, + width: width + 2 * this._outlineSize, + height: height + 2 * this._outlineSize + }); + + return outline; + } + + _queueGeometryUpdate() { + const windowActor = this._window.get_compositor_private(); + + if (!windowActor) + return; + + this._laterID = global.compositor + .get_laters() + .add(Meta.LaterType.BEFORE_REDRAW, () => { + this._updateGeometry(); + this._outline.set_style(this._getCssStyle()); + this._outline.show(); + + global.window_group.set_child_below_sibling( + this._outline, + windowActor + ); + + this._laterID = 0; + return GLib.SOURCE_REMOVE; + }); + } + + _updateOutline() { + this._cancelGeometryUpdate(); + + this._window?.disconnectObject(this); + + const window = global.display.focus_window; + + if (!window || !this._allowedWindowType(window.get_window_type())) { + this._outline.hide(); + return; + } + + this._window = window; + this._window.connectObject( + 'position-changed', + () => this._updateGeometry(), + 'size-changed', + () => this._updateGeometry(), + this + ); + + if ( + this._window.is_fullscreen() || + this._window.get_maximized() === Meta.MaximizeFlags.BOTH + ) + this._outline.hide(); + else + this._queueGeometryUpdate(); + } + + _updateGeometry() { + const { x, y, width, height } = this._window.get_frame_rect(); + + this._outline.set({ + x: x - this._outlineSize, + y: y - this._outlineSize, + width: width + this._outlineSize * 2, + height: height + this._outlineSize * 2 + }); + } +} + +/** + * Gets the absolute position of a Clutter.AcotActor. + * `Clutter.Actor.get_transformed_position` doesn't work as I expected it + * + * @param {Clutter.Actor} actor + * + * @returns {{x: number, y: number}} + */ +function getAbsPos(actor) { + const pos = { x: actor.x, y: actor.y }; + let parent = actor.get_parent(); + + while (parent) { + pos.x += parent.x; + pos.y += parent.y; + + parent = parent.get_parent(); + } + + return pos; +} + +/** + * Creates containers to put clones of the monitor/workspace into to create a + * workspaceSwitch with the focus hint + * + * @param {Meta.Window} window + * @param {Clutter.Clone} workspaceAnimationWindowClone + * @param {number} workspaceSwitchAnimationDuration + * + * @returns {[Clutter.Actor, Clutter.Actor]} a monitor and a workspace containers + * for Clutter.Clones that are laid over the actual actors + */ +function createContainers( + window, + workspaceAnimationWindowClone, + workspaceSwitchAnimationDuration +) { + const monitorNr = window.get_monitor(); + const monitorRect = global.display.get_monitor_geometry(monitorNr); + let startingPos; + + if (workspaceAnimationWindowClone) { + const actorAbsPos = getAbsPos(window.get_compositor_private(), monitorNr); + const cloneAbsPos = getAbsPos(workspaceAnimationWindowClone, monitorNr); + + startingPos = { + x: monitorRect.x + cloneAbsPos.x - actorAbsPos.x, + y: monitorRect.y + cloneAbsPos.y - actorAbsPos.y + }; + } else { + startingPos = { x: 0, y: 0 }; + } + + const monitorContainer = new Clutter.Actor({ + clip_to_allocation: true, + x: monitorRect.x, + y: monitorRect.y, + width: monitorRect.width, + height: monitorRect.height + }); + + // Allow tiled window to be animate above the panel. Also, When changing + // workspaces we want to put everything above the animating clones. + if (workspaceAnimationWindowClone) { + const osdWindow = Main.uiGroup + .get_children() + .find(child => child instanceof OsdWindow.OsdWindow); + + if (osdWindow) + Main.uiGroup.insert_child_below(monitorContainer, osdWindow); + else + Main.uiGroup.add_child(monitorContainer); + } else { + global.window_group.add_child(monitorContainer); + } + + const workspaceContainer = new Clutter.Actor({ + x: startingPos.x, + y: startingPos.y, + width: monitorContainer.width, + height: monitorContainer.height + }); + + monitorContainer.add_child(workspaceContainer); + + workspaceContainer.ease({ + x: 0, + y: 0, + duration: workspaceSwitchAnimationDuration, + mode: Clutter.AnimationMode.EASE_OUT_CUBIC + }); + + return [monitorContainer, workspaceContainer]; +} + +/** + * Creates a clone of a window actor for the custom workspaceSwitch animation + * with the focus hint + * + * @param {Meta.WindowActor} windowActor + * @param {Clutter.Actor} container + * + * @returns {Clutter.Clone} + */ +function createWindowClone(windowActor, container) { + const monitor = windowActor.get_meta_window().get_monitor(); + const { x, y } = getAbsPos(windowActor, monitor); + + const windowClone = new Clutter.Clone({ + source: windowActor, + x: x - container.x, + y: y - container.y, + width: windowActor.width, + height: windowActor.height + }); + + return windowClone; +} + +/** + * Finds the window clone of a window during the native workspaceSwitch animation + * + * @param {Meta.WindowActor} windowActor + * @param {boolean} animatingWorkspaceSwitch + * + * @returns {Clutter.Clone|null} the clone. It may be `null` if the focus is on + * the secondary monitor with 'WS only on primary display' + */ +function findWindowCloneForWorkspaceAnimation( + windowActor, + animatingWorkspaceSwitch +) { + if (!animatingWorkspaceSwitch) + return null; + + const switchData = Main.wm._workspaceAnimation._switchData; + let clone = null; + + switchData.monitors.find(monitorGroup => { + return monitorGroup._workspaceGroups.find(workspaceGroup => { + return workspaceGroup._windowRecords.find(record => { + const foundClone = record.windowActor === windowActor; + + if (foundClone) + ({ clone } = record); + + return foundClone; + }); + }); + }); + + return clone; +} diff --git a/tiling-assistant@leleat-on-github/src/ui/prefs.ui b/tiling-assistant@leleat-on-github/src/ui/prefs.ui index 0e5daa5..d91275f 100644 --- a/tiling-assistant@leleat-on-github/src/ui/prefs.ui +++ b/tiling-assistant@leleat-on-github/src/ui/prefs.ui @@ -280,6 +280,98 @@ + + + + + Focus Hint + Generally, the focused window will be indicated if it may be ambiguous. That means, if a window is focused with the pointer, the focus won't be indicated unless you use the Static Outline + + + + Disabled + Do <i>not</i> indicate the focused window + 1 + disabled-focus-hint-button + + + + + + + + Outline Animation + When the focus changes, temporarily outline the focused window. Maximized and fullscreen windows are exempt from this + animated-outline-focus-hint-button + + + disabled-focus-hint-button + + + + + + + Upscale Animation + When the focus changes, temporarily scale the focused window up. Maximized and fullscreen windows are exempt from this + animated-upscale-focus-hint-button + + + disabled-focus-hint-button + + + + + + + Static Outline + Indicate the focused window with a static outline unless it's maximized or in fullscreen. + static-outline-focus-hint-button + + + disabled-focus-hint-button + + + + + + + + + + + + Outline Color + focus_hint_color_button + + + center + + + + + + + Outline Size + focus_hint_outline_size + + + 50 + 1 + 8 + + + + + + + From 2dc8d58ee5ffc42244bb5a612e5908f888fa3abd Mon Sep 17 00:00:00 2001 From: Leleat Date: Thu, 9 May 2024 19:27:29 +0200 Subject: [PATCH 2/2] ActiveWindowHint: Remove active window hint The newly added focus hint is a better feature. So remove the active window hint in favor of it. --- .../extension.js | 4 - tiling-assistant@leleat-on-github/prefs.js | 8 - ...ll.extensions.tiling-assistant.gschema.xml | 13 - .../src/extension/activeWindowHint.js | 340 ------------------ .../src/ui/prefs.ui | 90 ----- 5 files changed, 455 deletions(-) delete mode 100644 tiling-assistant@leleat-on-github/src/extension/activeWindowHint.js diff --git a/tiling-assistant@leleat-on-github/extension.js b/tiling-assistant@leleat-on-github/extension.js index 816181c..ba5fe28 100644 --- a/tiling-assistant@leleat-on-github/extension.js +++ b/tiling-assistant@leleat-on-github/extension.js @@ -23,7 +23,6 @@ import MoveHandler from './src/extension/moveHandler.js'; import ResizeHandler from './src/extension/resizeHandler.js'; import KeybindingHandler from './src/extension/keybindingHandler.js'; import LayoutsManager from './src/extension/layoutsManager.js'; -import ActiveWindowHint from './src/extension/activeWindowHint.js'; import AltTabOverride from './src/extension/altTab.js'; import FocusHintManager from './src/extension/focusHint.js'; import { Rect } from './src/extension/utility.js'; @@ -161,7 +160,6 @@ export default class TilingAssistantExtension extends Extension { this._resizeHandler = new ResizeHandler(); this._keybindingHandler = new KeybindingHandler(); this._layoutsManager = new LayoutsManager(); - this._activeWindowHintHandler = new ActiveWindowHint(); this._focusHintManager = new FocusHintManager(); this._altTabOverride = new AltTabOverride(); @@ -241,8 +239,6 @@ export default class TilingAssistantExtension extends Extension { this._keybindingHandler = null; this._layoutsManager.destroy(); this._layoutsManager = null; - this._activeWindowHintHandler.destroy(); - this._activeWindowHintHandler = null; this._focusHintManager.destroy(); this._focusHintManager = null; diff --git a/tiling-assistant@leleat-on-github/prefs.js b/tiling-assistant@leleat-on-github/prefs.js index aefa00f..c335efd 100644 --- a/tiling-assistant@leleat-on-github/prefs.js +++ b/tiling-assistant@leleat-on-github/prefs.js @@ -168,14 +168,6 @@ export default class Prefs extends ExtensionPreferences { 'dynamic_keybinding_favorite_layout_row' ] }, - { - key: 'active-window-hint', - rowNames: [ - 'active_window_hint_disabled_row', - 'active_window_hint_minimal_row', - 'active_window_hint_always_row' - ] - }, { key: 'focus-hint', rowNames: [ diff --git a/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml b/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml index 50c651e..b1877b9 100644 --- a/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml +++ b/tiling-assistant@leleat-on-github/schemas/org.gnome.shell.extensions.tiling-assistant.gschema.xml @@ -32,19 +32,6 @@ 8 - - - 1 - - - '' - - - 5 - - - 0 - 0 diff --git a/tiling-assistant@leleat-on-github/src/extension/activeWindowHint.js b/tiling-assistant@leleat-on-github/src/extension/activeWindowHint.js deleted file mode 100644 index 5b4a004..0000000 --- a/tiling-assistant@leleat-on-github/src/extension/activeWindowHint.js +++ /dev/null @@ -1,340 +0,0 @@ -import { Clutter, GObject, Meta, St } from '../dependencies/gi.js'; -import { Main } from '../dependencies/shell.js'; - -import { Settings } from '../common.js'; -import { TilingWindowManager as Twm } from './tilingWindowManager.js'; - -export default class ActiveWindowHintHandler { - constructor() { - // On a fresh install no color is set for the hint yet. Use the bg color - // from the tile preview style by using a temporary widget. - if (Settings.getString('active-window-hint-color') === '') { - const widget = new St.Widget({ style_class: 'tile-preview' }); - global.stage.add_child(widget); - - const color = widget.get_theme_node().get_background_color(); - const { red, green, blue } = color; - - Settings.setString('active-window-hint-color', `rgb(${red},${green},${blue})`); - - widget.destroy(); - } - - this._hint = null; - this._settingsId = 0; - - this._setupHint(); - - this._settingsId = Settings.changed('active-window-hint', - () => this._setupHint()); - } - - destroy() { - Settings.disconnect(this._settingsId); - this._hint?.destroy(); - this._hint = null; - } - - _setupHint() { - switch (Settings.getInt('active-window-hint')) { - case 0: // Disabled - this._hint?.destroy(); - this._hint = null; - break; - case 1: // Minimal - this._hint?.destroy(); - this._hint = new MinimalHint(); - break; - case 2: // Always - this._hint?.destroy(); - this._hint = new AlwaysHint(); - } - } -} - -const Hint = GObject.registerClass( -class ActiveWindowHint extends St.Widget { - _init() { - super._init(); - - this._color = Settings.getString('active-window-hint-color'); - this._borderSize = Settings.getInt('active-window-hint-border-size'); - this._innerBorderSize = Settings.getInt('active-window-hint-inner-border-size'); // 'Inner border' to cover rounded corners - this._settingsIds = []; - - this._settingsIds.push(Settings.changed('active-window-hint-color', () => { - this._color = Settings.getString('active-window-hint-color'); - })); - this._settingsIds.push(Settings.changed('active-window-hint-border-size', () => { - this._borderSize = Settings.getInt('active-window-hint-border-size'); - })); - this._settingsIds.push(Settings.changed('active-window-hint-inner-border-size', () => { - this._innerBorderSize = Settings.getInt('active-window-hint-inner-border-size'); - })); - - global.window_group.add_child(this); - } - - destroy() { - this._settingsIds.forEach(id => Settings.disconnect(id)); - super.destroy(); - } -}); - -const MinimalHint = GObject.registerClass( -class MinimalActiveWindowHint extends Hint { - _init() { - super._init(); - - this._windowClone = null; - - this._updateStyle(); - - this._settingsIds.push(Settings.changed('active-window-hint-color', () => { - this._updateStyle(); - })); - - global.workspace_manager.connectObject('workspace-switched', - () => this._onWsSwitched(), this); - } - - destroy() { - this._reset(); - super.destroy(); - } - - _reset() { - if (this._laterId) { - global.compositor.get_laters().remove(this._laterId); - delete this._laterId; - } - this._windowClone?.destroy(); - this._windowClone = null; - this.hide(); - } - - _updateStyle() { - this.set_style(`background-color: ${this._color};`); - } - - _onWsSwitched() { - // Reset in case multiple workspaces are switched at once. - this._reset(); - - // If we are in the overview, it's likely the user actively chose - // a window to focus. So the hint is unnecessary. - if (Main.overview.visible) - return; - - const window = global.display.focus_window; - if (!window) - return; - - // Maximized or fullscreen windows don't require a hint since they - // cover the entire screen. - if (window.is_fullscreen() || Twm.isMaximized(window)) - return; - - // Now figure out if the focused window is easily identifiable by - // checking (in stacking order) if all other windows are being - // overlapped by higher windows. If a window is not overlapped, the - // focused window is ambiguous. - const windows = Twm.getWindows(); - const overlapping = windows.splice(windows.indexOf(window), 1); - - const notOverlappedWindowExists = windows.some(w => { - if (!overlapping.some(o => o.get_frame_rect().overlap(w.get_frame_rect()))) - return true; - - overlapping.push(w); - return false; - }); - - if (notOverlappedWindowExists) - this._giveHint(window); - } - - _giveHint(window) { - this._scaleClone(window); - this._rippleFade(window); - } - - _scaleClone(window) { - const actor = window.get_compositor_private(); - if (!actor) - return; - - const { x, y, width, height } = actor; - const scaleAmount = 15; - this._windowClone = new Clutter.Clone({ - source: actor, - x: x - scaleAmount, - y: y - scaleAmount, - width: width + 2 * scaleAmount, - height: height + 2 * scaleAmount - }); - global.window_group.insert_child_above(this._windowClone, actor); - - this._windowClone.ease({ - x, y, width, height, - delay: 250, - duration: 250, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => { - // May already have been destroyed by a reset - this._windowClone?.destroy(); - this._windowClone = null; - } - }); - } - - _rippleFade(window) { - const actor = window.get_compositor_private(); - if (!actor) - return; - - if (!this._laterId) { - this._laterId = global.compositor.get_laters().add( - Meta.LaterType.BEFORE_REDRAW, - () => { - global.window_group.set_child_below_sibling(this, actor); - delete this._laterId; - return false; - } - ); - } - - const { x, y, width, height } = window.get_frame_rect(); - this.set({ x, y, width, height }); - - this.set_opacity(255); - this.show(); - - const rippleSize = 30; - this.ease({ - x: x - rippleSize, - y: y - rippleSize, - width: width + 2 * rippleSize, - height: height + 2 * rippleSize, - opacity: 0, - delay: 250, - duration: 350, - mode: Clutter.AnimationMode.EASE_OUT_QUAD, - onComplete: () => this.hide() - }); - } -}); - -// TODO a solid bg color looks better than a border when launching an app since -// the border will appear before the window is fully visible. However there was -// an issue with global.window_group.set_child_below_sibling not putting the hint -// below the window for some reason. laters-add solved it but I don't know -// why. So as to not potentially cover the entire window's content use the border -// style until I figure out if laters-add is the proper solution... -const AlwaysHint = GObject.registerClass( -class AlwaysActiveWindowHint extends Hint { - _init() { - super._init(); - - this._window = null; - - this._updateGeometry(); - this._updateStyle(); - - global.display.connectObject('notify::focus-window', - () => this._updateGeometry(), this); - - this._settingsIds.push(Settings.changed('active-window-hint-color', () => { - this._updateStyle(); - this._updateGeometry(); - })); - this._settingsIds.push(Settings.changed('active-window-hint-border-size', () => { - this._updateStyle(); - this._updateGeometry(); - })); - this._settingsIds.push(Settings.changed('active-window-hint-inner-border-size', () => { - this._updateStyle(); - this._updateGeometry(); - })); - } - - destroy() { - this._reset(); - super.destroy(); - } - - vfunc_hide() { - this._cancelShowLater(); - super.vfunc_hide(); - } - - _reset() { - this._cancelShowLater(); - - this._window?.disconnectObject(this); - this._window = null; - } - - _cancelShowLater() { - if (!this._showLater) - return; - - - global.compositor.get_laters().remove(this._showLater); - delete this._showLater; - } - - _updateGeometry() { - this._reset(); - - const window = global.display.focus_window; - const allowTypes = [Meta.WindowType.NORMAL, Meta.WindowType.DIALOG, Meta.WindowType.MODAL_DIALOG]; - if (!window || !allowTypes.includes(window.get_window_type())) { - this.hide(); - return; - } - - this._window = window; - this._window.connectObject( - 'position-changed', - () => this._updateGeometry(), - this - ); - this._window.connectObject( - 'size-changed', - () => this._updateGeometry(), - this - ); - - // Don't show hint on maximzed/fullscreen windows - if (window.is_fullscreen() || Twm.isMaximized(window)) { - this.hide(); - return; - } - - const { x, y, width, height } = window.get_frame_rect(); - this.set({ x, y, width, height }); - - const actor = window.get_compositor_private(); - - if (!actor || this._showLater) - return; - - this._showLater = global.compositor.get_laters().add( - Meta.LaterType.IDLE, - () => { - global.window_group.set_child_below_sibling(this, actor); - this.show(); - delete this._showLater; - return false; - } - ); - } - - _updateStyle() { - this.set_style(` - border: ${this._innerBorderSize}px solid ${this._color}; - outline: ${this._borderSize}px solid ${this._color}; - `); - } -}); diff --git a/tiling-assistant@leleat-on-github/src/ui/prefs.ui b/tiling-assistant@leleat-on-github/src/ui/prefs.ui index d91275f..940e112 100644 --- a/tiling-assistant@leleat-on-github/src/ui/prefs.ui +++ b/tiling-assistant@leleat-on-github/src/ui/prefs.ui @@ -372,96 +372,6 @@ - - - - - Active Window Hint - - - Disabled - Don't indicate the focused window - active_window_hint_disabled_button - - - - - - - - Minimal - Temporarily indicate the focused window when switching to a workspace with multiple non-overlapping windows - active_window_hint_minimal_button - - - active_window_hint_disabled_button - - - - - - - Always - Always indicate the focused window unless it's maximized or in fullscreen. There are issues on Wayland with GTK4 popups - active_window_hint_always_button - - - active_window_hint_disabled_button - - - - - - - - - - - - Hint Color - The color of the frame indicating the focused window - active_window_hint_color_button - - - center - - - - - - - Border Size - The border size of the frame indicating the focused window for the always active hint - active_window_hint_border_size - - - 50 - 1 - 8 - - - - - - - Inner Border Size - The border for the always active hint reaching inside the window frame. This is meant to cover rounded corners. However, GTK4 popups on Wayland will put the window behind the hint for whatever reason... - active_window_hint_inner_border_size - - - 50 - 1 - 8 - - - - - - -