From aa7aba7f4818e58df57d3342bf36dee933a3a4d0 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Tue, 8 Apr 2025 02:26:08 +0100 Subject: [PATCH 01/23] some hacking --- src/events/eventManager.js | 46 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/events/eventManager.js b/src/events/eventManager.js index 9fb2096..9b95b68 100644 --- a/src/events/eventManager.js +++ b/src/events/eventManager.js @@ -5,6 +5,7 @@ class EventManager { this.evenIds = {}; // event name -> {target, eventID} this.monitoredWindows = {}; // windowID -> {eventName -> eventID} this.unlockHandlerId = null; + this.positionCallbacks = new Set(); // Set of callbacks for position changes } attachGlobalEventOnce(eventName, target, callback) { @@ -23,6 +24,7 @@ class EventManager { event.target.disconnect(event.id); }); this.detachUnlockScreenEvent(); + this.positionCallbacks.clear(); this.evenIds = {}; } @@ -61,6 +63,50 @@ class EventManager { this.unlockHandlerId = null; } } + + attachWindowPositionCallback(window, callback) { + const eventName = 'position-changed'; + this.attachWindowEventOnce(eventName, window, callback); + this.positionCallbacks.add(callback); + return callback; + } + + isPositionCallbackAttached(callback) { + return this.positionCallbacks.has(callback); + } + + attachGlobalPositionCallback(target, callback) { + const eventName = 'position-changed'; + this.attachGlobalEventOnce(eventName, target, callback); + this.positionCallbacks.add(callback); + return callback; + } + + detachGlobalPositionCallback(callback) { + if (this.evenIds['position-changed'] && this.positionCallbacks.has(callback)) { + const event = this.evenIds['position-changed']; + event.target.disconnect(event.id); + delete this.evenIds['position-changed']; + this.positionCallbacks.delete(callback); + } + } + + detachWindowPositionCallback(window, callback) { + const windowId = window.get_id(); + const eventName = 'position-changed'; + + if (this.monitoredWindows[windowId] && + this.monitoredWindows[windowId][eventName] !== undefined) { + window.disconnect(this.monitoredWindows[windowId][eventName]); + delete this.monitoredWindows[windowId][eventName]; + + // If no more events for this window, clean up the entry + if (Object.keys(this.monitoredWindows[windowId]).length === 0) + delete this.monitoredWindows[windowId]; + } + + this.positionCallbacks.delete(callback); + } } export default EventManager; From 666b306bdfc484baf7ca1d72a328ece0dfd03f52 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 17:10:00 +0100 Subject: [PATCH 02/23] Bump version to 25 in metadata and package files --- package.json | 2 +- src/metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index b3b0ea7..6bd93bb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "gnome-gradienttopbar", "title": "Gradient Top Bar", "description": "Give the top bar a customizable gradient. Choose direction and colors, and optionally use a different style or keep the theme when windows are maximized.", - "version": "24", + "version": "25", "engines": { "gnome": ">=49" }, diff --git a/src/metadata.json b/src/metadata.json index 845ea5c..13069ce 100644 --- a/src/metadata.json +++ b/src/metadata.json @@ -9,7 +9,7 @@ }, "shell-version": ["49", "50", "51"], "url": "https://github.com/petar-v/gradienttopbar", - "version": 24, + "version": 25, "gettext-domain": "org.pshow.gradienttopbar", "settings-schema": "org.gnome.shell.extensions.org.pshow.gradienttopbar", "hasPrefs": true From 26fe39000664e168b26bce4a1aac8e8221b33a9b Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 17:10:23 +0100 Subject: [PATCH 03/23] Add style-trigger option for alternate panel behavior --- src/schemas/gschemas.compiled | Bin 844 -> 944 bytes ...ensions.org.pshow.gradienttopbar.gschema.xml | 13 +++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/schemas/gschemas.compiled b/src/schemas/gschemas.compiled index 1d9ebc699b00d38fa4d82e432d681080549295af..8992bc271655cc35f8e4bdd834621610789d14c3 100644 GIT binary patch delta 479 zcmYLFF-yZx5WYm4ilWqlgU~^wNF$^jEhwEF1zl8d5K?`lk=7SpVzCu%{Rfq7;_4zD zItq1g6&)PZ!9~Qyv6E~3u4sGk@qPEbyW`$H8J&1*Wj4@=VlzY$;%0;pYt#mJE;g4N zW1J&ULps3kj8Py47{LAM*#tND2T_%pD82ICzX?P4rN8;{E`R@`eNAAw~`h6*sZvAv2PVc$*#Img-v}YF|_w& zSt>ug)E3g&yh+n(%GkcyzYo4WxEp e1;7yv&4|ZFo|3#+P@`y^GJd9JADL#qlzssmLT-ov delta 365 zcmdnMeuiy=jbsEP0|Tol1H*qHaARfw0v`qsAnh|T)S8nW#0LV#iF3v4*SH7;umU+C z9Rdtx3>iRrG7y_rKmG<12eH|J;tPQENg%$abpI+y9K@CZioXETYD^3a2WsoMLAHU| ztUz%CAe{lkS?qC3LFz$lA)t5zkiG@PP}f=h2XR2oy91;@0P(}d(@7u=AT~$?2hiCd zFXSdxWaeg8rKad6=jY@X6;GbPq|FXtGEDAbQd8Ar5L9B&NG#Sws7x5y&w>Un?+0i2*|h$YcXz7LXVa OfSfvc2V)pW3 + + + + + + 'maximized' + Condition that triggers the alternate panel style + Use the alternate panel behavior for maximized windows or windows near the panel. + 'keep-gradient' - Behavior when windows are maximized - Determines what style to apply to the top bar when there is a maximized window: keep the normal gradient, use the default theme, or apply a custom style. + Behavior when the style trigger is active + Determines what style to apply to the top bar when the selected window condition is met: keep the normal gradient, use the default theme, or apply a custom style. ["rgba(0, 0, 0, 1)", "rgba(0, 0, 0, 0)"] From 398bf7baa50820b2b8c6d882237d84bcb1313547 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 17:11:10 +0100 Subject: [PATCH 04/23] Add style-trigger configuration for enhanced panel behavior --- src/config.js | 10 ++++++++++ src/constants.js | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/src/config.js b/src/config.js index cf3bd5c..bab6410 100644 --- a/src/config.js +++ b/src/config.js @@ -8,6 +8,7 @@ const SETTINGS_GSCHEMA_PATH = `/${SETTINGS_GSCHEMA.replaceAll('.', '/')}/`; // Settings keys export const MAXIMIZED_BEHAVIOR = 'maximized-behavior'; +export const STYLE_TRIGGER = 'style-trigger'; export const GRADIENT_DIRECTION = 'gradient-direction'; export const MAXIMIZED_GRADIENT_DIRECTION = 'maximized-gradient-direction'; export const COLORS = 'colors'; @@ -21,6 +22,7 @@ export const MAXIMIZED_COLORS = 'maximized-colors'; */ export const getConfig = settings => { const maximizedBehavior = settings.get_string(MAXIMIZED_BEHAVIOR); + const styleTrigger = settings.get_string(STYLE_TRIGGER); const colors = settings.get_value(COLORS).deep_unpack(); const gradientDirection = settings.get_string(GRADIENT_DIRECTION); const maximizedColors = settings.get_value(MAXIMIZED_COLORS).deep_unpack(); @@ -28,6 +30,7 @@ export const getConfig = settings => { return { maximizedBehavior, + styleTrigger, gradientDirection, colors: { start: colors[0], @@ -61,6 +64,12 @@ export const setMaximizedBehavior = (settings, value) => { settings.set_string(MAXIMIZED_BEHAVIOR, value); }; +export const getStyleTrigger = settings => settings.get_string(STYLE_TRIGGER); + +export const setStyleTrigger = (settings, value) => { + settings.set_string(STYLE_TRIGGER, value); +}; + /** * Gets the gradient direction setting * @@ -153,6 +162,7 @@ export const attachSettingsListeners = (settings, listener) => { return [ GRADIENT_DIRECTION, MAXIMIZED_BEHAVIOR, + STYLE_TRIGGER, COLORS, MAXIMIZED_COLORS, MAXIMIZED_GRADIENT_DIRECTION diff --git a/src/constants.js b/src/constants.js index 4e99d52..4fde623 100644 --- a/src/constants.js +++ b/src/constants.js @@ -9,6 +9,11 @@ export const MAXIMIZED_BEHAVIOR = { APPLY_STYLE: 'apply-style' }; +export const STYLE_TRIGGER = { + MAXIMIZED: 'maximized', + PROXIMITY: 'proximity' +}; + // Gradient directions export const GRADIENT_DIRECTION = { VERTICAL: 'vertical', From ec2cd0f60d15817abbb6002be694dc556b89fb7d Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 17:39:16 +0100 Subject: [PATCH 05/23] oops, bumped again lol --- package.json | 2 +- src/metadata.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6bd93bb..aa29a55 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "gnome-gradienttopbar", "title": "Gradient Top Bar", "description": "Give the top bar a customizable gradient. Choose direction and colors, and optionally use a different style or keep the theme when windows are maximized.", - "version": "25", + "version": "26", "engines": { "gnome": ">=49" }, diff --git a/src/metadata.json b/src/metadata.json index 13069ce..d8a5829 100644 --- a/src/metadata.json +++ b/src/metadata.json @@ -9,7 +9,7 @@ }, "shell-version": ["49", "50", "51"], "url": "https://github.com/petar-v/gradienttopbar", - "version": 25, + "version": 26, "gettext-domain": "org.pshow.gradienttopbar", "settings-schema": "org.gnome.shell.extensions.org.pshow.gradienttopbar", "hasPrefs": true From 02f911dded5508d5be08aa47151fac0c04b9340a Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 17:41:43 +0100 Subject: [PATCH 06/23] Refactor window event handling to support alternate panel styling --- src/events/windowEvents.js | 122 ++++++++++++++++++++----------------- src/gradient.js | 4 +- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/src/events/windowEvents.js b/src/events/windowEvents.js index aa3c453..9a5be2d 100644 --- a/src/events/windowEvents.js +++ b/src/events/windowEvents.js @@ -1,12 +1,14 @@ import Meta from 'gi://Meta'; -import { overview } from 'resource:///org/gnome/shell/ui/main.js'; +import { layoutManager, overview, panel } from 'resource:///org/gnome/shell/ui/main.js'; import EventManager from './eventManager.js'; import { areSameState } from './states.js'; +import { STYLE_TRIGGER } from '../constants.js'; const { VERTICAL, BOTH } = Meta.MaximizeFlags; const SIZE_CHANGE_EVENT = 'size-changed'; +const POSITION_CHANGE_EVENT = 'position-changed'; const WORKSPACE_CHANGE_EVENT = 'workspace-switched'; const WINDOW_CREATE_EVENT = 'window-created'; const WINDOW_DESTROY_EVENT = 'destroy'; @@ -14,6 +16,7 @@ const WINDOW_MINIMIZED_EVENT = 'minimize'; const WINDOW_RAISED_EVENT = 'unminimize'; const WINDOW_EXIT_MONITOR = 'window-left-monitor'; +const MONITORS_CHANGED_EVENT = 'monitors-changed'; // const WINDOW_REMOVED_FROM_WORKSPACE = 'window-removed'; // const WINDOW_ADDED_TO_WORKSPACE = 'window-added'; const WINDOW_WORKSPACE_CHANGED = 'workspace-changed'; @@ -46,9 +49,17 @@ const isMaximized = window => { return [BOTH, VERTICAL].includes(maximizeFlags); }; +const isVisibleApplicationWindow = window => + !isDesktopIconsNG(window) && + window.is_on_primary_monitor() && + window.showing_on_its_workspace() && + !window.is_hidden() && + window.get_window_type() !== Meta.WindowType.DESKTOP && + !window.skip_taskbar; + /** * Manages window events and tracks window state changes - * Used to detect maximized windows and trigger appropriate UI updates + * Used to detect windows that trigger alternate panel styling */ export default class WindowEvents { /** @@ -58,10 +69,11 @@ export default class WindowEvents { * @param {Meta.WindowManager} windowManager - The GNOME Shell window manager * @param {Meta.WorkspaceManager} workspaceManager - The GNOME Shell workspace manager */ - constructor(display, windowManager, workspaceManager) { + constructor(display, windowManager, workspaceManager, styleTrigger) { this.display = display; this.windowManager = windowManager; this.workspaceManager = workspaceManager; + this.styleTrigger = styleTrigger; this.eventManager = new EventManager(); @@ -69,7 +81,7 @@ export default class WindowEvents { this.workspace = null; this.inOverview = false; - this.maximizedWindows = new Set(); + this.triggerWindows = new Set(); this.lastState = null; } @@ -85,11 +97,11 @@ export default class WindowEvents { /** * Gets the current window state * - * @returns {Object} An object containing the current state (maximizedWindows, currentWorkspace, inOverview) + * @returns {Object} The current trigger, workspace, and overview state */ getCurrentState() { return { - maximizedWindows: this.maximizedWindows, + triggerWindows: this.triggerWindows, currentWorkspace: this.workspace, inOverview: this.inOverview }; @@ -112,11 +124,9 @@ export default class WindowEvents { * Forces a state update by re-evaluating maximized windows and emitting a state change * Used when the state needs to be refreshed regardless of detected changes */ - forceStateUpdate() { - // Re-evaluate maximized windows - this.maximizedWindows = this.getMaximizedWindowIds(); - // Force state change emission - this.emitStateChange(true); + updateState(force = false) { + this.triggerWindows = this.getTriggerWindowIds(); + this.emitStateChange(force); } /** @@ -129,14 +139,7 @@ export default class WindowEvents { * * @param {Meta.Window} window - The window that changed size */ - const onWindowSizeChange = window => { - if (isMaximized(window)) - this.maximizedWindows.add(window.get_id()); - else - this.maximizedWindows.delete(window.get_id()); - - this.emitStateChange(); - }; + const onWindowGeometryChange = () => this.updateState(); /** * Handles workspace change events @@ -145,7 +148,7 @@ export default class WindowEvents { */ const onWorkspaceChanged = workspaceManager => { this.workspace = workspaceManager.get_active_workspace(); - this.emitStateChange(); + this.updateState(); }; /** @@ -156,9 +159,8 @@ export default class WindowEvents { */ const onWindowDestroy = (_, windowActor) => { const window = windowActor.get_meta_window(); - this.maximizedWindows.delete(window.get_id()); + this.triggerWindows.delete(window.get_id()); this.eventManager.disconnectWindowEvents(window); - this.emitStateChange(); }; @@ -171,21 +173,21 @@ export default class WindowEvents { if (isDesktopIconsNG(window)) return; - if (window.can_maximize()) { - this.eventManager.attachWindowEventOnce( - SIZE_CHANGE_EVENT, - window, - onWindowSizeChange - ); - } - - if (isMaximized(window)) - this.maximizedWindows.add(window.get_id()); + this.eventManager.attachWindowEventOnce( + SIZE_CHANGE_EVENT, + window, + onWindowGeometryChange + ); + this.eventManager.attachWindowEventOnce( + POSITION_CHANGE_EVENT, + window, + onWindowGeometryChange + ); this.eventManager.attachWindowEventOnce( WINDOW_WORKSPACE_CHANGED, window, - () => this.forceStateUpdate() + () => this.updateState() ); }; @@ -196,8 +198,7 @@ export default class WindowEvents { * @param {Meta.WindowActor} windowActor - The window actor being minimized */ const onWindowMinimize = (_, windowActor) => { - const windowId = windowActor.get_meta_window().get_id(); - this.maximizedWindows.delete(windowId); + this.triggerWindows.delete(windowActor.get_meta_window().get_id()); this.emitStateChange(); }; @@ -207,11 +208,8 @@ export default class WindowEvents { * @param {*} _ - Unused parameter * @param {Meta.WindowActor} windowActor - The window actor being raised */ - const onWindowRaise = (_, windowActor) => { - const window = windowActor.get_meta_window(); - if (isMaximized(window)) - this.maximizedWindows.add(window.get_id()); - this.emitStateChange(); + const onWindowRaise = () => { + this.updateState(); }; this.workspace = this.workspaceManager.get_active_workspace(); @@ -224,7 +222,7 @@ export default class WindowEvents { this.display, (_, window) => { attachWindowEvents(window); - this.emitStateChange(); + this.updateState(); } ); this.eventManager.attachGlobalEventOnce( @@ -251,7 +249,12 @@ export default class WindowEvents { this.eventManager.attachGlobalEventOnce( WINDOW_EXIT_MONITOR, this.display, - () => this.emitStateChange(true) + () => this.updateState() + ); + this.eventManager.attachGlobalEventOnce( + MONITORS_CHANGED_EVENT, + layoutManager, + () => this.updateState(true) ); // FIXME: the workspace changes so this needs to be attached to every workspace as it is created/deleted @@ -268,18 +271,14 @@ export default class WindowEvents { this.emitStateChange(); }); - // TODO: instead of on size change, listen for https://gjs-docs.gnome.org/meta13~13/meta.window#property-maximized_horizontally or vertically - // to make it work with tiling, I would need to figure out the position in case it is maximized horizontally but on top. - // if it's maximized vertically, then it's likely on either side. In that case I want to make the bar opaque. this.display.list_all_windows().forEach(attachWindowEvents); - // FIXME: find a way to do that only for the windows on the primary monitor's current workspace - this.maximizedWindows = this.getMaximizedWindowIds(); + this.triggerWindows = this.getTriggerWindowIds(); // Add screen lock/unlock event handling this.eventManager.attachUnlockScreenEvent(() => { // Force state re-evaluation after screen unlock - this.forceStateUpdate(); + this.updateState(true); }); this.emitStateChange(); @@ -296,22 +295,35 @@ export default class WindowEvents { }); this.workspace = null; - this.maximizedWindows = new Set(); + this.triggerWindows = new Set(); this.inOverview = null; this.lastState = null; } /** - * Gets the IDs of all currently maximized windows + * Gets the IDs of windows that currently trigger alternate styling * - * @returns {Set} A set containing the IDs of all maximized windows + * @returns {Set} A set containing the triggering window IDs */ - getMaximizedWindowIds() { + getTriggerWindowIds() { + const { primaryMonitor } = layoutManager; + if (!primaryMonitor || !this.workspace) + return new Set(); + + const panelBottom = primaryMonitor.y + panel.get_height() + 5; + return new Set( - this.display - .list_all_windows() - .filter(isMaximized) + this.workspace + .list_windows() + .filter(isVisibleApplicationWindow) + .filter(window => this.styleTrigger === STYLE_TRIGGER.PROXIMITY + ? window.get_frame_rect().y <= panelBottom + : isMaximized(window)) .map(window => window.get_id()) ); } + + setStyleTrigger(styleTrigger) { + this.styleTrigger = styleTrigger; + } } diff --git a/src/gradient.js b/src/gradient.js index 999b57c..3471c7d 100644 --- a/src/gradient.js +++ b/src/gradient.js @@ -52,11 +52,11 @@ export const unloadGradientStylesheet = extensionPath => { theme.unload_stylesheet(getUserStylesheet(extensionPath)); }; -export const toggleGradient = (enabled, isMaximized = false) => { +export const toggleGradient = (enabled, useAlternateStyle = false) => { // Remove all styles first panel.remove_style_class_name(GRADIENT_CLASS); panel.remove_style_class_name(MAXIMIZED_GRADIENT_CLASS); if (enabled) - panel.add_style_class_name(isMaximized ? MAXIMIZED_GRADIENT_CLASS : GRADIENT_CLASS); + panel.add_style_class_name(useAlternateStyle ? MAXIMIZED_GRADIENT_CLASS : GRADIENT_CLASS); }; From 74430da25d05da85ce5f220e82cb155c9b858b0b Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 17:46:06 +0100 Subject: [PATCH 07/23] Remove position callback handling from event manager --- src/events/eventManager.js | 46 -------------------------------------- 1 file changed, 46 deletions(-) diff --git a/src/events/eventManager.js b/src/events/eventManager.js index 9b95b68..9fb2096 100644 --- a/src/events/eventManager.js +++ b/src/events/eventManager.js @@ -5,7 +5,6 @@ class EventManager { this.evenIds = {}; // event name -> {target, eventID} this.monitoredWindows = {}; // windowID -> {eventName -> eventID} this.unlockHandlerId = null; - this.positionCallbacks = new Set(); // Set of callbacks for position changes } attachGlobalEventOnce(eventName, target, callback) { @@ -24,7 +23,6 @@ class EventManager { event.target.disconnect(event.id); }); this.detachUnlockScreenEvent(); - this.positionCallbacks.clear(); this.evenIds = {}; } @@ -63,50 +61,6 @@ class EventManager { this.unlockHandlerId = null; } } - - attachWindowPositionCallback(window, callback) { - const eventName = 'position-changed'; - this.attachWindowEventOnce(eventName, window, callback); - this.positionCallbacks.add(callback); - return callback; - } - - isPositionCallbackAttached(callback) { - return this.positionCallbacks.has(callback); - } - - attachGlobalPositionCallback(target, callback) { - const eventName = 'position-changed'; - this.attachGlobalEventOnce(eventName, target, callback); - this.positionCallbacks.add(callback); - return callback; - } - - detachGlobalPositionCallback(callback) { - if (this.evenIds['position-changed'] && this.positionCallbacks.has(callback)) { - const event = this.evenIds['position-changed']; - event.target.disconnect(event.id); - delete this.evenIds['position-changed']; - this.positionCallbacks.delete(callback); - } - } - - detachWindowPositionCallback(window, callback) { - const windowId = window.get_id(); - const eventName = 'position-changed'; - - if (this.monitoredWindows[windowId] && - this.monitoredWindows[windowId][eventName] !== undefined) { - window.disconnect(this.monitoredWindows[windowId][eventName]); - delete this.monitoredWindows[windowId][eventName]; - - // If no more events for this window, clean up the entry - if (Object.keys(this.monitoredWindows[windowId]).length === 0) - delete this.monitoredWindows[windowId]; - } - - this.positionCallbacks.delete(callback); - } } export default EventManager; From c39d46c4990e02e69469b817d3f58ffcdf53fc11 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 20:22:46 +0100 Subject: [PATCH 08/23] Fix workspace comparison logic in state equality check --- src/events/states.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/events/states.js b/src/events/states.js index 135eeaf..fab9ae5 100644 --- a/src/events/states.js +++ b/src/events/states.js @@ -15,10 +15,10 @@ export const areSameState = (state1, state2) => { return false; if (state1.inOverview !== state2.inOverview) return false; - if ([state1.workspace, state2.workspace].includes(undefined)) + if ([state1.currentWorkspace, state2.currentWorkspace].includes(undefined)) return false; - if (state1.workspace.index() !== state2.workspace.index()) + if (state1.currentWorkspace.index() !== state2.currentWorkspace.index()) return false; - return eqSet(state1.maximizedWindows, state2.maximizedWindows); + return eqSet(state1.triggerWindows, state2.triggerWindows); }; From 59ebfe39bec9b6f9e3126c911574f6726c9f4f52 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 20:23:01 +0100 Subject: [PATCH 09/23] Add style trigger handling to gradient toggle logic --- src/extension.js | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/src/extension.js b/src/extension.js index 0426331..4efb473 100755 --- a/src/extension.js +++ b/src/extension.js @@ -3,6 +3,7 @@ import { applyGradientStyle, toggleGradient, unloadGradientStylesheet } from './ import { getConfig, getMaximizedBehavior, + getStyleTrigger, attachSettingsListeners, detachSettingsListeners } from './config.js'; @@ -10,22 +11,13 @@ import { MAXIMIZED_BEHAVIOR } from './constants.js'; import WindowEvents from './events/windowEvents.js'; -const isOnPrimaryMonitor = win => { - if (win?.is_on_primary_monitor) - return win.is_on_primary_monitor(); - - if (win?.get_monitor && global.display?.get_primary_monitor) - return win.get_monitor() === global.display.get_primary_monitor(); - - return true; -}; - export default class GradientTopBar extends Extension { onSettingsChanged(settings) { const config = getConfig(settings); applyGradientStyle(config, this.path); const maximizedBehavior = getMaximizedBehavior(settings); + this.windowEvents.setStyleTrigger(getStyleTrigger(settings)); // If set to keep-gradient, disable window events to save resources @@ -38,43 +30,37 @@ export default class GradientTopBar extends Extension { } this.windowEvents.enable(); - this.windowEvents.forceStateUpdate(); + this.windowEvents.updateState(true); } - toggleGradient(enabled, hasMaximizedWindows = false) { + toggleGradient(enabled, useAlternateStyle = false) { // this checks if the gradient state has changed // so we don't add classes multiple times. - if (this.isEffectApplied === enabled && this.hasMaximizedWindows === hasMaximizedWindows) + if (this.isEffectApplied === enabled && this.isAlternateStyleApplied === useAlternateStyle) return; - toggleGradient(enabled, hasMaximizedWindows); + toggleGradient(enabled, useAlternateStyle); this.isEffectApplied = enabled; - this.hasMaximizedWindows = hasMaximizedWindows; + this.isAlternateStyleApplied = useAlternateStyle; } setWindowStateCallback() { this.windowEvents.setStateChangeCallback( - ({ maximizedWindows, currentWorkspace, inOverview }) => { + ({ triggerWindows, inOverview }) => { if (inOverview) { this.toggleGradient(true, false); return; } - const workspaceDisplayMaximizedWindows = currentWorkspace - .list_windows() - // filter windows only on the primary monitor - .filter(isOnPrimaryMonitor) - // filter maximized windows on the primary monitor - .filter(window => maximizedWindows.has(window.get_id())); - const hasMaximizedWindows = workspaceDisplayMaximizedWindows.length > 0; + const hasTriggeredWindows = triggerWindows.size > 0; const maximizedBehavior = getMaximizedBehavior(this._settings); - if (!hasMaximizedWindows) { - // No maximized windows, apply normal gradient + if (!hasTriggeredWindows) { + // No triggering windows, apply normal gradient this.toggleGradient(true, false); return; } - // Handle different behaviors for maximized windows + // Handle the configured behavior for triggering windows switch (maximizedBehavior) { case MAXIMIZED_BEHAVIOR.KEEP_GRADIENT: // Keep the normal gradient @@ -85,7 +71,7 @@ export default class GradientTopBar extends Extension { this.toggleGradient(false, false); break; case MAXIMIZED_BEHAVIOR.APPLY_STYLE: - // Apply the maximized gradient style + // Apply the alternate gradient style this.toggleGradient(true, true); break; } @@ -95,7 +81,7 @@ export default class GradientTopBar extends Extension { enable() { this.isEffectApplied = false; - this.hasMaximizedWindows = false; + this.isAlternateStyleApplied = false; this._settings = this.getSettings(); this._settingsHandlerIds = attachSettingsListeners( this._settings, @@ -105,7 +91,8 @@ export default class GradientTopBar extends Extension { this.windowEvents = new WindowEvents( global.display, global.window_manager, - global.get_workspace_manager() + global.get_workspace_manager(), + getStyleTrigger(this._settings) ); this.setWindowStateCallback(); From e000530e11a8cbf3974fc71c38e0cb0c51a7c4f4 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 20:23:08 +0100 Subject: [PATCH 10/23] Add style trigger selection to behavior settings --- src/settings/behavioursPage.js | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/src/settings/behavioursPage.js b/src/settings/behavioursPage.js index 31d95d6..46bb72b 100644 --- a/src/settings/behavioursPage.js +++ b/src/settings/behavioursPage.js @@ -10,10 +10,12 @@ import { attachSettingsListeners, detachSettingsListeners, getMaximizedBehavior, - setMaximizedBehavior + setMaximizedBehavior, + getStyleTrigger, + setStyleTrigger } from '../config.js'; -import { MAXIMIZED_BEHAVIOR } from '../constants.js'; +import { MAXIMIZED_BEHAVIOR, STYLE_TRIGGER } from '../constants.js'; const MaximizedBehavior = GObject.registerClass( { @@ -55,6 +57,19 @@ class Behavior extends Adw.PreferencesPage { title: gettext('Behavior') }); + const triggerModel = new Gio.ListStore({ item_type: MaximizedBehavior }); + [ + new MaximizedBehavior(gettext('Maximized windows'), STYLE_TRIGGER.MAXIMIZED), + new MaximizedBehavior(gettext('Proximity'), STYLE_TRIGGER.PROXIMITY) + ].forEach(trigger => triggerModel.append(trigger)); + + const triggerRow = new Adw.ComboRow({ + title: gettext('Style trigger'), + subtitle: gettext('Choose when to apply the alternate top bar behavior'), + model: triggerModel, + expression: new Gtk.PropertyExpression(MaximizedBehavior, null, 'name') + }); + // Create model for maximized behavior dropdown const maximizedBehaviorModel = new Gio.ListStore({ item_type: MaximizedBehavior @@ -68,9 +83,9 @@ class Behavior extends Adw.PreferencesPage { // Create dropdown for maximized behavior const maximizedBehaviorRow = new Adw.ComboRow({ - title: gettext('When windows are maximized'), + title: gettext('When triggered'), subtitle: gettext( - 'Choose what style to apply to the top bar when there is a maximized window' + 'Choose what style to apply to the top bar' ), model: maximizedBehaviorModel, expression: new Gtk.PropertyExpression(MaximizedBehavior, null, 'name') @@ -89,19 +104,25 @@ class Behavior extends Adw.PreferencesPage { // Set initial value setMaximizedBehaviorOnRow(maximizedBehaviorRow, getMaximizedBehavior(this._settings)); + setMaximizedBehaviorOnRow(triggerRow, getStyleTrigger(this._settings)); // Connect to changes maximizedBehaviorRow.connect('notify::selected', () => { const { selectedItem } = maximizedBehaviorRow; setMaximizedBehavior(this._settings, selectedItem.value); }); + triggerRow.connect('notify::selected', () => { + setStyleTrigger(this._settings, triggerRow.selectedItem.value); + }); + behaviorGroup.add(triggerRow); behaviorGroup.add(maximizedBehaviorRow); this.add(behaviorGroup); const onSettingsChanged = s => { const config = getConfig(s); setMaximizedBehaviorOnRow(maximizedBehaviorRow, config.maximizedBehavior); + setMaximizedBehaviorOnRow(triggerRow, config.styleTrigger); }; const settingsHandlerIds = attachSettingsListeners(settings, onSettingsChanged); window.connect('close-request', () => { From e8b1a478acef6e85506d08bac67d8043fb024e11 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 20:23:14 +0100 Subject: [PATCH 11/23] Update gradient settings for alternate style in maximized windows --- README.md | 3 ++- src/settings/appearancePage.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a231d0c..8d5b4f2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ Give GNOME’s top bar a customizable gradient. - Choose start and end colors of the gradient - Vertical or horizontal orientation -- Separate style when a window is maximized: keep the gradient, keep the original theme, or apply a custom gradient +- Trigger an alternate style for maximized windows or windows near the panel +- Keep the gradient, restore the original theme, or apply a custom gradient when triggered - Simple preferences dialog diff --git a/src/settings/appearancePage.js b/src/settings/appearancePage.js index 57f7412..1e8a58b 100644 --- a/src/settings/appearancePage.js +++ b/src/settings/appearancePage.js @@ -136,15 +136,15 @@ class Appearance extends Adw.PreferencesPage { this.add(gradientGroup); - // Add a new group for maximized window gradient settings + // Add a group for the alternate gradient settings const maximizedGradientGroup = new Adw.PreferencesGroup({ - title: gettext('Maximized Window Gradient Appearance') + title: gettext('Alternate Gradient Appearance') }); // Create direction dropdown for maximized windows const maximizedDirectionRow = new Adw.ComboRow({ - title: gettext('Maximized Gradient Direction'), - subtitle: gettext('The orientation of the gradient when a window is maximized.'), + title: gettext('Alternate Gradient Direction'), + subtitle: gettext('The orientation when the style trigger is active.'), model: gradientDirectionModel, expression: new Gtk.PropertyExpression(GradientDirection, null, 'name') }); @@ -159,8 +159,8 @@ class Appearance extends Adw.PreferencesPage { // Create color pickers for maximized windows const maximizedStartColorChooserRow = createColorChooserRow( - gettext('Maximized Gradient Start Colour'), - gettext('The start color of the gradient when a window is maximized'), + gettext('Alternate Gradient Start Colour'), + gettext('The start color of the alternate gradient'), maximizedColors.start, rgba => { maximizedColors.start = rgba; @@ -170,8 +170,8 @@ class Appearance extends Adw.PreferencesPage { maximizedGradientGroup.add(maximizedStartColorChooserRow); const maximizedEndColorChooserRow = createColorChooserRow( - gettext('Maximized Gradient End Colour'), - gettext('The end color of the gradient when a window is maximized'), + gettext('Alternate Gradient End Colour'), + gettext('The end color of the alternate gradient'), maximizedColors.end, rgba => { maximizedColors.end = rgba; From 3208506dd9f34bd243cf0f57cf7be53e5e2219b9 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 21:47:15 +0100 Subject: [PATCH 12/23] Add MaximizedWindows and ProximityWindows classes for window management --- src/events/behaviours/maximizedWindows.js | 23 ++++++++++++++++ src/events/behaviours/proximityWindows.js | 32 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 src/events/behaviours/maximizedWindows.js create mode 100644 src/events/behaviours/proximityWindows.js diff --git a/src/events/behaviours/maximizedWindows.js b/src/events/behaviours/maximizedWindows.js new file mode 100644 index 0000000..0544f91 --- /dev/null +++ b/src/events/behaviours/maximizedWindows.js @@ -0,0 +1,23 @@ +import Meta from 'gi://Meta'; + +const { VERTICAL, BOTH } = Meta.MaximizeFlags; + +// Detects windows that occupy the full monitor or are vertically maximized. +// This behaviour deliberately avoids layout and position-change dependencies. +export default class MaximizedWindows { + matches(window) { + if (window.is_monitor_sized() || window.is_screen_sized()) + return true; + + return [BOTH, VERTICAL].includes(window.get_maximize_flags()); + } + + // Maximization changes window size; moving an unmaximized window is irrelevant. + attachWindowEvents(eventManager, window, updateState) { + eventManager.attachWindowEventOnce('size-changed', window, updateState); + eventManager.attachWindowEventOnce('workspace-changed', window, updateState); + } + + // Maximized detection requires no behaviour-specific global events. + attachGlobalEvents() {} +} diff --git a/src/events/behaviours/proximityWindows.js b/src/events/behaviours/proximityWindows.js new file mode 100644 index 0000000..a5c1abf --- /dev/null +++ b/src/events/behaviours/proximityWindows.js @@ -0,0 +1,32 @@ +import { layoutManager, panel } from 'resource:///org/gnome/shell/ui/main.js'; + +const DEFAULT_PROXIMITY_DISTANCE = 5; + +// Keeps all panel-layout dependencies inside the proximity behaviour. +export default class ProximityWindows { + matches(window) { + const { primaryMonitor } = layoutManager; + if (!primaryMonitor) + return false; + + // Include a small tolerance below the panel to avoid an exact-edge requirement. + const panelBottom = primaryMonitor.y + panel.get_height() + DEFAULT_PROXIMITY_DISTANCE; + return window.get_frame_rect().y <= panelBottom; + } + + // Proximity can change when a window moves, resizes, or changes workspace. + attachWindowEvents(eventManager, window, updateState) { + eventManager.attachWindowEventOnce('size-changed', window, updateState); + eventManager.attachWindowEventOnce('position-changed', window, updateState); + eventManager.attachWindowEventOnce('workspace-changed', window, updateState); + } + + // Recalculate the boundary whenever GNOME changes the monitor layout. + attachGlobalEvents(eventManager, updateState) { + eventManager.attachGlobalEventOnce( + 'monitors-changed', + layoutManager, + () => updateState(true) + ); + } +} From fded703e83e67421045ee3e8b6a69830aa5b105d Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 21:47:54 +0100 Subject: [PATCH 13/23] Refactor window event handling to use behavior classes for style triggers --- src/events/windowEvents.js | 92 +++++++++++++------------------------- src/extension.js | 14 ++++-- 2 files changed, 42 insertions(+), 64 deletions(-) diff --git a/src/events/windowEvents.js b/src/events/windowEvents.js index 9a5be2d..a4c7ffc 100644 --- a/src/events/windowEvents.js +++ b/src/events/windowEvents.js @@ -1,14 +1,9 @@ import Meta from 'gi://Meta'; -import { layoutManager, overview, panel } from 'resource:///org/gnome/shell/ui/main.js'; +import { overview } from 'resource:///org/gnome/shell/ui/main.js'; import EventManager from './eventManager.js'; import { areSameState } from './states.js'; -import { STYLE_TRIGGER } from '../constants.js'; -const { VERTICAL, BOTH } = Meta.MaximizeFlags; - -const SIZE_CHANGE_EVENT = 'size-changed'; -const POSITION_CHANGE_EVENT = 'position-changed'; const WORKSPACE_CHANGE_EVENT = 'workspace-switched'; const WINDOW_CREATE_EVENT = 'window-created'; const WINDOW_DESTROY_EVENT = 'destroy'; @@ -16,39 +11,14 @@ const WINDOW_MINIMIZED_EVENT = 'minimize'; const WINDOW_RAISED_EVENT = 'unminimize'; const WINDOW_EXIT_MONITOR = 'window-left-monitor'; -const MONITORS_CHANGED_EVENT = 'monitors-changed'; // const WINDOW_REMOVED_FROM_WORKSPACE = 'window-removed'; // const WINDOW_ADDED_TO_WORKSPACE = 'window-added'; -const WINDOW_WORKSPACE_CHANGED = 'workspace-changed'; - const OVERVIEW_SHOWING = 'showing'; const OVERVIEW_HIDING = 'hiding'; // FIXME: this causes the overview to close on login const isDesktopIconsNG = window => window.customJS_ding !== undefined; // this is to ignore "Desktop Icons NG"'s window hacks -/** - * Determines if a window is maximized or full-screen - * - * @param {Meta.Window} window - The window to check - * @returns {boolean} True if the window is maximized or full-screen - */ -const isMaximized = window => { - // Ignore Desktop Icons NG windows - if (isDesktopIconsNG(window)) - return false; - - - // Check if the window is full-screen - if (window.is_monitor_sized() || window.is_screen_sized()) - return true; - - - // Check if the window is maximized (either vertically or both dimensions) - const maximizeFlags = window.get_maximize_flags(); - return [BOTH, VERTICAL].includes(maximizeFlags); -}; - const isVisibleApplicationWindow = window => !isDesktopIconsNG(window) && window.is_on_primary_monitor() && @@ -69,13 +39,14 @@ export default class WindowEvents { * @param {Meta.WindowManager} windowManager - The GNOME Shell window manager * @param {Meta.WorkspaceManager} workspaceManager - The GNOME Shell workspace manager */ - constructor(display, windowManager, workspaceManager, styleTrigger) { + constructor(display, windowManager, workspaceManager, behaviour) { this.display = display; this.windowManager = windowManager; this.workspaceManager = workspaceManager; - this.styleTrigger = styleTrigger; + this.behaviour = behaviour; this.eventManager = new EventManager(); + this.enabled = false; this.stateChangeCallback = () => {}; @@ -134,12 +105,17 @@ export default class WindowEvents { * Sets up all event listeners and initializes the current state */ enable() { + if (this.enabled) + return; + + this.enabled = true; + /** * Handles window size change events * * @param {Meta.Window} window - The window that changed size */ - const onWindowGeometryChange = () => this.updateState(); + const onWindowChange = () => this.updateState(); /** * Handles workspace change events @@ -173,21 +149,10 @@ export default class WindowEvents { if (isDesktopIconsNG(window)) return; - this.eventManager.attachWindowEventOnce( - SIZE_CHANGE_EVENT, + this.behaviour.attachWindowEvents( + this.eventManager, window, - onWindowGeometryChange - ); - this.eventManager.attachWindowEventOnce( - POSITION_CHANGE_EVENT, - window, - onWindowGeometryChange - ); - - this.eventManager.attachWindowEventOnce( - WINDOW_WORKSPACE_CHANGED, - window, - () => this.updateState() + onWindowChange ); }; @@ -251,10 +216,9 @@ export default class WindowEvents { this.display, () => this.updateState() ); - this.eventManager.attachGlobalEventOnce( - MONITORS_CHANGED_EVENT, - layoutManager, - () => this.updateState(true) + this.behaviour.attachGlobalEvents( + this.eventManager, + force => this.updateState(force) ); // FIXME: the workspace changes so this needs to be attached to every workspace as it is created/deleted @@ -298,6 +262,7 @@ export default class WindowEvents { this.triggerWindows = new Set(); this.inOverview = null; this.lastState = null; + this.enabled = false; } /** @@ -306,24 +271,29 @@ export default class WindowEvents { * @returns {Set} A set containing the triggering window IDs */ getTriggerWindowIds() { - const { primaryMonitor } = layoutManager; - if (!primaryMonitor || !this.workspace) + if (!this.workspace) return new Set(); - const panelBottom = primaryMonitor.y + panel.get_height() + 5; - return new Set( this.workspace .list_windows() .filter(isVisibleApplicationWindow) - .filter(window => this.styleTrigger === STYLE_TRIGGER.PROXIMITY - ? window.get_frame_rect().y <= panelBottom - : isMaximized(window)) + .filter(window => this.behaviour.matches(window)) .map(window => window.get_id()) ); } - setStyleTrigger(styleTrigger) { - this.styleTrigger = styleTrigger; + setBehaviour(behaviour) { + if (this.behaviour.constructor === behaviour.constructor) + return; + + const wasEnabled = this.enabled; + if (wasEnabled) + this.disable(); + + this.behaviour = behaviour; + + if (wasEnabled) + this.enable(); } } diff --git a/src/extension.js b/src/extension.js index 4efb473..ed67a9f 100755 --- a/src/extension.js +++ b/src/extension.js @@ -7,9 +7,11 @@ import { attachSettingsListeners, detachSettingsListeners } from './config.js'; -import { MAXIMIZED_BEHAVIOR } from './constants.js'; +import { MAXIMIZED_BEHAVIOR, STYLE_TRIGGER } from './constants.js'; import WindowEvents from './events/windowEvents.js'; +import MaximizedWindows from './events/behaviours/maximizedWindows.js'; +import ProximityWindows from './events/behaviours/proximityWindows.js'; export default class GradientTopBar extends Extension { onSettingsChanged(settings) { @@ -17,7 +19,11 @@ export default class GradientTopBar extends Extension { applyGradientStyle(config, this.path); const maximizedBehavior = getMaximizedBehavior(settings); - this.windowEvents.setStyleTrigger(getStyleTrigger(settings)); + this.windowEvents.setBehaviour( + getStyleTrigger(settings) === STYLE_TRIGGER.PROXIMITY + ? new ProximityWindows() + : new MaximizedWindows() + ); // If set to keep-gradient, disable window events to save resources @@ -92,7 +98,9 @@ export default class GradientTopBar extends Extension { global.display, global.window_manager, global.get_workspace_manager(), - getStyleTrigger(this._settings) + getStyleTrigger(this._settings) === STYLE_TRIGGER.PROXIMITY + ? new ProximityWindows() + : new MaximizedWindows() ); this.setWindowStateCallback(); From bc15cd9bdb4d0e1ecabcdba398b51f971f9ba003 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 21:53:09 +0100 Subject: [PATCH 14/23] Add proximity distance setting for alternate panel style trigger --- src/schemas/gschemas.compiled | Bin 944 -> 1028 bytes ...nsions.org.pshow.gradienttopbar.gschema.xml | 6 ++++++ 2 files changed, 6 insertions(+) diff --git a/src/schemas/gschemas.compiled b/src/schemas/gschemas.compiled index 8992bc271655cc35f8e4bdd834621610789d14c3..4e09c5747cffd90c9b4e21f1f6f046b43dc7d99b 100644 GIT binary patch delta 472 zcmYLGJxIe~6nsgOwjzQQx`;?|kY=ceiy}_q;NajODh@)L1pA3?N*ao_Xc5H0K`L2v za1z&SSLgkmDJ8i}sE!$xss%vL) z$KUjWN_3>>(*IL=uV7b|A0RHu=&O_Ez-M4sLrf*KOOD3_xl^{VTt2cYTgxhCS=Mi= aM2GrFv=Up)aP9c`&adcP7MkM;fb|3R=ww#_ delta 376 zcmZqS*uXx)MyiC7fq_+&f#E+8tYT&W0w0DoKzal7#4xLR4v+v402MJX0WnAc2(th& zD-g2*@qyYpZjf%UNepES4M4>^fcRnK=_HUk5L*Z+egR1H0S(Dwk6Q{72eCoo3P8FF zh}XCX1%SjsYyqJ71SW=hhTT98%y}R-$a#B!^c5h!rgZ-*NCSv11Jv*VNb3PzXI}mI z8%P|)W&?^_0O>>^4&Qw%A0!T9^8&>SfHcU|*zTXKF*eo-+{R|Uv&1^{`$OAG)2 diff --git a/src/schemas/org.gnome.shell.extensions.org.pshow.gradienttopbar.gschema.xml b/src/schemas/org.gnome.shell.extensions.org.pshow.gradienttopbar.gschema.xml index ecfb5a1..acbc895 100644 --- a/src/schemas/org.gnome.shell.extensions.org.pshow.gradienttopbar.gschema.xml +++ b/src/schemas/org.gnome.shell.extensions.org.pshow.gradienttopbar.gschema.xml @@ -19,6 +19,12 @@ Condition that triggers the alternate panel style Use the alternate panel behavior for maximized windows or windows near the panel. + + + 5 + Proximity distance + Distance in pixels below the panel at which a window triggers the alternate style. + 'keep-gradient' Behavior when the style trigger is active From 3dcbbc090a15fbb2c30f4ff54c593d063594923e Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 21:53:18 +0100 Subject: [PATCH 15/23] Add proximity distance setting and related functions to config --- src/config.js | 10 ++++++++++ src/constants.js | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/config.js b/src/config.js index bab6410..06d75d7 100644 --- a/src/config.js +++ b/src/config.js @@ -9,6 +9,7 @@ const SETTINGS_GSCHEMA_PATH = `/${SETTINGS_GSCHEMA.replaceAll('.', '/')}/`; // Settings keys export const MAXIMIZED_BEHAVIOR = 'maximized-behavior'; export const STYLE_TRIGGER = 'style-trigger'; +export const PROXIMITY_DISTANCE = 'proximity-distance'; export const GRADIENT_DIRECTION = 'gradient-direction'; export const MAXIMIZED_GRADIENT_DIRECTION = 'maximized-gradient-direction'; export const COLORS = 'colors'; @@ -23,6 +24,7 @@ export const MAXIMIZED_COLORS = 'maximized-colors'; export const getConfig = settings => { const maximizedBehavior = settings.get_string(MAXIMIZED_BEHAVIOR); const styleTrigger = settings.get_string(STYLE_TRIGGER); + const proximityDistance = settings.get_int(PROXIMITY_DISTANCE); const colors = settings.get_value(COLORS).deep_unpack(); const gradientDirection = settings.get_string(GRADIENT_DIRECTION); const maximizedColors = settings.get_value(MAXIMIZED_COLORS).deep_unpack(); @@ -31,6 +33,7 @@ export const getConfig = settings => { return { maximizedBehavior, styleTrigger, + proximityDistance, gradientDirection, colors: { start: colors[0], @@ -70,6 +73,12 @@ export const setStyleTrigger = (settings, value) => { settings.set_string(STYLE_TRIGGER, value); }; +export const getProximityDistance = settings => settings.get_int(PROXIMITY_DISTANCE); + +export const setProximityDistance = (settings, value) => { + settings.set_int(PROXIMITY_DISTANCE, value); +}; + /** * Gets the gradient direction setting * @@ -163,6 +172,7 @@ export const attachSettingsListeners = (settings, listener) => { GRADIENT_DIRECTION, MAXIMIZED_BEHAVIOR, STYLE_TRIGGER, + PROXIMITY_DISTANCE, COLORS, MAXIMIZED_COLORS, MAXIMIZED_GRADIENT_DIRECTION diff --git a/src/constants.js b/src/constants.js index 4fde623..2324f02 100644 --- a/src/constants.js +++ b/src/constants.js @@ -14,6 +14,8 @@ export const STYLE_TRIGGER = { PROXIMITY: 'proximity' }; +export const DEFAULT_PROXIMITY_DISTANCE = 5; + // Gradient directions export const GRADIENT_DIRECTION = { VERTICAL: 'vertical', From a4a06fc57f1b4a46b26c41194d5556434cb47369 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 21:58:51 +0100 Subject: [PATCH 16/23] Add proximity distance setting to behaviors page --- src/settings/behavioursPage.js | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/settings/behavioursPage.js b/src/settings/behavioursPage.js index 46bb72b..85137ff 100644 --- a/src/settings/behavioursPage.js +++ b/src/settings/behavioursPage.js @@ -12,7 +12,9 @@ import { getMaximizedBehavior, setMaximizedBehavior, getStyleTrigger, - setStyleTrigger + setStyleTrigger, + getProximityDistance, + setProximityDistance } from '../config.js'; import { MAXIMIZED_BEHAVIOR, STYLE_TRIGGER } from '../constants.js'; @@ -70,6 +72,19 @@ class Behavior extends Adw.PreferencesPage { expression: new Gtk.PropertyExpression(MaximizedBehavior, null, 'name') }); + const proximityDistanceRow = new Adw.SpinRow({ + title: gettext('Proximity distance'), + subtitle: gettext('Distance from the panel in pixels'), + adjustment: new Gtk.Adjustment({ + lower: 0, + upper: 100, + step_increment: 1, + page_increment: 5, + value: getProximityDistance(this._settings) + }), + digits: 0 + }); + // Create model for maximized behavior dropdown const maximizedBehaviorModel = new Gio.ListStore({ item_type: MaximizedBehavior @@ -105,6 +120,9 @@ class Behavior extends Adw.PreferencesPage { // Set initial value setMaximizedBehaviorOnRow(maximizedBehaviorRow, getMaximizedBehavior(this._settings)); setMaximizedBehaviorOnRow(triggerRow, getStyleTrigger(this._settings)); + proximityDistanceRow.set_sensitive( + getStyleTrigger(this._settings) === STYLE_TRIGGER.PROXIMITY + ); // Connect to changes maximizedBehaviorRow.connect('notify::selected', () => { @@ -114,8 +132,12 @@ class Behavior extends Adw.PreferencesPage { triggerRow.connect('notify::selected', () => { setStyleTrigger(this._settings, triggerRow.selectedItem.value); }); + proximityDistanceRow.connect('notify::value', () => { + setProximityDistance(this._settings, Math.round(proximityDistanceRow.value)); + }); behaviorGroup.add(triggerRow); + behaviorGroup.add(proximityDistanceRow); behaviorGroup.add(maximizedBehaviorRow); this.add(behaviorGroup); @@ -123,6 +145,8 @@ class Behavior extends Adw.PreferencesPage { const config = getConfig(s); setMaximizedBehaviorOnRow(maximizedBehaviorRow, config.maximizedBehavior); setMaximizedBehaviorOnRow(triggerRow, config.styleTrigger); + proximityDistanceRow.set_value(config.proximityDistance); + proximityDistanceRow.set_sensitive(config.styleTrigger === STYLE_TRIGGER.PROXIMITY); }; const settingsHandlerIds = attachSettingsListeners(settings, onSettingsChanged); window.connect('close-request', () => { From b58d4e54510230534270644ad7bb2eab2f448bec Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 22:01:11 +0100 Subject: [PATCH 17/23] Update proximity distance handling in config and ProximityWindows class --- src/config.js | 5 ++++- src/events/behaviours/proximityWindows.js | 9 +++++++-- src/extension.js | 4 ++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/config.js b/src/config.js index 06d75d7..a361c8c 100644 --- a/src/config.js +++ b/src/config.js @@ -1,6 +1,8 @@ import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; +import { DEFAULT_PROXIMITY_DISTANCE } from './constants.js'; + export const SETTINGS_GSCHEMA = 'org.gnome.shell.extensions.org.pshow.gradienttopbar'; @@ -73,7 +75,8 @@ export const setStyleTrigger = (settings, value) => { settings.set_string(STYLE_TRIGGER, value); }; -export const getProximityDistance = settings => settings.get_int(PROXIMITY_DISTANCE); +export const getProximityDistance = settings => + settings?.get_int(PROXIMITY_DISTANCE) ?? DEFAULT_PROXIMITY_DISTANCE; export const setProximityDistance = (settings, value) => { settings.set_int(PROXIMITY_DISTANCE, value); diff --git a/src/events/behaviours/proximityWindows.js b/src/events/behaviours/proximityWindows.js index a5c1abf..9248849 100644 --- a/src/events/behaviours/proximityWindows.js +++ b/src/events/behaviours/proximityWindows.js @@ -1,16 +1,21 @@ import { layoutManager, panel } from 'resource:///org/gnome/shell/ui/main.js'; -const DEFAULT_PROXIMITY_DISTANCE = 5; +import { getProximityDistance } from '../../config.js'; // Keeps all panel-layout dependencies inside the proximity behaviour. export default class ProximityWindows { + constructor(settings) { + this.settings = settings; + } + matches(window) { const { primaryMonitor } = layoutManager; if (!primaryMonitor) return false; // Include a small tolerance below the panel to avoid an exact-edge requirement. - const panelBottom = primaryMonitor.y + panel.get_height() + DEFAULT_PROXIMITY_DISTANCE; + const distance = getProximityDistance(this.settings); + const panelBottom = primaryMonitor.y + panel.get_height() + distance; return window.get_frame_rect().y <= panelBottom; } diff --git a/src/extension.js b/src/extension.js index ed67a9f..dbf4033 100755 --- a/src/extension.js +++ b/src/extension.js @@ -21,7 +21,7 @@ export default class GradientTopBar extends Extension { const maximizedBehavior = getMaximizedBehavior(settings); this.windowEvents.setBehaviour( getStyleTrigger(settings) === STYLE_TRIGGER.PROXIMITY - ? new ProximityWindows() + ? new ProximityWindows(settings) : new MaximizedWindows() ); @@ -99,7 +99,7 @@ export default class GradientTopBar extends Extension { global.window_manager, global.get_workspace_manager(), getStyleTrigger(this._settings) === STYLE_TRIGGER.PROXIMITY - ? new ProximityWindows() + ? new ProximityWindows(this._settings) : new MaximizedWindows() ); this.setWindowStateCallback(); From b17276afcc1ac02854064c84cebda461a80584cf Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Thu, 24 Sep 2026 22:20:40 +0100 Subject: [PATCH 18/23] Update README to clarify alternate style trigger distance for windows --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d5b4f2..861be52 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Give GNOME’s top bar a customizable gradient. - Choose start and end colors of the gradient - Vertical or horizontal orientation -- Trigger an alternate style for maximized windows or windows near the panel +- Trigger an alternate style for maximized windows or windows within a configurable distance of the panel - Keep the gradient, restore the original theme, or apply a custom gradient when triggered - Simple preferences dialog @@ -113,7 +113,7 @@ The project includes several scripts to help with development: 4. If needed, debug using `yarn pref-debug` or `yarn nested-wayland`. 5. Submit a pull request with your changes. 6. To publish a new version: - 1) Run `yarn zip-extension` (or `yarn zip`) to create the zip + 1) Run `yarn zip` to create the complete extension zip 2) Run `yarn upload` and follow the browser flow # To Do From 31967ca9a6bf6274fb73a0d3bd886c6e500c03e1 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Fri, 25 Sep 2026 01:32:07 +0100 Subject: [PATCH 19/23] Update package.json scripts for schema validation and README documentation --- README.md | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 861be52..1b0c2b6 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ The project includes several scripts to help with development: ### Building and Installation -- `yarn compile-schemas`: Compiles the GSettings schemas. +- `yarn validate-schemas`: Validates the GSettings schemas. - `yarn zip`: Creates a zip archive of the extension. - `yarn zip-extension`: Packs the extension using GNOME's extension tools. - `yarn local-install`: Compiles schemas, zips the extension, installs it locally, and enables it (all-in-one command for testing changes). diff --git a/package.json b/package.json index aa29a55..31193ea 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "nested-wayland": "dbus-run-session -- gnome-shell --devkit --wayland", "pref-debug": "journalctl -f -o cat /usr/bin/gjs", "open-prefs": "gnome-extensions prefs gradienttopbar@pshow.org", - "local-install": "yarn compile-schemas && yarn zip && gnome-extensions install gradienttopbar@pshow.org.shell-extension.zip --force && gnome-extensions enable gradienttopbar@pshow.org", + "local-install": "yarn validate-schemas && yarn zip && gnome-extensions install gradienttopbar@pshow.org.shell-extension.zip --force && gnome-extensions enable gradienttopbar@pshow.org", "zip-extension": "gnome-extensions pack src --force", - "compile-schemas": "glib-compile-schemas ./src/schemas/", - "zip": "cd src && zip -r ../gradienttopbar@pshow.org.shell-extension.zip .", + "validate-schemas": "glib-compile-schemas --strict --dry-run ./src/schemas/", + "zip": "cd src && zip -FS -r ../gradienttopbar@pshow.org.shell-extension.zip .", "prettify": "prettier --write 'src/**/*.{css}' && yarn lint --fix", "lint": "eslint 'src/**/*.{js,json}'", "setup-python": "python3 -m venv .venv && .venv/bin/python -m pip install -r requirements-dev.txt", From df51a13cd3f0e6160a0532177b5e87b5de43cb28 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Fri, 25 Sep 2026 01:32:38 +0100 Subject: [PATCH 20/23] remove compiled schemas --- src/schemas/gschemas.compiled | Bin 1028 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/schemas/gschemas.compiled diff --git a/src/schemas/gschemas.compiled b/src/schemas/gschemas.compiled deleted file mode 100644 index 4e09c5747cffd90c9b4e21f1f6f046b43dc7d99b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1028 zcmbtTJ&4pm6n^UMou?>zAXr$)VUd8FtXgcY@{*bNF3~z|gP%bdoFZ~qv;cvw6Y~O~O&XeN*31Yax4rV5G?aI7QgEdz{d{eSOg!QFz7mIp+Lb@I@5#aaZlI&apoS zoB)@%e*9*g<3;3`;1|H{w_h~t93O-J8vGq-`^GW%?I$zLPQ`|rs!cf=w#^w%l#z)= z>o{{Jja7-rIhOo|<$BSKOn#5 z2Nbt8Gy{r>s#e)s)&JJjyH0ejO+{qVu?R-z&o-Y6;XYv$t%Y>*90j5Z`-jIe6Emgx zCf%2^>{^K5U9a|TH2w0L&E(W)IgFZ|D9gMaR<_PrbjQ0MmPYf4Y>gIFRuqYUXn-Vt zgTWJk0u|JOQehajmRQ|q<6mEJwd!5@HxI2*X)1AWXA)=l7lK_cHa@!2$Q|kG<@-p) Qp7_7yuN#=_T>^Oh1?2$xr~m)} From 2485ae2a530f6e6021c062b0e84713336404d389 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Fri, 25 Sep 2026 01:32:54 +0100 Subject: [PATCH 21/23] fix async stuff based on shexli --- src/config.js | 19 +++++++++++++------ src/settings/components/fileChooser.js | 4 ++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/config.js b/src/config.js index a361c8c..a6f9cb7 100644 --- a/src/config.js +++ b/src/config.js @@ -3,6 +3,8 @@ import GLib from 'gi://GLib'; import { DEFAULT_PROXIMITY_DISTANCE } from './constants.js'; +Gio._promisify(Gio.Subprocess.prototype, 'communicate_utf8_async'); + export const SETTINGS_GSCHEMA = 'org.gnome.shell.extensions.org.pshow.gradienttopbar'; @@ -197,14 +199,19 @@ export const detachSettingsListeners = (settings, handlerIds) => { * * @param {Gio.File} file - The file to export settings to */ -export const exportSettingsToFile = file => { +export const exportSettingsToFile = async file => { + const process = Gio.Subprocess.new( + ['dconf', 'dump', SETTINGS_GSCHEMA_PATH], + Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + ); + const [stdout, stderr] = await process.communicate_utf8_async(null, null); + + if (!process.get_successful()) + throw new Error(stderr.trim() || 'Failed to export settings'); + const raw = file.replace(null, false, Gio.FileCreateFlags.NONE, null); const out = Gio.BufferedOutputStream.new_sized(raw, 4096); - - const settings = GLib.spawn_command_line_sync( - `dconf dump ${SETTINGS_GSCHEMA_PATH}` - )[1]; - out.write_all(settings, null); + out.write_all(new TextEncoder().encode(stdout), null); out.close(null); }; diff --git a/src/settings/components/fileChooser.js b/src/settings/components/fileChooser.js index 2882de4..ab5d9a5 100644 --- a/src/settings/components/fileChooser.js +++ b/src/settings/components/fileChooser.js @@ -35,10 +35,10 @@ export const saveFileDialog = ({ onSelected, onError, transientFor }) => { }); dialog.set_accept_label('Save'); - dialog.save(transientFor, null, (self, res) => { + dialog.save(transientFor, null, async (self, res) => { try { const file = self.save_finish(res); - onSelected(file); + await onSelected(file); } catch (error) { if ( error instanceof Gtk.DialogError && From c2a3212551c35453cdaca02c47a78397f4a81eb2 Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Fri, 25 Sep 2026 01:33:55 +0100 Subject: [PATCH 22/23] Update verify.yml to include schema validation in lint job --- .github/workflows/verify.yml | 3 ++- src/stylesheet.css | 0 2 files changed, 2 insertions(+), 1 deletion(-) mode change 100755 => 100644 src/stylesheet.css diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index f7bf89b..f53b5fa 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,7 +8,7 @@ permissions: {} jobs: lint: - name: ESLint + name: ESLint and schemas runs-on: ubuntu-latest timeout-minutes: 10 steps: @@ -20,6 +20,7 @@ jobs: - run: corepack enable - run: yarn install --immutable - run: yarn lint + - run: yarn validate-schemas shexli: name: Shexli diff --git a/src/stylesheet.css b/src/stylesheet.css old mode 100755 new mode 100644 From 3878f1551b74d168aab1a48f8b57c0dcaa8e45de Mon Sep 17 00:00:00 2001 From: Petar Vasilev Date: Fri, 25 Sep 2026 01:48:21 +0100 Subject: [PATCH 23/23] Update shexli command to check for specific rule ID and enhance verify.yml with yarn install --- .github/workflows/verify.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index f53b5fa..e84cf50 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -1,7 +1,6 @@ name: Verify on: - push: pull_request: permissions: {} @@ -37,5 +36,6 @@ jobs: cache: pip cache-dependency-path: requirements-dev.txt - run: corepack enable + - run: yarn install --immutable - run: yarn setup-python - run: yarn shexli diff --git a/package.json b/package.json index 31193ea..2b7b9e7 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "prettify": "prettier --write 'src/**/*.{css}' && yarn lint --fix", "lint": "eslint 'src/**/*.{js,json}'", "setup-python": "python3 -m venv .venv && .venv/bin/python -m pip install -r requirements-dev.txt", - "shexli": ".venv/bin/shexli --format json \"$PWD/src\" | .venv/bin/python -c \"import json, sys; report = json.load(sys.stdin); print(json.dumps(report, indent=2)); raise SystemExit(report['summary']['finding_count'] != 0)\"", + "shexli": ".venv/bin/shexli --format json \"$PWD/src\" | .venv/bin/python -c \"import json, sys; report = json.load(sys.stdin); print(json.dumps(report, indent=2)); raise SystemExit(any(finding['rule_id'] != 'EGO-M-004' for finding in report['findings']))\"", "verify": "yarn lint && yarn shexli", "precommit": "lint-staged", "upload": "gnome-extensions upload --accept-tos"