From 0ace2ffa78bcde088b11bcaa85f7d8e669526a3d Mon Sep 17 00:00:00 2001 From: flexagoon Date: Thu, 29 Aug 2024 13:55:06 +0300 Subject: [PATCH] refactor!: refactor the utils directory A third commit in a series of major refactors, this time it greatly simplifies the utils directory, both in terms of code readability and in terms of application logic. Since the things defined inside of utils are used throughout the entire codebase, this commit also introduces significant changes outside of the utils directory. BREAKING CHANGE: This commit changes the settings schema, which means that a lot of the settings will have to be reset when the extension is upgraded. BREAKING CHANGE: The code that manually disabled shadows for X11 windows has been removed, since it uses GNOME APIs which weren't supposed to be public and which will are now removed[1]. I didn't notice any issues after this change, but they may happen in the future. [1]: https://gitlab.gnome.org/GNOME/mutter/-/commit/0104fbe577594bcc7fdc965be323be08e4bd35d0 --- biome.json | 3 +- po/ar.po | 39 +- po/cs.po | 39 +- po/de.po | 39 +- po/eo.po | 36 +- po/es.po | 39 +- po/et.po | 36 +- po/fi.po | 39 +- po/fr.po | 39 +- po/hu.po | 36 +- po/id.po | 39 +- po/it.po | 39 +- po/ja.po | 39 +- po/nb_NO.po | 36 +- po/nl.po | 39 +- po/pl.po | 39 +- po/pt.po | 39 +- po/pt_BR.po | 39 +- po/rounded-window-corners@fxgn.pot | 36 +- po/ru.po | 39 +- po/tr.po | 39 +- po/uk.po | 39 +- po/zh_CN.po | 39 +- ....rounded-window-corners-reborn.gschema.xml | 27 +- resources/stylesheet.css | 2 +- src/effect/clip_shadow_effect.ts | 4 +- src/effect/rounded_corners_effect.ts | 8 +- src/effect/shader/clip_shadow.frag | 3 +- src/extension.ts | 105 +++-- src/manager/README.md | 4 + src/manager/event_handlers.ts | 218 ++--------- src/manager/event_manager.ts | 106 ++--- src/manager/utils.ts | 316 +++++++++++++++ src/preferences/pages/blacklist.ts | 16 +- src/preferences/pages/custom.ts | 137 +++---- src/preferences/pages/general.ts | 78 ++-- src/preferences/widgets/app_row.ts | 12 +- src/preferences/widgets/edit_shadow_page.ts | 59 +-- src/preferences/widgets/paddings_row.ts | 9 +- src/preferences/widgets/reset_page.ts | 49 +-- src/preferences/window_picker/service.ts | 11 +- src/prefs.ts | 20 +- src/utils/README.md | 35 ++ src/utils/background_menu.ts | 89 +++++ src/utils/box_shadow.ts | 22 ++ src/utils/connections.ts | 134 ------- src/utils/constants.ts | 23 +- src/utils/file.ts | 51 +++ src/utils/io.ts | 41 -- src/utils/log.ts | 34 +- src/utils/prefs.ts | 48 --- src/utils/settings.ts | 367 ++++++++---------- src/utils/types.ts | 80 ++-- src/utils/ui.ts | 246 ------------ 54 files changed, 1501 insertions(+), 1699 deletions(-) create mode 100644 src/manager/utils.ts create mode 100644 src/utils/README.md create mode 100644 src/utils/background_menu.ts create mode 100644 src/utils/box_shadow.ts delete mode 100644 src/utils/connections.ts create mode 100644 src/utils/file.ts delete mode 100644 src/utils/io.ts delete mode 100644 src/utils/prefs.ts delete mode 100644 src/utils/ui.ts diff --git a/biome.json b/biome.json index 23bbede..4a830fc 100644 --- a/biome.json +++ b/biome.json @@ -13,8 +13,7 @@ "style": { "noNamespaceImport": "off", "noDefaultExport": "off", - "useNamingConvention": "off", - "useSingleCaseStatement": "off" + "useNamingConvention": "off" }, "performance": { "noDelete": "off" diff --git a/po/ar.po b/po/ar.po index 46de833..0e32445 100644 --- a/po/ar.po +++ b/po/ar.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: 12\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-22 11:54-0700\n" +"POT-Creation-Date: 2024-09-14 21:07+0300\n" "PO-Revision-Date: 2023-12-10 10:05+0000\n" "Last-Translator: Nadjib Chergui \n" "Language-Team: Arabic \n" "Language-Team: Czech \n" "Language-Team: German \n" "Language-Team: Esperanto \n" "Language-Team: Spanish \n" "Language-Team: Finnish \n" "Language-Team: French \n" "Language-Team: Hungarian \n" "Language-Team: Indonesian \n" "Language-Team: Italian \n" "Language-Team: Japanese \n" "Language-Team: Norwegian Bokmål \n" "Language-Team: Dutch \n" "Language-Team: Polish \n" "Language-Team: Portuguese \n" "Language-Team: Portuguese (Brazil) \n" "Language-Team: LANGUAGE \n" @@ -124,7 +124,7 @@ msgid "Debug" msgstr "" #: src/preferences/pages/general.ui:174 -#: src/preferences/widgets/reset_page.ts:61 +#: src/preferences/widgets/reset_page.ts:64 msgid "Enable Log" msgstr "" @@ -230,15 +230,15 @@ msgstr "" msgid "_Reset" msgstr "" -#: src/preferences/widgets/app_row.ts:28 +#: src/preferences/widgets/app_row.ts:27 msgid "Window class" msgstr "" -#: src/preferences/widgets/app_row.ts:48 +#: src/preferences/widgets/app_row.ts:47 msgid "Expand this row, to pick a window" msgstr "" -#: src/preferences/widgets/app_row.ts:92 +#: src/preferences/widgets/app_row.ts:86 msgid "Can't pick window from this position" msgstr "" @@ -262,50 +262,46 @@ msgstr "" msgid "Always clip rounded corners even for fullscreen window" msgstr "" -#: src/preferences/widgets/reset_page.ts:54 +#: src/preferences/widgets/reset_page.ts:57 msgid "Skip LibAdwaita Applications" msgstr "" -#: src/preferences/widgets/reset_page.ts:56 +#: src/preferences/widgets/reset_page.ts:59 msgid "Skip LibHandy Applications" msgstr "" -#: src/preferences/widgets/reset_page.ts:57 +#: src/preferences/widgets/reset_page.ts:60 msgid "Focus Window Shadow Style" msgstr "" -#: src/preferences/widgets/reset_page.ts:58 +#: src/preferences/widgets/reset_page.ts:61 msgid "Unfocus Window Shadow Style" msgstr "" -#: src/preferences/widgets/reset_page.ts:59 +#: src/preferences/widgets/reset_page.ts:62 msgid "Border Width" msgstr "" -#: src/preferences/widgets/reset_page.ts:60 +#: src/preferences/widgets/reset_page.ts:63 msgid "Border Color" msgstr "" -#: src/preferences/widgets/reset_page.ts:65 +#: src/preferences/widgets/reset_page.ts:68 msgid "Border Radius" msgstr "" -#: src/preferences/widgets/reset_page.ts:66 +#: src/preferences/widgets/reset_page.ts:69 msgid "Padding" msgstr "" -#: src/preferences/widgets/reset_page.ts:68 +#: src/preferences/widgets/reset_page.ts:71 msgid "Keep Rounded Corners when Maximized or Fullscreen" msgstr "" -#: src/preferences/widgets/reset_page.ts:70 +#: src/preferences/widgets/reset_page.ts:73 msgid "Corner Smoothing" msgstr "" -#: src/utils/prefs.ts:48 -msgid "Expand this row to pick a window." -msgstr "" - -#: src/utils/ui.ts:105 src/utils/ui.ts:134 +#: src/utils/background_menu.ts:60 src/utils/background_menu.ts:82 msgid "Rounded Corners Settings..." msgstr "" diff --git a/po/ru.po b/po/ru.po index 59ae98e..4874b74 100644 --- a/po/ru.po +++ b/po/ru.po @@ -1,7 +1,7 @@ msgid "" msgstr "" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-07-22 11:54-0700\n" +"POT-Creation-Date: 2024-09-14 21:07+0300\n" "PO-Revision-Date: 2023-03-09 15:37+0000\n" "Last-Translator: Evgeniy Khramov \n" "Language-Team: Russian \n" "Language-Team: Turkish \n" "Language-Team: Ukrainian \n" "Language-Team: Chinese (Simplified) - 4 + 5 - + [] window here will not be rounded @@ -54,12 +54,13 @@ 'top': <uint32 1>, 'bottom': <uint32 1> }>, - 'keep_rounded_corners': <{ + 'keepRoundedCorners': <{ 'maximized': <false>, 'fullscreen': <false> }>, - 'border_radius': <uint32 12>, - 'smoothing': <0> + 'borderRadius': <uint32 12>, + 'smoothing': <0>, + 'enabled': <true> } @@ -74,10 +75,10 @@ Shadow for focused window { - 'horizontal_offset': 0, - 'vertical_offset': 4, - 'blur_offset': 28, - 'spread_radius': 4, + 'horizontalOffset': 0, + 'verticalOffset': 4, + 'blurOffset': 28, + 'spreadRadius': 4, 'opacity': 60 } @@ -87,10 +88,10 @@ Shadow for unfocused window { - 'horizontal_offset': 0, - 'vertical_offset': 2, - 'blur_offset': 12, - 'spread_radius': -1, + 'horizontalOffset': 0, + 'verticalOffset': 2, + 'blurOffset': 12, + 'spreadRadius': -1, 'opacity': 65 } diff --git a/resources/stylesheet.css b/resources/stylesheet.css index 60391fa..f948487 100644 --- a/resources/stylesheet.css +++ b/resources/stylesheet.css @@ -3,4 +3,4 @@ transition-duration: 300ms; transition-property: box-shadow; transition-timing-function: linear; -} \ No newline at end of file +} diff --git a/src/effect/clip_shadow_effect.ts b/src/effect/clip_shadow_effect.ts index 2616ad4..75f665e 100644 --- a/src/effect/clip_shadow_effect.ts +++ b/src/effect/clip_shadow_effect.ts @@ -4,11 +4,11 @@ import GObject from 'gi://GObject'; import Shell from 'gi://Shell'; // local modules -import {loadShader} from '../utils/io.js'; +import {readShader} from '../utils/file.js'; // ------------------------------------------------------------------- [imports] -const {declarations, code} = loadShader( +const [declarations, code] = readShader( import.meta.url, 'shader/clip_shadow.frag', ); diff --git a/src/effect/rounded_corners_effect.ts b/src/effect/rounded_corners_effect.ts index b0c11de..0f6624c 100644 --- a/src/effect/rounded_corners_effect.ts +++ b/src/effect/rounded_corners_effect.ts @@ -4,7 +4,7 @@ import Meta from 'gi://Meta'; import Shell from 'gi://Shell'; // local modules -import {loadShader} from '../utils/io.js'; +import {readShader} from '../utils/file.js'; import type * as types from '../utils/types.js'; // types @@ -13,7 +13,7 @@ import type Clutter from 'gi://Clutter'; // --------------------------------------------------------------- [end imports] // Load fragment shader of rounded corners effect. -const {declarations, code} = loadShader( +const [declarations, code] = readShader( import.meta.url, 'shader/rounded_corners.frag', ); @@ -80,7 +80,7 @@ export const RoundedCornersEffect = GObject.registerClass( */ update_uniforms( scale_factor: number, - corners_cfg: types.RoundedCornersCfg, + corners_cfg: types.RoundedCornerSettings, outer_bounds: types.Bounds, border: { width: number; @@ -91,7 +91,7 @@ export const RoundedCornersEffect = GObject.registerClass( const border_width = border.width * scale_factor; const border_color = border.color; - const outer_radius = corners_cfg.border_radius * scale_factor; + const outer_radius = corners_cfg.borderRadius * scale_factor; const {padding, smoothing} = corners_cfg; const bounds = [ diff --git a/src/effect/shader/clip_shadow.frag b/src/effect/shader/clip_shadow.frag index e893b15..23e76d6 100644 --- a/src/effect/shader/clip_shadow.frag +++ b/src/effect/shader/clip_shadow.frag @@ -1,7 +1,6 @@ - // clip shadow in a simple way void main() { vec4 color = cogl_color_out; float gray = (color.r + color.g + color.b) / 3.0; cogl_color_out *= (1.0 - smoothstep(0.4, 1.0, gray)) * color.a; -} \ No newline at end of file +} diff --git a/src/extension.ts b/src/extension.ts index 776e730..b97be74 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,9 +1,7 @@ // imports.gi import Clutter from 'gi://Clutter'; import GObject from 'gi://GObject'; -import type Gio from 'gi://Gio'; import Graphene from 'gi://Graphene'; -import type Meta from 'gi://Meta'; // gnome-shell modules import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js'; @@ -14,15 +12,24 @@ import {WorkspaceAnimationController} from 'resource:///org/gnome/shell/ui/works // local modules import {LinearFilterEffect} from './effect/linear_filter_effect.js'; import {disableEffect, enableEffect} from './manager/event_manager.js'; +import { + getRoundedCornersEffect, + shouldEnableEffect, + windowScaleFactor, +} from './manager/utils.js'; import {WindowPicker} from './preferences/window_picker/service.js'; -import {connections} from './utils/connections.js'; -import {constants, SHADOW_PADDING} from './utils/constants.js'; -import {_log, stackMsg} from './utils/log.js'; -import {init_settings, settings, uninit_settings} from './utils/settings.js'; -import * as UI from './utils/ui.js'; +import { + disableBackgroundMenuItem, + enableBackgroundMenuItem, +} from './utils/background_menu.js'; +import {OVERVIEW_SHADOW_ACTOR, SHADOW_PADDING} from './utils/constants.js'; +import {logDebug} from './utils/log.js'; +import {getPref, initPrefs, prefs, uninitPrefs} from './utils/settings.js'; // types, which will be removed in output -import type {ExtensionsWindowActor} from './utils/types.js'; +import type Gio from 'gi://Gio'; +import type Meta from 'gi://Meta'; +import type {RoundedWindowActor} from './utils/types.js'; // --------------------------------------------------------------- [end imports] @@ -35,7 +42,7 @@ export default class RoundedWindowCornersReborn extends Extension { private _windowPicker: WindowPicker | null = null; enable() { - init_settings(this.getSettings()); + initPrefs(this.getSettings()); // Restore original methods, those methods will be restore when // extensions is disabled @@ -55,18 +62,17 @@ export default class RoundedWindowCornersReborn extends Extension { // 21d4bbde15acf7c3bf348f7375a12f7b14c3ab6f/src/extension.js#L87 if (layoutManager._startingUp) { - const c = connections.get(); - c.connect(layoutManager, 'startup-complete', () => { + const connection = layoutManager.connect('startup-complete', () => { enableEffect(); - if (settings().enable_preferences_entry) { - UI.SetupBackgroundMenu(); + if (getPref('enable-preferences-entry')) { + enableBackgroundMenuItem(); } - c.disconnect_all(layoutManager); + layoutManager.disconnect(connection); }); } else { enableEffect(); - if (settings().enable_preferences_entry) { - UI.SetupBackgroundMenu(); + if (getPref('enable-preferences-entry')) { + enableBackgroundMenuItem(); } } @@ -89,7 +95,7 @@ export default class RoundedWindowCornersReborn extends Extension { // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js // /ui/windowPreview.js#L42 - const stack = stackMsg(); + const stack = Error().stack?.trim(); if ( stack === undefined || stack.indexOf('_updateAttachedDialogs') !== -1 || @@ -101,17 +107,17 @@ export default class RoundedWindowCornersReborn extends Extension { // If the window don't have rounded corners and shadows, // just return let has_rounded_corners = false; - const window_actor: ExtensionsWindowActor = - window.get_compositor_private() as ExtensionsWindowActor; - const shadow = window_actor.__rwcRoundedWindowInfo?.shadow; + const window_actor: RoundedWindowActor = + window.get_compositor_private() as RoundedWindowActor; + const shadow = window_actor.rwcCustomData?.shadow; if (shadow) { - has_rounded_corners = UI.shouldEnableEffect(window); + has_rounded_corners = shouldEnableEffect(window); } if (!(has_rounded_corners && shadow)) { return; } - _log(`Add shadow for ${window.title} in overview`); + logDebug(`Add shadow for ${window.title} in overview`); // WindowPreview.windowContainer used to show content of window const windowContainer = this.windowContainer; @@ -128,12 +134,12 @@ export default class RoundedWindowCornersReborn extends Extension { this.insert_child_below(shadow_clone, windowContainer); // Disconnect all signals when Window preview in overview is destroy - c.connect(this, 'destroy', () => { + const connection = this.connect('destroy', () => { shadow_clone.destroy(); firstChild?.clear_effects(); firstChild = null; - c.disconnect_all(this); + this.disconnect(connection); }); }; @@ -176,10 +182,10 @@ export default class RoundedWindowCornersReborn extends Extension { } of workspace._windowRecords) { const win = actor.metaWindow; const frame_rect = win.get_frame_rect(); - const shadow = (actor as ExtensionsWindowActor) - .__rwcRoundedWindowInfo?.shadow; + const shadow = (actor as RoundedWindowActor) + .rwcCustomData?.shadow; const enabled = - UI.get_rounded_corners_effect(actor)?.enabled; + getRoundedCornersEffect(actor)?.enabled; if (shadow && enabled) { // Only create shadow actor when window should have rounded // corners when switching workspace @@ -190,7 +196,7 @@ export default class RoundedWindowCornersReborn extends Extension { source: shadow, }); const paddings = - SHADOW_PADDING * UI.WindowScaleFactor(win); + SHADOW_PADDING * windowScaleFactor(win); shadow_clone.width = frame_rect.width + paddings * 2; @@ -252,32 +258,27 @@ export default class RoundedWindowCornersReborn extends Extension { self._orig_finish_workspace_swt.apply(this, [switchData]); }; - const c = connections.get(); - // Gnome-shell will not disable extensions when _logout/shutdown/restart // system, it means that the signal handlers will not be cleaned when // gnome-shell is closing. // // Now clear all resources manually before gnome-shell closes - c.connect(global.display, 'closing', () => { - _log('Clear all resources because gnome-shell is shutdown'); + const connection = global.display.connect('closing', () => { + logDebug('Clear all resources because gnome-shell is shutdown'); this.disable(); + global.display.disconnect(connection); }); // Watch changes of GSettings - c.connect( - settings().g_settings, - 'changed', - (_: Gio.Settings, k: string) => { - if (k === 'enable-preferences-entry') { - settings().enable_preferences_entry - ? UI.SetupBackgroundMenu() - : UI.RestoreBackgroundMenu(); - } - }, - ); + prefs.connect('changed', (_: Gio.Settings, k: string) => { + if (k === 'enable-preferences-entry') { + getPref('enable-preferences-entry') + ? enableBackgroundMenuItem() + : disableBackgroundMenuItem(); + } + }); - _log('Enabled'); + logDebug('Enabled'); } disable() { @@ -289,21 +290,17 @@ export default class RoundedWindowCornersReborn extends Extension { this._orig_finish_workspace_swt; // Remove the item to open preferences page in background menu - UI.RestoreBackgroundMenu(); + disableBackgroundMenuItem(); this._windowPicker?.unexport(); disableEffect(); - // Disconnect all signals in global connections.get() - connections.get().disconnect_all(); - connections.del(); - // Set all props to null this._windowPicker = null; - _log('Disabled'); + logDebug('Disabled'); - uninit_settings(); + uninitPrefs(); } } @@ -324,7 +321,7 @@ const OverviewShadowActor = GObject.registerClass( constructor(source: Clutter.Actor, window_preview: WindowPreview) { super({ source, // the source shadow actor shown in desktop - name: constants.OVERVIEW_SHADOW_ACTOR, + name: OVERVIEW_SHADOW_ACTOR, pivotPoint: new Graphene.Point({x: 0.5, y: 0.5}), }); @@ -361,9 +358,7 @@ const OverviewShadowActor = GObject.registerClass( windowContainerBox.get_width() / meta_win.get_frame_rect().width; const paddings = - SHADOW_PADDING * - container_scaled * - UI.WindowScaleFactor(meta_win); + SHADOW_PADDING * container_scaled * windowScaleFactor(meta_win); // Setup bounds box of shadow actor box.set_origin(-paddings, -paddings); diff --git a/src/manager/README.md b/src/manager/README.md index 3f0f1a5..2e32797 100644 --- a/src/manager/README.md +++ b/src/manager/README.md @@ -14,3 +14,7 @@ effect. It attaches the necessary signals to matching handlers on each effect. ## `event_handlers.ts` Contains the implementation of handlers for all of the events. + +## `utils.ts` + +Provides various utility functions used withing signal handling code. diff --git a/src/manager/event_handlers.ts b/src/manager/event_handlers.ts index 8fa7984..949d50b 100644 --- a/src/manager/event_handlers.ts +++ b/src/manager/event_handlers.ts @@ -7,37 +7,39 @@ import Clutter from 'gi://Clutter'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; -import Meta from 'gi://Meta'; import St from 'gi://St'; import {ClipShadowEffect} from '../effect/clip_shadow_effect.js'; import {RoundedCornersEffect} from '../effect/rounded_corners_effect.js'; import { - APP_SHADOWS, CLIP_SHADOW_EFFECT, ROUNDED_CORNERS_EFFECT, - SHADOW_PADDING, } from '../utils/constants.js'; -import {_log} from '../utils/log.js'; -import {settings} from '../utils/settings.js'; -import * as types from '../utils/types.js'; +import {logDebug} from '../utils/log.js'; +import {getPref} from '../utils/settings.js'; import { - WindowScaleFactor, + computeBounds, + computeShadowActorOffset, computeWindowContentsOffset, getRoundedCornersCfg, - get_rounded_corners_effect as getRoundedCornersEffect, + getRoundedCornersEffect, shouldEnableEffect, -} from '../utils/ui.js'; + unwrapActor, + updateShadowActorStyle, + windowScaleFactor, +} from './utils.js'; -import type {SchemasKeys} from '../utils/settings.js'; -import type {ExtensionsWindowActor} from '../utils/types.js'; +import type Meta from 'gi://Meta'; +import type {SchemaKey} from '../utils/settings.js'; +import type {RoundedWindowActor} from '../utils/types.js'; -export function onAddEffect(actor: ExtensionsWindowActor) { - _log(`opened: ${actor?.metaWindow.title}: ${actor}`); +export function onAddEffect(actor: RoundedWindowActor) { + logDebug(`opened: ${actor?.metaWindow.title}: ${actor}`); const win = actor.metaWindow; if (!shouldEnableEffect(win)) { + logDebug(`Skipping ${win.title}`); return; } @@ -46,13 +48,6 @@ export function onAddEffect(actor: ExtensionsWindowActor) { new RoundedCornersEffect(), ); - // Some applications on X11 use server-side decorations, so their shadows - // are drawn my Mutter rather than by the application itself. This disables - // the shadow for those windows. - if (actor.shadow_mode !== undefined) { - actor.shadow_mode = Meta.ShadowMode.FORCED_OFF; - } - const shadow = createShadow(actor); // Bind properties of the window to the shadow actor. @@ -69,7 +64,7 @@ export function onAddEffect(actor: ExtensionsWindowActor) { } // Store shadow, app type, visible binding, so that we can access them later - actor.__rwcRoundedWindowInfo = { + actor.rwcCustomData = { shadow, unminimizedTimeoutId: 0, }; @@ -79,17 +74,12 @@ export function onAddEffect(actor: ExtensionsWindowActor) { refreshShadow(actor); } -export function onRemoveEffect(actor: ExtensionsWindowActor): void { +export function onRemoveEffect(actor: RoundedWindowActor): void { const name = ROUNDED_CORNERS_EFFECT; unwrapActor(actor)?.remove_effect_by_name(name); - // Restore shadow for x11 windows - if (actor.shadow_mode) { - actor.shadow_mode = Meta.ShadowMode.AUTO; - } - // Remove shadow actor - const shadow = actor.__rwcRoundedWindowInfo?.shadow; + const shadow = actor.rwcCustomData?.shadow; if (shadow) { global.windowGroup.remove_child(shadow); shadow.clear_effects(); @@ -97,33 +87,33 @@ export function onRemoveEffect(actor: ExtensionsWindowActor): void { } // Remove all timeout handler - const timeoutId = actor.__rwcRoundedWindowInfo?.unminimizedTimeoutId; + const timeoutId = actor.rwcCustomData?.unminimizedTimeoutId; if (timeoutId) { GLib.source_remove(timeoutId); } - delete actor.__rwcRoundedWindowInfo; + delete actor.rwcCustomData; } -export function onMinimize(actor: ExtensionsWindowActor): void { +export function onMinimize(actor: RoundedWindowActor): void { // Compatibility with "Compiz alike magic lamp effect". // When minimizing a window, disable the shadow to make the magic lamp effect // work. const magicLampEffect = actor.get_effect('minimize-magic-lamp-effect'); - const shadow = actor.__rwcRoundedWindowInfo?.shadow; + const shadow = actor.rwcCustomData?.shadow; const roundedCornersEffect = getRoundedCornersEffect(actor); if (magicLampEffect && shadow && roundedCornersEffect) { - _log('Minimizing with magic lamp effect'); + logDebug('Minimizing with magic lamp effect'); shadow.visible = false; roundedCornersEffect.enabled = false; } } -export function onUnminimize(actor: ExtensionsWindowActor): void { +export function onUnminimize(actor: RoundedWindowActor): void { // Compatibility with "Compiz alike magic lamp effect". // When unminimizing a window, wait until the effect is completed before // showing the shadow. const magicLampEffect = actor.get_effect('unminimize-magic-lamp-effect'); - const shadow = actor.__rwcRoundedWindowInfo?.shadow; + const shadow = actor.rwcCustomData?.shadow; const roundedCornersEffect = getRoundedCornersEffect(actor); if (magicLampEffect && shadow && roundedCornersEffect) { shadow.visible = false; @@ -133,7 +123,7 @@ export function onUnminimize(actor: ExtensionsWindowActor): void { const id = timer.connect('new-frame', source => { // Wait until the effect is 98% completed if (source.get_progress() > 0.98) { - _log('Unminimizing with magic lamp effect'); + logDebug('Unminimizing with magic lamp effect'); shadow.visible = true; roundedCornersEffect.enabled = true; source.disconnect(id); @@ -146,8 +136,7 @@ export function onUnminimize(actor: ExtensionsWindowActor): void { export function onRestacked(): void { for (const actor of global.get_window_actors()) { - const shadow = (actor as ExtensionsWindowActor).__rwcRoundedWindowInfo - ?.shadow; + const shadow = (actor as RoundedWindowActor).rwcCustomData?.shadow; if (!(actor.visible && shadow)) { continue; @@ -161,11 +150,11 @@ export const onSizeChanged = refreshRoundedCorners; export const onFocusChanged = refreshShadow; -export function onSettingsChanged(key: SchemasKeys): void { +export function onSettingsChanged(key: SchemaKey): void { switch (key) { case 'skip-libadwaita-app': case 'skip-libhandy-app': - case 'black-list': + case 'blacklist': refreshEffectState(); break; case 'focused-shadow': @@ -183,19 +172,6 @@ export function onSettingsChanged(key: SchemasKeys): void { } } -/** - * Get the actor that rounded corners should be applied to. - * In Wayland, the effect is applied to WindowActor, but in X11, it is applied - * to WindowActor.first_child. - * - * @param actor - The window actor to unwrap. - * @returns The correct actor that the effect should be applied to. - */ -function unwrapActor(actor: Meta.WindowActor): Clutter.Actor | null { - const type = actor.metaWindow.get_client_type(); - return type === Meta.WindowClientType.X11 ? actor.get_first_child() : actor; -} - /** * Create the shadow actor for a window. * @@ -233,120 +209,6 @@ function createShadow(actor: Meta.WindowActor): St.Bin { return shadow; } -/** Compute outer bounds for rounded corners of a window - * - * @param actor - The window actor to compute the bounds for. - * @param [x, y, width, height] - The content offsets of the window actor. - * */ -function computeBounds( - actor: Meta.WindowActor, - [x, y, width, height]: [number, number, number, number], -): types.Bounds { - const bounds = { - x1: x + 1, - y1: y + 1, - x2: x + actor.width + width, - y2: y + actor.height + height, - }; - - // Kitty draws its window decoration by itself, so we need to manually - // clip its shadow and recompute the outer bounds for it. - if (settings().tweak_kitty_terminal) { - if ( - actor.metaWindow.get_client_type() === - Meta.WindowClientType.WAYLAND && - actor.metaWindow.get_wm_class_instance() === 'kitty' - ) { - const [x1, y1, x2, y2] = APP_SHADOWS.kitty; - const scale = WindowScaleFactor(actor.metaWindow); - bounds.x1 += x1 * scale; - bounds.y1 += y1 * scale; - bounds.x2 -= x2 * scale; - bounds.y2 -= y2 * scale; - } - } - - return bounds; -} - -/** - * Compute the offset of the shadow actor for a window. - * - * @param actor - The window actor to compute the offset for. - * @param [offsetX, offsetY, offsetWidth, offsetHeight] - The content offsets of the window actor. - */ -function computeShadowActorOffset( - actor: Meta.WindowActor, - [offsetX, offsetY, offsetWidth, offsetHeight]: [ - number, - number, - number, - number, - ], -): number[] { - const win = actor.metaWindow; - const shadowPadding = SHADOW_PADDING * WindowScaleFactor(win); - - return [ - offsetX - shadowPadding, - offsetY - shadowPadding, - 2 * shadowPadding + offsetWidth, - 2 * shadowPadding + offsetHeight, - ]; -} - -/** Update css style of a shadow actor - * - * @param win - The window to update the style for. - * @param actor - The shadow actor to update the style for. - * @param borderRadiusRaw - The border radius of the shadow actor. - * @param shadow - The shadow settings for the window. - * @param padding - The padding of the shadow actor. - * */ -function updateShadowActorStyle( - win: Meta.Window, - actor: St.Bin, - borderRadiusRaw = settings().global_rounded_corner_settings.border_radius, - shadow = settings().focused_shadow, - padding = settings().global_rounded_corner_settings.padding, -) { - const {left, right, top, bottom} = padding; - - // Increase border_radius when smoothing is on - let borderRadius = borderRadiusRaw; - if (settings().global_rounded_corner_settings !== null) { - borderRadius *= - 1.0 + settings().global_rounded_corner_settings.smoothing; - } - - // If there are two monitors with different scale factors, the scale of - // the window may be different from the scale that has to be applied in - // the css, so we have to adjust the scale factor accordingly. - - const originalScale = St.ThemeContext.get_for_stage( - global.stage as Clutter.Stage, - ).scaleFactor; - - const scale = WindowScaleFactor(win) / originalScale; - - actor.style = `padding: ${SHADOW_PADDING * scale}px;`; - - const child = actor.firstChild as St.Bin; - - child.style = - win.maximizedHorizontally || win.maximizedVertically || win.fullscreen - ? 'opacity: 0;' - : `background: white; - border-radius: ${borderRadius * scale}px; - ${types.box_shadow_css(shadow, scale)}; - margin: ${top * scale}px - ${right * scale}px - ${bottom * scale}px - ${left * scale}px;`; - - child.queue_redraw(); -} - /** Traverse all windows, and check if they should have rounded corners. */ function refreshEffectState() { for (const actor of global.get_window_actors()) { @@ -371,20 +233,20 @@ function refreshEffectState() { * * @param actor - The window actor to refresh the shadow for. */ -function refreshShadow(actor: ExtensionsWindowActor) { +function refreshShadow(actor: RoundedWindowActor) { const win = actor.metaWindow; - const shadow = actor.__rwcRoundedWindowInfo?.shadow; + const shadow = actor.rwcCustomData?.shadow; if (!shadow) { return; } const shadowSettings = win.appears_focused - ? settings().focused_shadow - : settings().unfocused_shadow; + ? getPref('focused-shadow') + : getPref('unfocused-shadow'); - const {border_radius, padding} = getRoundedCornersCfg(win); + const {borderRadius, padding} = getRoundedCornersCfg(win); - updateShadowActorStyle(win, shadow, border_radius, shadowSettings, padding); + updateShadowActorStyle(win, shadow, borderRadius, shadowSettings, padding); } /** Refresh the style of all shadow actors */ @@ -399,10 +261,10 @@ function refreshAllShadows() { * * @param actor - The window actor to refresh the rounded corners settings for. */ -function refreshRoundedCorners(actor: ExtensionsWindowActor): void { +function refreshRoundedCorners(actor: RoundedWindowActor): void { const win = actor.metaWindow; - const windowInfo = actor.__rwcRoundedWindowInfo; + const windowInfo = actor.rwcCustomData; const effect = getRoundedCornersEffect(actor); if (!(effect && windowInfo)) { @@ -422,12 +284,12 @@ function refreshRoundedCorners(actor: ExtensionsWindowActor): void { // When window size is changed, update uniforms for corner rounding shader. effect.update_uniforms( - WindowScaleFactor(win), + windowScaleFactor(win), cfg, computeBounds(actor, windowContentOffset), { - width: settings().border_width, - color: settings().border_color, + width: getPref('border-width'), + color: getPref('border-color'), }, ); diff --git a/src/manager/event_manager.ts b/src/manager/event_manager.ts index 577e10d..e707049 100644 --- a/src/manager/event_manager.ts +++ b/src/manager/event_manager.ts @@ -3,18 +3,16 @@ * effect. See {@link enableEffect} for more information. */ -import {connections} from '../utils/connections.js'; -import {_log} from '../utils/log.js'; -import {settings} from '../utils/settings.js'; +import {logDebug} from '../utils/log.js'; +import {prefs} from '../utils/settings.js'; import * as handlers from './event_handlers.js'; +import type GObject from 'gi://GObject'; import type Gio from 'gi://Gio'; import type Meta from 'gi://Meta'; import type Shell from 'gi://Shell'; -import type {SchemasKeys} from '../utils/settings.js'; -import type {ExtensionsWindowActor} from '../utils/types.js'; - -const connectionManager = connections.get(); +import type {SchemaKey} from '../utils/settings.js'; +import type {RoundedWindowActor} from '../utils/types.js'; /** * The rounded corners effect has to perform some actions when differen events @@ -26,24 +24,21 @@ const connectionManager = connections.get(); */ export function enableEffect() { // Update the effect when settings are changed. - connectionManager.connect( - settings().g_settings, - 'changed', - (_: Gio.Settings, key: string) => - handlers.onSettingsChanged(key as SchemasKeys), + connect(prefs, 'changed', (_: Gio.Settings, key: string) => + handlers.onSettingsChanged(key as SchemaKey), ); const wm = global.windowManager; // Add the effect to all windows when the extension is enabled. const windowActors = global.get_window_actors(); - _log(`Initial window count: ${windowActors.length}`); + logDebug(`Initial window count: ${windowActors.length}`); for (const actor of windowActors) { applyEffectTo(actor); } // Add the effect to new windows when they are opened. - connectionManager.connect( + connect( global.display, 'window-created', (_: Meta.Display, win: Meta.Window) => { @@ -63,32 +58,22 @@ export function enableEffect() { ); // Window minimized. - connectionManager.connect( - wm, - 'minimize', - (_: Shell.WM, actor: Meta.WindowActor) => handlers.onMinimize(actor), + connect(wm, 'minimize', (_: Shell.WM, actor: Meta.WindowActor) => + handlers.onMinimize(actor), ); // Window unminimized. - connectionManager.connect( - wm, - 'unminimize', - (_: Shell.WM, actor: Meta.WindowActor) => handlers.onUnminimize(actor), + connect(wm, 'unminimize', (_: Shell.WM, actor: Meta.WindowActor) => + handlers.onUnminimize(actor), ); // When closing the window, remove the effect from it. - connectionManager.connect( - wm, - 'destroy', - (_: Shell.WM, actor: Meta.WindowActor) => removeEffectFrom(actor), + connect(wm, 'destroy', (_: Shell.WM, actor: Meta.WindowActor) => + removeEffectFrom(actor), ); // When windows are restacked, the order of shadow actors as well. - connectionManager.connect( - global.display, - 'restacked', - handlers.onRestacked, - ); + connect(global.display, 'restacked', handlers.onRestacked); } /** Disable the effect for all windows. */ @@ -97,7 +82,44 @@ export function disableEffect() { removeEffectFrom(actor); } - connectionManager?.disconnect_all(); + disconnectAll(); +} + +const connections: {object: GObject.Object; id: number}[] = []; + +/** + * Connect a callback to an object signal and add it to the list of all + * connections. This allows to easily disconnect all signals when removing + * the effect. + * + * @param object - The object to connect the callback to. + * @param signal - The name of the signal. + * @param callback - The function to connect to the signal. + */ +function connect( + object: GObject.Object, + signal: string, + // Signal callbacks can have any return args and return types. + // biome-ignore lint/suspicious/noExplicitAny: + callback: (...args: any[]) => any, +) { + connections.push({ + object: object, + id: object.connect(signal, callback), + }); +} + +/** + * Disconnect all connected signals from all actors or a specific object. + * + * @param object - If object is provided, only disconnect signals from it. + */ +function disconnectAll(object?: GObject.Object) { + for (const connection of connections) { + if (object === undefined || connection.object === object) { + connection.object.disconnect(connection.id); + } + } } /** @@ -109,7 +131,7 @@ export function disableEffect() { * * @param actor - The window actor to apply the effect to. */ -function applyEffectTo(actor: ExtensionsWindowActor) { +function applyEffectTo(actor: RoundedWindowActor) { // In wayland sessions, the surface actor of XWayland clients is sometimes // not ready when the window is created. In this case, we wait until it is // ready before applying the effect. @@ -133,20 +155,18 @@ function applyEffectTo(actor: ExtensionsWindowActor) { // that? I have no idea. But without that, weird bugs can happen. For // example, when using Dash to Dock, all opened windows will be invisible // *unless they are pinned in the dock*. So yeah, GNOME is magic. - connectionManager.connect(actor, 'notify::size', () => - handlers.onSizeChanged(actor), - ); - connectionManager.connect(texture, 'size-changed', () => { + connect(actor, 'notify::size', () => handlers.onSizeChanged(actor)); + connect(texture, 'size-changed', () => { handlers.onSizeChanged(actor); }); // Window focus changed. - connectionManager.connect(actor.metaWindow, 'notify::appears-focused', () => + connect(actor.metaWindow, 'notify::appears-focused', () => handlers.onFocusChanged(actor), ); // Workspace or monitor of the window changed. - connectionManager.connect(actor.metaWindow, 'workspace-changed', () => { + connect(actor.metaWindow, 'workspace-changed', () => { handlers.onFocusChanged(actor); }); @@ -158,11 +178,9 @@ function applyEffectTo(actor: ExtensionsWindowActor) { * * @param actor - The window actor to remove the effect from. */ -function removeEffectFrom(actor: ExtensionsWindowActor) { - if (connectionManager) { - connectionManager.disconnect_all(actor); - connectionManager.disconnect_all(actor.metaWindow); - } +function removeEffectFrom(actor: RoundedWindowActor) { + disconnectAll(actor); + disconnectAll(actor.metaWindow); handlers.onRemoveEffect(actor); } diff --git a/src/manager/utils.ts b/src/manager/utils.ts new file mode 100644 index 0000000..b570626 --- /dev/null +++ b/src/manager/utils.ts @@ -0,0 +1,316 @@ +/** @file Provides various utility functions used withing signal handling code. */ + +import Gio from 'gi://Gio'; +import Meta from 'gi://Meta'; +import St from 'gi://St'; + +import {boxShadowCss} from '../utils/box_shadow.js'; +import { + APP_SHADOWS, + ROUNDED_CORNERS_EFFECT, + SHADOW_PADDING, +} from '../utils/constants.js'; +import {readFile} from '../utils/file.js'; +import {logDebug} from '../utils/log.js'; +import {getPref} from '../utils/settings.js'; + +import type Clutter from 'gi://Clutter'; +import type {RoundedCornersEffect} from '../effect/rounded_corners_effect.js'; +import type {Bounds, RoundedCornerSettings} from '../utils/types.js'; + +/** + * Get the actor that rounded corners should be applied to. + * In Wayland, the effect is applied to WindowActor, but in X11, it is applied + * to WindowActor.first_child. + * + * @param actor - The window actor to unwrap. + * @returns The correct actor that the effect should be applied to. + */ +export function unwrapActor(actor: Meta.WindowActor): Clutter.Actor | null { + const type = actor.metaWindow.get_client_type(); + return type === Meta.WindowClientType.X11 ? actor.get_first_child() : actor; +} + +/** + * Get the correct rounded corner setting for a window (custom settings if a + * window has custom overrides, global settings otherwise). + * + * @param win - The window to get the settings for. + * @returns The matching settings object. + */ +export function getRoundedCornersCfg(win: Meta.Window): RoundedCornerSettings { + const globalCfg = getPref('global-rounded-corner-settings'); + const customCfgList = getPref('custom-rounded-corner-settings'); + + const wmClass = win.get_wm_class_instance(); + if ( + wmClass == null || + !customCfgList[wmClass] || + !customCfgList[wmClass].enabled + ) { + return globalCfg; + } + + return customCfgList[wmClass]; +} + +// Weird TypeScript magic :) +type RoundedCornersEffectType = InstanceType; + +/** + * Get the Clutter.Effect object for the rounded corner effect of a specific + * window. + * + * @param actor - The window actor to get the effect for. + * @returns The corresponding Clutter.Effect object. + */ +export function getRoundedCornersEffect( + actor: Meta.WindowActor, +): RoundedCornersEffectType | null { + const win = actor.metaWindow; + const name = ROUNDED_CORNERS_EFFECT; + return win.get_client_type() === Meta.WindowClientType.X11 + ? (actor.firstChild.get_effect(name) as RoundedCornersEffectType) + : (actor.get_effect(name) as RoundedCornersEffectType); +} + +/** + * Get the scaling factor of a window. + * + * @param win - The window to get the scaling factor for. + * @returns The scaling factor of the window. + */ +export function windowScaleFactor(win: Meta.Window) { + // When fractional scaling is enabled, always return 1 + const features = Gio.Settings.new('org.gnome.mutter').get_strv( + 'experimental-features', + ); + if ( + Meta.is_wayland_compositor() && + features.includes('scale-monitor-framebuffer') + ) { + return 1; + } + + const monitorIndex = win.get_monitor(); + return global.display.get_monitor_scale(monitorIndex); +} + +/** Compute outer bounds for rounded corners of a window + * + * @param actor - The window actor to compute the bounds for. + * @param [x, y, width, height] - The content offsets of the window actor. + */ +export function computeBounds( + actor: Meta.WindowActor, + [x, y, width, height]: [number, number, number, number], +): Bounds { + const bounds = { + x1: x + 1, + y1: y + 1, + x2: x + actor.width + width, + y2: y + actor.height + height, + }; + + // Kitty draws its window decoration by itself, so we need to manually + // clip its shadow and recompute the outer bounds for it. + if (getPref('tweak-kitty-terminal')) { + if ( + actor.metaWindow.get_client_type() === + Meta.WindowClientType.WAYLAND && + actor.metaWindow.get_wm_class_instance() === 'kitty' + ) { + const [x1, y1, x2, y2] = APP_SHADOWS.kitty; + const scale = windowScaleFactor(actor.metaWindow); + bounds.x1 += x1 * scale; + bounds.y1 += y1 * scale; + bounds.x2 -= x2 * scale; + bounds.y2 -= y2 * scale; + } + } + + return bounds; +} + +/** + * Compute the offset of actual window contents from the entire window buffer. + * + * @param window - The window to compute the offset for. + * @returns The content offsets of the window (x, y, width, height). + */ +export function computeWindowContentsOffset( + window: Meta.Window, +): [number, number, number, number] { + const bufferRect = window.get_buffer_rect(); + const frameRect = window.get_frame_rect(); + return [ + frameRect.x - bufferRect.x, + frameRect.y - bufferRect.y, + frameRect.width - bufferRect.width, + frameRect.height - bufferRect.height, + ]; +} + +/** + * Compute the offset of the shadow actor for a window. + * + * @param actor - The window actor to compute the offset for. + * @param [offsetX, offsetY, offsetWidth, offsetHeight] - The content offsets of the window actor. + */ +export function computeShadowActorOffset( + actor: Meta.WindowActor, + [offsetX, offsetY, offsetWidth, offsetHeight]: [ + number, + number, + number, + number, + ], +): number[] { + const win = actor.metaWindow; + const shadowPadding = SHADOW_PADDING * windowScaleFactor(win); + + return [ + offsetX - shadowPadding, + offsetY - shadowPadding, + 2 * shadowPadding + offsetWidth, + 2 * shadowPadding + offsetHeight, + ]; +} + +/** Update the CSS style of a shadow actor + * + * @param win - The window to update the style for. + * @param actor - The shadow actor to update the style for. + * @param borderRadius - The border radius of the shadow actor. + * @param shadow - The shadow settings for the window. + * @param padding - The padding of the shadow actor. + */ +export function updateShadowActorStyle( + win: Meta.Window, + actor: St.Bin, + borderRadius = getPref('global-rounded-corner-settings').borderRadius, + shadow = getPref('focused-shadow'), + padding = getPref('global-rounded-corner-settings').padding, +) { + const {left, right, top, bottom} = padding; + + // Increase border_radius when smoothing is on + let adjustedBorderRadius = borderRadius; + if (getPref('global-rounded-corner-settings') !== null) { + adjustedBorderRadius *= + 1.0 + getPref('global-rounded-corner-settings').smoothing; + } + + // If there are two monitors with different scale factors, the scale of + // the window may be different from the scale that has to be applied in + // the css, so we have to adjust the scale factor accordingly. + + const originalScale = St.ThemeContext.get_for_stage( + global.stage as Clutter.Stage, + ).scaleFactor; + + const scale = windowScaleFactor(win) / originalScale; + + actor.style = `padding: ${SHADOW_PADDING * scale}px;`; + + const child = actor.firstChild as St.Bin; + + child.style = + win.maximizedHorizontally || win.maximizedVertically || win.fullscreen + ? 'opacity: 0;' + : `background: white; + border-radius: ${adjustedBorderRadius * scale}px; + ${boxShadowCss(shadow, scale)}; + margin: ${top * scale}px + ${right * scale}px + ${bottom * scale}px + ${left * scale}px;`; + + child.queue_redraw(); +} + +/** + * Check whether a window should have rounded corners. + * + * @param win - The window to check. + * @returns Whether the window should have rounded corners. + */ +export function shouldEnableEffect( + win: Meta.Window & {_appType?: AppType}, +): boolean { + // Skip rounded corners for the DING (Desktop Icons NG) extension. + // + // https://extensions.gnome.org/extension/2087/desktop-icons-ng-ding/ + if (win.gtkApplicationId === 'com.rastersoft.ding') { + return false; + } + + // Skip blacklisted applications. + const wmClass = win.get_wm_class_instance(); + if (wmClass == null) { + logDebug(`Warning: wm_class_instance of ${win}: ${win.title} is null`); + return false; + } + if (getPref('blacklist').includes(wmClass)) { + return false; + } + + // Only apply the effect to normal windows (skip menus, tooltips, etc.) + if ( + win.windowType !== Meta.WindowType.NORMAL && + win.windowType !== Meta.WindowType.DIALOG && + win.windowType !== Meta.WindowType.MODAL_DIALOG + ) { + return false; + } + + // Skip libhandy/libadwaita applications according to settings. + const appType = win._appType ?? getAppType(win); + win._appType = appType; // Cache the result. + logDebug(`Check Type of window:${win.title} => ${appType}`); + + if (getPref('skip-libadwaita-app') && appType === 'LibAdwaita') { + return false; + } + if (getPref('skip-libhandy-app') && appType === 'LibHandy') { + return false; + } + + // Skip maximized/fullscreen windows according to settings. + const maximized = win.maximizedHorizontally || win.maximizedVertically; + const fullscreen = win.fullscreen; + const cfg = getRoundedCornersCfg(win); + return ( + !(maximized || fullscreen) || + (maximized && cfg.keepRoundedCorners.maximized) || + (fullscreen && cfg.keepRoundedCorners.fullscreen) + ); +} + +type AppType = 'LibAdwaita' | 'LibHandy' | 'Other'; + +/** + * Get the type of the application (LibHandy/LibAdwaita/Other). + * + * @param win - The window to get the type of. + * @returns the type of the application. + */ +function getAppType(win: Meta.Window): AppType { + try { + // May throw a permission error. + const contents = readFile(`/proc/${win.get_pid()}/maps`); + + if (contents.includes('libhandy-1.so')) { + return 'LibHandy'; + } + + if (contents.includes('libadwaita-1.so')) { + return 'LibAdwaita'; + } + + return 'Other'; + } catch (e) { + logError(e); + return 'Other'; + } +} diff --git a/src/preferences/pages/blacklist.ts b/src/preferences/pages/blacklist.ts index 85c7b88..d0e3382 100644 --- a/src/preferences/pages/blacklist.ts +++ b/src/preferences/pages/blacklist.ts @@ -1,17 +1,21 @@ import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import type Gtk from 'gi://Gtk'; -import {settings} from '../../utils/settings.js'; +import {getPref, setPref} from '../../utils/settings.js'; import type {AppRowCallbacks, AppRowClass} from '../widgets/app_row.js'; import {BlacklistRow} from '../widgets/blacklist_row.js'; import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; -import {uri} from '../../utils/io.js'; export const BlackList = GObject.registerClass( { - Template: uri(import.meta.url, 'blacklist.ui'), + Template: GLib.uri_resolve_relative( + import.meta.url, + 'blacklist.ui', + GLib.UriFlags.NONE, + ), GTypeName: 'PrefsBlacklist', InternalChildren: ['blacklist_group'], }, @@ -22,7 +26,7 @@ export const BlackList = GObject.registerClass( constructor() { super(); - this.blacklist = settings().black_list; + this.blacklist = getPref('blacklist'); for (const title of this.blacklist) { this.add_window(undefined, title); @@ -43,7 +47,7 @@ export const BlackList = GObject.registerClass( private delete_row(row: AppRowClass) { this.blacklist.splice(this.blacklist.indexOf(row.title), 1); - settings().black_list = this.blacklist; + setPref('blacklist', this.blacklist); this._blacklist_group.remove(row); } @@ -67,7 +71,7 @@ export const BlackList = GObject.registerClass( this.blacklist.splice(old_id, 1, new_title); } - settings().black_list = this.blacklist; + setPref('blacklist', this.blacklist); return true; } diff --git a/src/preferences/pages/custom.ts b/src/preferences/pages/custom.ts index b0d369c..981cb17 100644 --- a/src/preferences/pages/custom.ts +++ b/src/preferences/pages/custom.ts @@ -1,10 +1,10 @@ import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import type Gtk from 'gi://Gtk'; import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; -import {connections} from '../../utils/connections.js'; -import {settings} from '../../utils/settings.js'; +import {getPref, setPref} from '../../utils/settings.js'; import type {AppRowCallbacks, AppRowClass} from '../widgets/app_row.js'; import { CustomEffectRow, @@ -12,23 +12,26 @@ import { } from '../widgets/customeffect_row.js'; import type {PaddingsRowClass} from '../widgets/paddings_row.js'; -import {uri} from '../../utils/io.js'; -import type {CustomRoundedCornersCfg} from '../../utils/types.js'; +import type {CustomRoundedCornerSettings} from '../../utils/types.js'; export const Custom = GObject.registerClass( { - Template: uri(import.meta.url, 'custom.ui'), + Template: GLib.uri_resolve_relative( + import.meta.url, + 'custom.ui', + GLib.UriFlags.NONE, + ), GTypeName: 'PrefsCustom', InternalChildren: ['custom_group'], }, class extends Adw.PreferencesPage { private declare _custom_group: Adw.PreferencesGroup; - private declare _settings_cfg: CustomRoundedCornersCfg; + private declare _settings_cfg: CustomRoundedCornerSettings; constructor() { super(); - this._settings_cfg = settings().custom_rounded_corner_settings; + this._settings_cfg = getPref('custom-rounded-corner-settings'); for (const title in this._settings_cfg) { this.add_window(undefined, title); @@ -52,8 +55,7 @@ export const Custom = GObject.registerClass( private delete_row(row: AppRowClass) { delete this._settings_cfg[row.subtitle]; - settings().custom_rounded_corner_settings = this._settings_cfg; - this.disconnect_row(row); + setPref('custom-rounded-corner-settings', this._settings_cfg); this._custom_group.remove(row); } @@ -75,139 +77,124 @@ export const Custom = GObject.registerClass( } if (old_title === '') { - this._settings_cfg[new_title] = - settings().global_rounded_corner_settings; + this._settings_cfg[new_title] = getPref( + 'global-rounded-corner-settings', + ); } else { const cfg = this._settings_cfg[old_title]; delete this._settings_cfg[old_title]; this._settings_cfg[new_title] = cfg; - this.disconnect_row(row); } this.setup_row(row, new_title); - settings().custom_rounded_corner_settings = this._settings_cfg; + setPref('custom-rounded-corner-settings', this._settings_cfg); return true; } private setup_row(row: AppRowClass, title: string) { - const c = connections.get(); if (!(row instanceof CustomEffectRowClass)) { return; } const r = row as CustomEffectRowClass; - c.connect(r, 'notify::subtitle', (row: CustomEffectRowClass) => { + r.connect('notify::subtitle', (row: CustomEffectRowClass) => { row.check_state(); }); r.enabled_row.set_active(this._settings_cfg[title].enabled); - c.connect(r.enabled_row, 'notify::active', (row: Adw.SwitchRow) => { + r.enabled_row.connect('notify::active', (row: Adw.SwitchRow) => { r.check_state(); this._settings_cfg[title].enabled = row.get_active(); - settings().custom_rounded_corner_settings = this._settings_cfg; + setPref('custom-rounded-corner-settings', this._settings_cfg); + }); + r.corner_radius.set_value(this._settings_cfg[title].borderRadius); + r.corner_radius.connect('value-changed', (adj: Gtk.Adjustment) => { + this._settings_cfg[title].borderRadius = adj.get_value(); + setPref('custom-rounded-corner-settings', this._settings_cfg); }); - r.corner_radius.set_value(this._settings_cfg[title].border_radius); - c.connect( - r.corner_radius, - 'value-changed', - (adj: Gtk.Adjustment) => { - this._settings_cfg[title].border_radius = adj.get_value(); - settings().custom_rounded_corner_settings = - this._settings_cfg; - }, - ); r.corner_smoothing.set_value(this._settings_cfg[title].smoothing); - c.connect( - r.corner_smoothing, + r.corner_smoothing.connect( 'value-changed', (adj: Gtk.Adjustment) => { this._settings_cfg[title].smoothing = adj.get_value(); - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); r.keep_for_maximized.set_active( - this._settings_cfg[title].keep_rounded_corners.maximized, + this._settings_cfg[title].keepRoundedCorners.maximized, ); - c.connect( - r.keep_for_maximized, + r.keep_for_maximized.connect( 'notify::active', (row: Adw.SwitchRow) => { - this._settings_cfg[title].keep_rounded_corners.maximized = + this._settings_cfg[title].keepRoundedCorners.maximized = row.get_active(); - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); r.keep_for_fullscreen.set_active( - this._settings_cfg[title].keep_rounded_corners.fullscreen, + this._settings_cfg[title].keepRoundedCorners.fullscreen, ); - c.connect( - r.keep_for_fullscreen, + r.keep_for_fullscreen.connect( 'notify::active', (row: Adw.SwitchRow) => { - this._settings_cfg[title].keep_rounded_corners.fullscreen = + this._settings_cfg[title].keepRoundedCorners.fullscreen = row.get_active(); - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); r.paddings.paddingTop = this._settings_cfg[title].padding.top; - c.connect( - r.paddings, + r.paddings.connect( 'notify::padding-top', (row: PaddingsRowClass) => { this._settings_cfg[title].padding.top = row.paddingTop; - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); r.paddings.paddingBottom = this._settings_cfg[title].padding.bottom; - c.connect( - r.paddings, + r.paddings.connect( 'notify::padding-bottom', (row: PaddingsRowClass) => { this._settings_cfg[title].padding.bottom = row.paddingBottom; - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); r.paddings.paddingStart = this._settings_cfg[title].padding.left; - c.connect( - r.paddings, + r.paddings.connect( 'notify::padding-start', (row: PaddingsRowClass) => { this._settings_cfg[title].padding.left = row.paddingStart; - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); r.paddings.paddingEnd = this._settings_cfg[title].padding.right; - c.connect( - r.paddings, + r.paddings.connect( 'notify::padding-end', (row: PaddingsRowClass) => { this._settings_cfg[title].padding.right = row.paddingEnd; - settings().custom_rounded_corner_settings = - this._settings_cfg; + setPref( + 'custom-rounded-corner-settings', + this._settings_cfg, + ); }, ); } - - private disconnect_row(row: AppRowClass) { - const c = connections.get(); - if (!(row instanceof CustomEffectRowClass)) { - return; - } - const r = row as CustomEffectRowClass; - - c.disconnect_all(r); - c.disconnect_all(r.enabled_row); - c.disconnect_all(r.corner_radius); - c.disconnect_all(r.corner_smoothing); - c.disconnect_all(r.keep_for_maximized); - c.disconnect_all(r.keep_for_fullscreen); - c.disconnect_all(r.paddings); - } }, ); diff --git a/src/preferences/pages/general.ts b/src/preferences/pages/general.ts index 73c027c..7dbd109 100644 --- a/src/preferences/pages/general.ts +++ b/src/preferences/pages/general.ts @@ -1,22 +1,25 @@ import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gdk from 'gi://Gdk'; import Gio from 'gi://Gio'; import type Gtk from 'gi://Gtk'; -import {connections} from '../../utils/connections.js'; -import {settings} from '../../utils/settings.js'; +import {bindPref, getPref, setPref} from '../../utils/settings.js'; import {EditShadowPage} from '../widgets/edit_shadow_page.js'; import type {PaddingsRowClass} from '../widgets/paddings_row.js'; import {ResetPage} from '../widgets/reset_page.js'; import '../widgets/paddings_row.js'; -import {uri} from '../../utils/io.js'; -import type {RoundedCornersCfg} from '../../utils/types.js'; +import type {RoundedCornerSettings} from '../../utils/types.js'; export const General = GObject.registerClass( { - Template: uri(import.meta.url, 'general.ui'), + Template: GLib.uri_resolve_relative( + import.meta.url, + 'general.ui', + GLib.UriFlags.NONE, + ), GTypeName: 'PrefsGeneral', InternalChildren: [ 'skip_libadwaita', @@ -47,28 +50,27 @@ export const General = GObject.registerClass( private declare _right_click_menu: Adw.SwitchRow; private declare _enable_log: Adw.SwitchRow; - private declare _cfg: RoundedCornersCfg; + private declare _cfg: RoundedCornerSettings; constructor() { super(); - this._cfg = settings().global_rounded_corner_settings; - const c = connections.get(); + this._cfg = getPref('global-rounded-corner-settings'); - settings().bind( + bindPref( 'skip-libadwaita-app', this._skip_libadwaita, 'active', Gio.SettingsBindFlags.DEFAULT, ); - settings().bind( + bindPref( 'skip-libhandy-app', this._skip_libhandy, 'active', Gio.SettingsBindFlags.DEFAULT, ); - settings().bind( + bindPref( 'border-width', this._border_width, 'value', @@ -77,35 +79,32 @@ export const General = GObject.registerClass( const color = new Gdk.RGBA(); [color.red, color.green, color.blue, color.alpha] = - settings().border_color; + getPref('border-color'); this._border_color.set_rgba(color); - c.connect( - this._border_color, + this._border_color.connect( 'notify::rgba', (btn: Gtk.ColorDialogButton) => { const color = btn.get_rgba(); - settings().border_color = [ + setPref('border-color', [ color.red, color.green, color.blue, color.alpha, - ]; + ]); }, ); - this._corner_radius.set_value(this._cfg.border_radius); - c.connect( - this._corner_radius, + this._corner_radius.set_value(this._cfg.borderRadius); + this._corner_radius.connect( 'value-changed', (adj: Gtk.Adjustment) => { - this._cfg.border_radius = adj.get_value(); + this._cfg.borderRadius = adj.get_value(); this._update_global_config(); }, ); this._corner_smoothing.set_value(this._cfg.smoothing); - c.connect( - this._corner_smoothing, + this._corner_smoothing.connect( 'value-changed', (adj: Gtk.Adjustment) => { this._cfg.smoothing = adj.get_value(); @@ -114,34 +113,30 @@ export const General = GObject.registerClass( ); this._keep_for_maximized.set_active( - this._cfg.keep_rounded_corners.maximized, + this._cfg.keepRoundedCorners.maximized, ); - c.connect( - this._keep_for_maximized, + this._keep_for_maximized.connect( 'notify::active', (swtch: Adw.SwitchRow) => { - this._cfg.keep_rounded_corners.maximized = - swtch.get_active(); + this._cfg.keepRoundedCorners.maximized = swtch.get_active(); this._update_global_config(); }, ); this._keep_for_fullscreen.set_active( - this._cfg.keep_rounded_corners.fullscreen, + this._cfg.keepRoundedCorners.fullscreen, ); - c.connect( - this._keep_for_fullscreen, + this._keep_for_fullscreen.connect( 'notify::active', (swtch: Adw.SwitchRow) => { - this._cfg.keep_rounded_corners.fullscreen = + this._cfg.keepRoundedCorners.fullscreen = swtch.get_active(); this._update_global_config(); }, ); this._paddings.paddingTop = this._cfg.padding.top; - c.connect( - this._paddings, + this._paddings.connect( 'notify::padding-top', (row: PaddingsRowClass) => { this._cfg.padding.top = row.paddingTop; @@ -150,8 +145,7 @@ export const General = GObject.registerClass( ); this._paddings.paddingBottom = this._cfg.padding.bottom; - c.connect( - this._paddings, + this._paddings.connect( 'notify::padding-bottom', (row: PaddingsRowClass) => { this._cfg.padding.bottom = row.paddingBottom; @@ -160,8 +154,7 @@ export const General = GObject.registerClass( ); this._paddings.paddingStart = this._cfg.padding.left; - c.connect( - this._paddings, + this._paddings.connect( 'notify::padding-start', (row: PaddingsRowClass) => { this._cfg.padding.left = row.paddingStart; @@ -170,8 +163,7 @@ export const General = GObject.registerClass( ); this._paddings.paddingEnd = this._cfg.padding.right; - c.connect( - this._paddings, + this._paddings.connect( 'notify::padding-end', (row: PaddingsRowClass) => { this._cfg.padding.right = row.paddingEnd; @@ -179,21 +171,21 @@ export const General = GObject.registerClass( }, ); - settings().bind( + bindPref( 'tweak-kitty-terminal', this._tweak_kitty, 'active', Gio.SettingsBindFlags.DEFAULT, ); - settings().bind( + bindPref( 'enable-preferences-entry', this._right_click_menu, 'active', Gio.SettingsBindFlags.DEFAULT, ); - settings().bind( + bindPref( 'debug-mode', this._enable_log, 'active', @@ -212,7 +204,7 @@ export const General = GObject.registerClass( } private _update_global_config() { - settings().global_rounded_corner_settings = this._cfg; + setPref('global-rounded-corner-settings', this._cfg); } }, ); diff --git a/src/preferences/widgets/app_row.ts b/src/preferences/widgets/app_row.ts index fe4b4bc..6b4c5bd 100644 --- a/src/preferences/widgets/app_row.ts +++ b/src/preferences/widgets/app_row.ts @@ -3,7 +3,6 @@ import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk'; import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; -import {connections} from '../../utils/connections.js'; import {onPicked, pick} from '../window_picker/client.js'; export class AppRowClass extends Adw.ExpanderRow { @@ -47,18 +46,13 @@ export class AppRowClass extends Adw.ExpanderRow { this.add_css_class('property'); this.set_title(_('Expand this row, to pick a window')); - const c = connections.get(); - - c.connect(this.remove_btn, 'clicked', () => { - connections.get().disconnect_all(this.remove_btn); - connections.get().disconnect_all(this.apply_btn); - connections.get().disconnect_all(this.pick_btn); + this.remove_btn.connect('clicked', () => { this.on_delete(); }); - c.connect(this.pick_btn, 'clicked', () => { + this.pick_btn.connect('clicked', () => { this.on_pick(this.wm_class_entry); }); - c.connect(this.apply_btn, 'clicked', () => { + this.apply_btn.connect('clicked', () => { this.on_title_change(this.wm_class_entry); }); } diff --git a/src/preferences/widgets/edit_shadow_page.ts b/src/preferences/widgets/edit_shadow_page.ts index a8b6c51..9a37c20 100644 --- a/src/preferences/widgets/edit_shadow_page.ts +++ b/src/preferences/widgets/edit_shadow_page.ts @@ -1,17 +1,21 @@ import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gio from 'gi://Gio'; import Gtk from 'gi://Gtk'; -import {settings} from '../../utils/settings.js'; -import {box_shadow_css} from '../../utils/types.js'; +import {boxShadowCss} from '../../utils/box_shadow.js'; +import {getPref, setPref} from '../../utils/settings.js'; -import {uri} from '../../utils/io.js'; import type {BoxShadow} from '../../utils/types.js'; export const EditShadowPage = GObject.registerClass( { - Template: uri(import.meta.url, 'edit-shadow-page.ui'), + Template: GLib.uri_resolve_relative( + import.meta.url, + 'edit-shadow-page.ui', + GLib.UriFlags.NONE, + ), GTypeName: 'EditShadowPage', InternalChildren: [ 'focused_shadow_preview', @@ -65,8 +69,8 @@ export const EditShadowPage = GObject.registerClass( this.unfocus_provider = new Gtk.CssProvider(); this.focus_provider = new Gtk.CssProvider(); this.backgroud_provider = new Gtk.CssProvider(); - this.focused_shadow = settings().focused_shadow; - this.unfocused_shadow = settings().unfocused_shadow; + this.focused_shadow = getPref('focused-shadow'); + this.unfocused_shadow = getPref('unfocused-shadow'); const style_manager = new Adw.StyleManager(); style_manager.connect('notify::dark', manager => { @@ -103,67 +107,64 @@ export const EditShadowPage = GObject.registerClass( private update_widget() { this._focused_horizontal_offset.set_value( - this.focused_shadow.horizontal_offset, + this.focused_shadow.horizontalOffset, ); this._focused_vertical_offset.set_value( - this.focused_shadow.vertical_offset, - ); - this._focused_blur_radius.set_value( - this.focused_shadow.blur_offset, + this.focused_shadow.verticalOffset, ); + this._focused_blur_radius.set_value(this.focused_shadow.blurOffset); this._focused_spread_radius.set_value( - this.focused_shadow.spread_radius, + this.focused_shadow.spreadRadius, ); this._focused_opacity.set_value(this.focused_shadow.opacity); this._unfocused_horizontal_offset.set_value( - this.unfocused_shadow.horizontal_offset, + this.unfocused_shadow.horizontalOffset, ); this._unfocused_vertical_offset.set_value( - this.unfocused_shadow.vertical_offset, + this.unfocused_shadow.verticalOffset, ); this._unfocused_blur_radius.set_value( - this.unfocused_shadow.blur_offset, + this.unfocused_shadow.blurOffset, ); this._unfocused_spread_radius.set_value( - this.unfocused_shadow.spread_radius, + this.unfocused_shadow.spreadRadius, ); this._unfocused_opacity.set_value(this.unfocused_shadow.opacity); } private update_cfg() { const focused_shadow: BoxShadow = { - vertical_offset: this._focused_vertical_offset.get_value(), - horizontal_offset: this._focused_horizontal_offset.get_value(), - blur_offset: this._focused_blur_radius.get_value(), - spread_radius: this._focused_spread_radius.get_value(), + verticalOffset: this._focused_vertical_offset.get_value(), + horizontalOffset: this._focused_horizontal_offset.get_value(), + blurOffset: this._focused_blur_radius.get_value(), + spreadRadius: this._focused_spread_radius.get_value(), opacity: this._focused_opacity.get_value(), }; this.focused_shadow = focused_shadow; const unfocused_shadow: BoxShadow = { - vertical_offset: this._unfocused_vertical_offset.get_value(), - horizontal_offset: - this._unfocused_horizontal_offset.get_value(), - blur_offset: this._unfocused_blur_radius.get_value(), - spread_radius: this._unfocused_spread_radius.get_value(), + verticalOffset: this._unfocused_vertical_offset.get_value(), + horizontalOffset: this._unfocused_horizontal_offset.get_value(), + blurOffset: this._unfocused_blur_radius.get_value(), + spreadRadius: this._unfocused_spread_radius.get_value(), opacity: this._unfocused_opacity.get_value(), }; this.unfocused_shadow = unfocused_shadow; // Store into settings - settings().unfocused_shadow = this.unfocused_shadow; - settings().focused_shadow = this.focused_shadow; + setPref('unfocused-shadow', this.unfocused_shadow); + setPref('focused-shadow', this.focused_shadow); } private update_style() { const gen_style = (normal: BoxShadow, hover: BoxShadow) => `.preview { transition: box-shadow 200ms; - ${box_shadow_css(normal)}; + ${boxShadowCss(normal)}; border-radius: 12px; } .preview:hover { - ${box_shadow_css(hover)}; + ${boxShadowCss(hover)}; }`; type A = Gtk.CssProvider & { diff --git a/src/preferences/widgets/paddings_row.ts b/src/preferences/widgets/paddings_row.ts index a4b4042..bd20330 100644 --- a/src/preferences/widgets/paddings_row.ts +++ b/src/preferences/widgets/paddings_row.ts @@ -1,8 +1,7 @@ import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; -import {uri} from '../../utils/io.js'; - export class PaddingsRowClass extends Adw.PreferencesRow { public declare paddingTop: number; public declare paddingBottom: number; @@ -12,7 +11,11 @@ export class PaddingsRowClass extends Adw.PreferencesRow { export const PaddingsRow = GObject.registerClass( { - Template: uri(import.meta.url, 'paddings-row.ui'), + Template: GLib.uri_resolve_relative( + import.meta.url, + 'paddings-row.ui', + GLib.UriFlags.NONE, + ), GTypeName: 'PaddingsRow', Properties: { PaddingTop: GObject.ParamSpec.int( diff --git a/src/preferences/widgets/reset_page.ts b/src/preferences/widgets/reset_page.ts index b134c53..a373d1a 100644 --- a/src/preferences/widgets/reset_page.ts +++ b/src/preferences/widgets/reset_page.ts @@ -1,13 +1,12 @@ import Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import type Gtk from 'gi://Gtk'; import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; -import {_log} from '../../utils/log.js'; -import {type SchemasKeys, settings} from '../../utils/settings.js'; -import type {RoundedCornersCfg} from '../../utils/types.js'; - -import {uri} from '../../utils/io.js'; +import {logDebug} from '../../utils/log.js'; +import {type SchemaKey, getPref, prefs, setPref} from '../../utils/settings.js'; +import type {RoundedCornerSettings} from '../../utils/types.js'; class Cfg { description: string; @@ -20,7 +19,11 @@ class Cfg { export const ResetPage = GObject.registerClass( { - Template: uri(import.meta.url, 'reset-page.ui'), + Template: GLib.uri_resolve_relative( + import.meta.url, + 'reset-page.ui', + GLib.UriFlags.NONE, + ), GTypeName: 'ResetPage', InternalChildren: ['reset_grp', 'reset_btn', 'dialog'], }, @@ -31,11 +34,11 @@ export const ResetPage = GObject.registerClass( /** Keys to reset */ private declare _reset_keys: { - [name in SchemasKeys]?: Cfg; + [name in SchemaKey]?: Cfg; }; /** Global rounded corners settings to reset */ private declare _reset_corners_cfg: { - [name in keyof RoundedCornersCfg]?: Cfg; + [name in keyof RoundedCornerSettings]?: Cfg; }; /** Used to select all CheckButtons */ private declare _rows: Adw.SwitchRow[]; @@ -62,9 +65,9 @@ export const ResetPage = GObject.registerClass( }; this._reset_corners_cfg = { - border_radius: new Cfg(_('Border Radius')), + borderRadius: new Cfg(_('Border Radius')), padding: new Cfg(_('Padding')), - keep_rounded_corners: new Cfg( + keepRoundedCorners: new Cfg( _('Keep Rounded Corners when Maximized or Fullscreen'), ), smoothing: new Cfg(_('Corner Smoothing')), @@ -93,13 +96,13 @@ export const ResetPage = GObject.registerClass( private on_toggled(source: Adw.SwitchRow): void { const k = source.name; - let v = this._reset_corners_cfg[k as keyof RoundedCornersCfg]; + let v = this._reset_corners_cfg[k as keyof RoundedCornerSettings]; if (v !== undefined) { v.reset = source.active; return; } - v = this._reset_keys[k as SchemasKeys]; + v = this._reset_keys[k as SchemaKey]; if (v !== undefined) { v.reset = source.active; return; @@ -125,25 +128,25 @@ export const ResetPage = GObject.registerClass( } for (const k in this._reset_keys) { - if (this._reset_keys[k as SchemasKeys]?.reset === true) { - settings().g_settings.reset(k); - _log(`Reset ${k}`); + if (this._reset_keys[k as SchemaKey]?.reset === true) { + prefs.reset(k); + logDebug(`Reset ${k}`); } } - const key: SchemasKeys = 'global-rounded-corner-settings'; - const default_cfg = settings() - .g_settings.get_default_value(key) - ?.recursiveUnpack() as RoundedCornersCfg; - const current_cfg = settings().global_rounded_corner_settings; + const key: SchemaKey = 'global-rounded-corner-settings'; + const default_cfg = prefs + .get_default_value(key) + ?.recursiveUnpack() as RoundedCornerSettings; + const current_cfg = getPref('global-rounded-corner-settings'); for (const k in this._reset_corners_cfg) { - const _k = k as keyof RoundedCornersCfg; + const _k = k as keyof RoundedCornerSettings; if (this._reset_corners_cfg[_k]?.reset === true) { current_cfg[_k] = default_cfg[_k] as never; - _log(`Reset ${k}`); + logDebug(`Reset ${k}`); } } - settings().global_rounded_corner_settings = current_cfg; + setPref('global-rounded-corner-settings', current_cfg); const root = this.root as unknown as Adw.PreferencesDialog; root.pop_subpage(); diff --git a/src/preferences/window_picker/service.ts b/src/preferences/window_picker/service.ts index 118c84e..a1d090f 100644 --- a/src/preferences/window_picker/service.ts +++ b/src/preferences/window_picker/service.ts @@ -10,9 +10,8 @@ import Meta from 'gi://Meta'; import {Inspector} from 'resource:///org/gnome/shell/ui/lookingGlass.js'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; -import {loadFile} from '../../utils/io.js'; -import {_log} from '../../utils/log.js'; - +import {readRelativeFile} from '../../utils/file.js'; +import {logDebug} from '../../utils/log.js'; /** * This class provides the implementation of the DBus interface for the window @@ -20,7 +19,7 @@ import {_log} from '../../utils/log.js'; * and allows the user to select a window. */ export class WindowPicker { - #iface = loadFile(import.meta.url, 'iface.xml'); + #iface = readRelativeFile(import.meta.url, 'iface.xml'); #dbus = Gio.DBusExportedObject.wrapJSObject(this.#iface, this); /** Emit the wm_class of the picked window to the `picked` signal. */ @@ -40,7 +39,7 @@ export class WindowPicker { const inspector = new Inspector(lookingGlass); inspector.connect('target', (me, target, x, y) => { - _log(`${me}: pick ${target} in ${x}, ${y}`); + logDebug(`${me}: pick ${target} in ${x}, ${y}`); // Remove the red border effect when the window is picked. const effectName = 'lookingGlass_RedBorderEffect'; @@ -82,7 +81,7 @@ export class WindowPicker { Gio.DBus.session, '/org/gnome/shell/extensions/RoundedWindowCorners', ); - _log('DBus Service exported'); + logDebug('DBus Service exported'); } unexport() { diff --git a/src/prefs.ts b/src/prefs.ts index 6264fce..78ffdff 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -1,27 +1,29 @@ import type Adw from 'gi://Adw'; +import GLib from 'gi://GLib'; import Gdk from 'gi://Gdk'; import Gtk from 'gi://Gtk'; import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; import {pages} from './preferences/index.js'; -import {connections} from './utils/connections.js'; -import * as Utils from './utils/io.js'; -import {_log} from './utils/log.js'; -import {init_settings, uninit_settings} from './utils/settings.js'; +import {logDebug} from './utils/log.js'; +import {initPrefs, uninitPrefs} from './utils/settings.js'; export default class RoundedWindowCornersRebornPrefs extends ExtensionPreferences { _load_css() { const display = Gdk.Display.get_default(); if (display) { const css = new Gtk.CssProvider(); - const path = Utils.path(import.meta.url, 'stylesheet-prefs.css'); + const path = GLib.build_filenamev([ + import.meta.url, + 'stylesheet-prefs.css', + ]); css.load_from_path(path); Gtk.StyleContext.add_provider_for_display(display, css, 0); } } fillPreferencesWindow(win: Adw.PreferencesWindow) { - init_settings(this.getSettings()); + initPrefs(this.getSettings()); for (const page of pages()) { win.add(page); @@ -29,10 +31,8 @@ export default class RoundedWindowCornersRebornPrefs extends ExtensionPreference // Disconnect all signal when close prefs win.connect('close-request', () => { - _log('Disconnect Signals'); - connections.get().disconnect_all(); - connections.del(); - uninit_settings(); + logDebug('Disconnect Signals'); + uninitPrefs(); }); this._load_css(); diff --git a/src/utils/README.md b/src/utils/README.md new file mode 100644 index 0000000..2f41d4b --- /dev/null +++ b/src/utils/README.md @@ -0,0 +1,35 @@ +# `utils` + +This directory contains functions which are used in different places throughout +the codebase, so they can't be put anywhere else. + +## `background_menu.ts` + +Handles adding and removing the RWC settings item in the desktop context menu. + +## `box_shadow.ts` + +Contains a function for converting box shadow JS objects into CSS styles for +those shadows. + +## `constants.ts` + +Defines the constants used in the codebase. + +## `file.ts` + +Contains utility functions for reading file contents. + +## `log.ts` + +Provides wrapper functions for printing out debug messages. + +## `settings.ts` + +Provides wrappers around the GSettings object that add type safety and +automatically convert values between JS types and GLib Variant types that are +used for storing GSettings. + +## `types.ts` + +Provides types used throughout the codebase, mostly for storing settings. diff --git a/src/utils/background_menu.ts b/src/utils/background_menu.ts new file mode 100644 index 0000000..d69a2bf --- /dev/null +++ b/src/utils/background_menu.ts @@ -0,0 +1,89 @@ +/** + * @file Handles adding and removing the RWC settings item in the desktop + * context menu. + * + * XXX: It seems like this relies on GNOME Shell methods which aren't supposed + * to be public. Perhaps this would be removed in the future. + */ + +import { + Extension, + gettext as _, +} from 'resource:///org/gnome/shell/extensions/extension.js'; + +import type Clutter from 'gi://Clutter'; +import type { + PopupMenu, + PopupMenuItem, +} from 'resource:///org/gnome/shell/ui/popupMenu.js'; + +/** + * Desktop background context menu. + * + * https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/backgroundMenu.js + */ +type BackgroundMenu = PopupMenu & { + _getMenuItems(): PopupMenuItem[]; +}; + +/** + * Clutter Actor of the desktop background. + * + * https://gjs-docs.gnome.org/meta15~15/meta.backgroundactor + */ +type BackgroundActor = Clutter.Actor & { + _backgroundMenu: BackgroundMenu; +}; + +/** Enable the "rounded corner settings" item in desktop context menu. */ +export function enableBackgroundMenuItem() { + for (const background of global.windowGroup.firstChild.get_children()) { + const menu = (background as BackgroundActor)._backgroundMenu; + addItemToMenu(menu); + } +} + +/** Disable the "rounded corner settings" item in desktop context menu. */ +export function disableBackgroundMenuItem() { + for (const background of global.windowGroup.firstChild.get_children()) { + const menu = (background as BackgroundActor)._backgroundMenu; + removeItemFromMenu(menu); + } +} + +/** + * Add the menu item to the background menu. + * + * @param menu - BackgroundMenu to add the item to. + */ +function addItemToMenu(menu: BackgroundMenu) { + const rwcMenuItemName = _('Rounded Corners Settings...'); + + // Check if the item already exists + for (const item of menu._getMenuItems()) { + if (item.label?.text === rwcMenuItemName) { + return; + } + } + + menu.addAction(rwcMenuItemName, () => { + const extension = Extension.lookupByURL(import.meta.url) as Extension; + extension.openPreferences(); + }); +} + +/** + * Remove the menu item from the background menu. + * + * @param menu - BackgroundMenu to remove the item from. + */ +function removeItemFromMenu(menu: BackgroundMenu) { + const items = menu._getMenuItems(); + const rwcMenuItemName = _('Rounded Corners Settings...'); + for (const item of items) { + if (item.label?.text === rwcMenuItemName) { + item.destroy(); + break; + } + } +} diff --git a/src/utils/box_shadow.ts b/src/utils/box_shadow.ts new file mode 100644 index 0000000..040d933 --- /dev/null +++ b/src/utils/box_shadow.ts @@ -0,0 +1,22 @@ +/** + * @file Contains a single function - {@link boxShadowCss}, which converts + * {@link BoxShadow} objects into CSS code for the shadow. + */ + +import type {BoxShadow} from './types.js'; + +/** + * Generate a CSS style for a box shadow from the provided {@link BoxShadow} + * object. + * + * @param shadow - The settings for the box shadow. + * @param scale - The scale of the window, 1 by default. + * @returns The box-shadow CSS string. + */ +export function boxShadowCss(shadow: BoxShadow, scale = 1) { + return `box-shadow: ${shadow.horizontalOffset * scale}px + ${shadow.verticalOffset * scale}px + ${shadow.blurOffset * scale}px + ${shadow.spreadRadius * scale}px + rgba(0,0,0, ${shadow.opacity / 100})`; +} diff --git a/src/utils/connections.ts b/src/utils/connections.ts deleted file mode 100644 index b95f1e8..0000000 --- a/src/utils/connections.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Types -import type GObject from 'gi://GObject'; - -// ---------------------------------------------------------------- [end import] - -/** - * This class is used to manager signal and handles of a object - */ -export class Connections { - // -------------------------------------------------------- [public methods] - - /** - * Map object to store signal sources and their handlers - * @type {Map} - */ - private connections: _Map = new Map(); - - /** - * Handler signal for a GObject - * - * ### Example: - * - * ```typescript - * const manager = new Connections () - * manager.connect (global.window_manager, 'destroy', (wm, actor) => - * console.log (`${actor} has been removed`) - * ) - * ``` - * @param source - Signal source - * @param args - Arguments pass into GObject.Object.connect() - * - * biome-ignore lint/suspicious/noExplicitAny: this will make life easier - */ - connect(source: T, ...args: any): void { - const signal: string = args[0]; - const id: number = source.connect(args[0], args[1]); - - // Source has been added into manager - { - const handlers = this.connections.get(source); - if (handlers !== undefined) { - if (handlers[signal] !== undefined) { - handlers[signal].push(id); - return; - } - handlers[signal] = [id]; - return; - } - } - - // Source is first time register signal - const handlers: {[signal: string]: number[]} = {}; - handlers[signal] = [id]; - this.connections.set(source, handlers); - } - - disconnect( - source: T, - signal: Parameters[0], - ): void; - disconnect(source: GObject.Object, signal: string): void; - /** Disconnect signal for source */ - disconnect( - source: T, - signal: Parameters[0], - ) { - const handlers = this.connections.get(source); - if (handlers !== undefined) { - const handler = handlers[signal]; - if (handler !== undefined) { - for (const id of handler) { - source.disconnect(id); - } - delete handlers[signal]; - if (Object.keys(handler).length === 0) { - this.connections.delete(source); - } - return; - } - } - } - - /** Disconnect **all** signals for object */ - disconnect_all(source: GObject.Object): void; - - /** Disconnect **all** signals for **all** objects */ - disconnect_all(): void; - - disconnect_all(source?: GObject.Object) { - // If provide source, disconnect all signal of it - if (source !== undefined) { - const handlers = this.connections.get(source); - if (handlers !== undefined) { - for (const signal in handlers) { - for (const id of handlers[signal]) { - source.disconnect(id); - } - delete handlers[signal]; - } - this.connections.delete(source); - } - return; - } - - // otherwise clear signal for all objects. - this.connections.forEach((handlers, source) => { - for (const signal in handlers) { - for (const id of handlers[signal]) { - source.disconnect(id); - } - delete handlers[signal]; - } - }); - this.connections.clear(); - } -} - -/** A singleton of connections */ -let _connections: Connections | null = null; - -export const connections = { - get: () => { - if (_connections === null) { - _connections = new Connections(); - } - return _connections; - }, - del: () => { - _connections = null; - }, -}; - -// Signal source signal name it's handler -type _Map = Map; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 557920d..11bfbda 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,4 @@ -// This files use for store const variants will used by other modules. +/** @file Defines the constants used in this extension. */ /** Name of the rounded corners effect */ export const ROUNDED_CORNERS_EFFECT = 'Rounded Corners Effect'; @@ -9,22 +9,13 @@ export const CLIP_SHADOW_EFFECT = 'Clip Shadow Effect'; /** Padding of shadow actors */ export const SHADOW_PADDING = 80; -/** Hardcoded shadow size for certain applications that have to be - * manually clipped */ +/** + * Hardcoded shadow size for certain applications that have to be + * manually clipped + */ export const APP_SHADOWS = { kitty: [11, 35, 11, 11], }; -// TODO: Those constants should be extracted into separate variables like the -// ones above. This will be done when refactoring the files that they are used -// in. -export const constants = { - /** Name of shadow actors */ - SHADOW_ACTOR_NAME: 'Rounded Window Shadow Actor', - /** Name of blur effect for window */ - BLUR_EFFECT: 'Patched Blur Effect', - /** Used to mark widget in preferences/page/custom.ts */ - DON_T_CONFIG: "Don't Configuration in Custom Page", - /** Name of shadow actor to be added in overview */ - OVERVIEW_SHADOW_ACTOR: 'Shadow Actor (Overview)', -}; +/** Name of shadow actor to be added in overview */ +export const OVERVIEW_SHADOW_ACTOR = 'Shadow Actor (Overview)'; diff --git a/src/utils/file.ts b/src/utils/file.ts new file mode 100644 index 0000000..c1c990f --- /dev/null +++ b/src/utils/file.ts @@ -0,0 +1,51 @@ +/** @file Contains utility functions for reading file contents. */ + +import GLib from 'gi://GLib'; +import Gio from 'gi://Gio'; + +/** + * Read file contents as a string. + * + * @param path - The path to the file to be read. + * @returns Contents of the file as a UTF-8 string. + */ +export function readFile(path: string) { + const file = Gio.File.new_for_path(path); + + const contents = file.load_contents(null)[1]; + + const decoder = new TextDecoder('utf-8'); + return decoder.decode(contents); +} + +/** + * Read a file relative to the current module. + * + * @param module - `import.meta.url` of the current module. + * @param path - File path relative to the current module. + * @returns Contents of the file as a UTF-8 string. + */ +export function readRelativeFile(module: string, path: string) { + const basedir = GLib.path_get_dirname(module); + const fileUri = GLib.build_filenamev([basedir, path]); + const filePath = GLib.filename_from_uri(fileUri)[0]; + return readFile(filePath ?? ''); +} + +/** + * Read a shader file and split it into declarations and main code, since + * GNOME's `add_glsl_snippet` function takes those parts as two separate + * arguments. + * + * @param module - `import.meta.url` of the current module. + * @param path - File path relative to the current module. + * @returns A list containing the declarations as the first element and + * contents of the main function as the second. + */ +export function readShader(module: string, path: string) { + const shader = readRelativeFile(module, path); + let [declarations, code] = shader.split(/^.*?main\(\s?\)\s?/m); + declarations = declarations.trim(); + code = code.trim().replace(/^[{}]/gm, '').trim(); + return [declarations, code]; +} diff --git a/src/utils/io.ts b/src/utils/io.ts deleted file mode 100644 index 7af1ab5..0000000 --- a/src/utils/io.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Gio from 'gi://Gio'; - -// --------------------------------------------------------------- [end imports] - -export const load = (path: string): string => { - const file = Gio.File.new_for_path(path); - - const [, contents] = file.load_contents(null); - - const decoder = new TextDecoder('utf-8'); - return decoder.decode(contents); -}; - -export const path = (mod_url: string, relative_path: string) => { - const parent = Gio.File.new_for_uri(mod_url).get_parent(); - - const mod_dir = parent?.get_path(); - return ( - Gio.File.new_for_path(`${mod_dir}/${relative_path}`).get_path() ?? '' - ); -}; - -export const uri = (mod_url: string, relative_path: string) => { - const parent = Gio.File.new_for_uri(mod_url).get_parent(); - - const mod_uri = parent?.get_uri(); - return `${mod_uri}/${relative_path}`; -}; - -export const loadFile = (mod_url: string, relative_path: string) => - load(path(mod_url, relative_path) ?? ''); - -export const loadShader = (mod_url: string, relative_path: string) => { - let [declarations, main] = load(path(mod_url, relative_path) ?? '').split( - /^.*?main\(\s?\)\s?/m, - ); - - declarations = declarations.trim(); - main = main.trim().replace(/^[{}]/gm, '').trim(); - return {declarations, code: main}; -}; diff --git a/src/utils/log.ts b/src/utils/log.ts index 2b88c20..5922472 100644 --- a/src/utils/log.ts +++ b/src/utils/log.ts @@ -1,30 +1,20 @@ -import {settings} from './settings.js'; +/** @file Provides wrapper functions for printing out debug messages. */ -// --------------------------------------------------------------- [end imports] +import {getPref} from './settings.js'; /** - * Log message Only when debug_mode of settings () is enabled + * Log a message with a [Rounded Window Corners] prefix, but only + * when debug mode is enabled. */ -export const _log = (...args: unknown[]) => { - if (settings().debug_mode) { - console.log(`[RoundedCornersEffect] ${args}`); +export function logDebug(...args: unknown[]) { + if (getPref('debug-mode')) { + console.log(`[Rounded Window Corners] ${args}`); } -}; - -/** Always log error message */ -export const _logError = (err: Error) => { - console.error(err); -}; +} /** - * Get stack message when called this function, this method - * will be used when monkey patch the code of gnome-shell to skip some - * function invocations. + * Log an error with a [Rounded Window Corners] prefix. */ -export const stackMsg = (): string | undefined => { - try { - throw Error(); - } catch (e) { - return (e as Error)?.stack?.trim(); - } -}; +export function logError(...args: unknown[]) { + console.error(`[Rounded Window Corners] ${args}`); +} diff --git a/src/utils/prefs.ts b/src/utils/prefs.ts deleted file mode 100644 index 92d1f77..0000000 --- a/src/utils/prefs.ts +++ /dev/null @@ -1,48 +0,0 @@ -import GLib from 'gi://GLib'; -import Gio from 'gi://Gio'; -import type Gtk from 'gi://Gtk'; -import {gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; - -export const list_children = (widget: Gtk.ListBox) => { - const children = []; - for ( - let child = widget.get_first_child(); - child != null; - child = child.get_next_sibling() - ) { - children.push(child); - } - return children; -}; - -export const show_err_msg = (info: string) => { - // Show error message with notifications - // by call DBus method of org.freedesktop.Notifications - // - // Ref: https://gjs.guide/guides/gio/dbus.html#direct-calls - - Gio.DBus.session.call( - 'org.freedesktop.Notifications', - '/org/freedesktop/Notifications', - 'org.freedesktop.Notifications', - 'Notify', - new GLib.Variant('(susssasa{sv}i)', [ - '', - 0, - '', - 'Rounded Window Corners', - info, - [], - {}, - 3000, - ]), - null, - Gio.DBusCallFlags.NONE, - -1, - null, - null, - ); -}; - -/** Tips when add new items in preferences Page */ -export const TIPS_EMPTY = () => _('Expand this row to pick a window.'); diff --git a/src/utils/settings.ts b/src/utils/settings.ts index 1441f85..1d90d0f 100644 --- a/src/utils/settings.ts +++ b/src/utils/settings.ts @@ -1,227 +1,188 @@ -// imports.gi +/** + * @file Provides wrappers around the GSettings object that add type safety and + * automatically convert values between JS types and GLib Variant types that + * are used for storing GSettings. + */ + import GLib from 'gi://GLib'; -import type Gio from 'gi://Gio'; -// used to mark types, will be remove in output files. +import {logDebug} from './log.js'; + import type GObject from 'gi://GObject'; +import type Gio from 'gi://Gio'; import type { BoxShadow, - CustomRoundedCornersCfg, - RoundedCornersCfg, + CustomRoundedCornerSettings, + RoundedCornerSettings, } from './types.js'; -// --------------------------------------------------------------- [end imports] +/** Mapping of schema keys to the JS representation of their type. */ +type Schema = { + 'settings-version': number; + blacklist: string[]; + 'skip-libadwaita-app': boolean; + 'skip-libhandy-app': boolean; + 'border-width': number; + 'border-color': [number, number, number, number]; + 'global-rounded-corner-settings': RoundedCornerSettings; + 'custom-rounded-corner-settings': CustomRoundedCornerSettings; + 'focused-shadow': BoxShadow; + 'unfocused-shadow': BoxShadow; + 'debug-mode': boolean; + 'tweak-kitty-terminal': boolean; + 'enable-preferences-entry': boolean; +}; + +/** All existing schema keys. */ +export type SchemaKey = keyof Schema; + +/** Mapping of schema keys to their GLib Variant type string */ +const Schema = { + 'settings-version': 'u', + blacklist: 'as', + 'skip-libadwaita-app': 'b', + 'skip-libhandy-app': 'b', + 'border-width': 'i', + 'border-color': '(dddd)', + 'global-rounded-corner-settings': 'a{sv}', + 'custom-rounded-corner-settings': 'a{sv}', + 'focused-shadow': 'a{si}', + 'unfocused-shadow': 'a{si}', + 'debug-mode': 'b', + 'tweak-kitty-terminal': 'b', + 'enable-preferences-entry': 'b', +}; -/** This object use to store key of settings and its type string */ -const type_of_keys: { - // Key // types string - [key: string]: string; -} = {}; +/** The raw GSettings object for direct manipulation. */ +export let prefs: Gio.Settings; /** - * Keys of settings, should update this type when add new key in schemas xml + * Initialize the {@link prefs} object with existing GSettings. + * + * @param gSettings - GSettings to initialize the prefs with. */ -export type SchemasKeys = - | 'black-list' - | 'skip-libadwaita-app' - | 'skip-libhandy-app' - | 'global-rounded-corner-settings' - | 'custom-rounded-corner-settings' - | 'focused-shadow' - | 'unfocused-shadow' - | 'debug-mode' - | 'border-width' - | 'border-color' - | 'settings-version' - | 'tweak-kitty-terminal' - | 'enable-preferences-entry'; +export function initPrefs(gSettings: Gio.Settings) { + resetOutdated(gSettings); + prefs = gSettings; +} + +/** Delete the {@link prefs} object for garbage collection. */ +export function uninitPrefs() { + (prefs as Gio.Settings | null) = null; +} /** - * Simple wrapper of Gio.Settings, we will use this class to store and - * load settings for this gnome-shell extensions. + * Get a preference from GSettings and convert it from a GLib Variant to a + * JavaScript type. + * + * @param key - The key of the preference to get. + * @returns The value of the preference. */ -export class Settings { - // Keys of settings, define getter and setter in constructor() - black_list!: string[]; - skip_libadwaita_app!: boolean; - skip_libhandy_app!: boolean; - global_rounded_corner_settings!: RoundedCornersCfg; - custom_rounded_corner_settings!: CustomRoundedCornersCfg; - focused_shadow!: BoxShadow; - unfocused_shadow!: BoxShadow; - debug_mode!: boolean; - tweak_kitty_terminal!: boolean; - enable_preferences_entry!: boolean; - border_width!: number; - settings_version!: number; - border_color!: [number, number, number, number]; - - /** GSettings, which used to store and load settings */ - g_settings: Gio.Settings; - - constructor(g_settings: Gio.Settings) { - this.g_settings = g_settings; - - // Define getter and setter for properties in class for keys in - // schemas - for (const key of this.g_settings.list_keys()) { - // Cache type string of keys first - const default_val = this.g_settings.get_default_value(key); - if (default_val == null) { - log(`Err: Key of Settings undefined: ${key}`); - return; - } - type_of_keys[key] = default_val.get_type_string(); - - // Define getter and setter for keys - Object.defineProperty(this, key.replace(/-/g, '_'), { - get: () => this.g_settings.get_value(key).recursiveUnpack(), - set: val => { - const variant = - type_of_keys[key] === 'a{sv}' - ? this._pack_val(val) - : new GLib.Variant(type_of_keys[key], val); - this.g_settings.set_value(key, variant); - }, - }); - } - - /** Port rounded corners settings to new version */ - this._fix(); - } - - /** - * Just a simple wrapper to this.settings.bind(), use SchemasKeys - * to help us check source_prop - */ - bind( - source_prop: SchemasKeys, - target: GObject.Object, - target_prop: string, - flags: Gio.SettingsBindFlags, - ) { - this.g_settings.bind(source_prop, target, target_prop, flags); - } +export function getPref(key: K): Schema[K] { + return prefs.get_value(key).recursiveUnpack(); +} - // ------------------------------------------------------- [private methods] - - /** - * this method is used to pack javascript values into GLib.Variant when type - * of key is `a{sv}` - * - * @param val Javascript object to convert - * @returns A GLib.Variant with type `a{sv}` - */ - private _pack_val(val: number | boolean | string | unknown): GLib.Variant { - if (val instanceof Object) { - const packed: {[prop: string]: GLib.Variant} = {}; - for (const k in val) { - packed[k] = this._pack_val( - (val as {[prop: string]: unknown})[k], - ); - } - return new GLib.Variant('a{sv}', packed); - } - - // Important: Just handler float number and unsigned int number. - // need to add handler to signed int number if we need store signed int - // value into GSettings in GLib.Variant - - if (typeof val === 'number') { - if (Math.abs(val - Math.floor(val)) < 10e-20) { - return GLib.Variant.new_uint32(val); - } - return GLib.Variant.new_double(val); - } - - if (typeof val === 'boolean') { - return GLib.Variant.new_boolean(val); - } - - if (typeof val === 'string') { - return GLib.Variant.new_string(val); - } - - if (Array.isArray(val)) { - return new GLib.Variant( - 'av', - val.map(i => this._pack_val(i)), - ); - } - - throw Error(`Unknown val to packed${val}`); +/** + * Pack a value into a GLib Variant type and store it in GSettings. + * + * @param key - The key of the preference to set. + * @param value - The value to set the preference to. + */ +export function setPref(key: K, value: Schema[K]) { + logDebug(`Settings pref: ${key}, ${value}`); + let variant: GLib.Variant; + + if (key === 'global-rounded-corner-settings') { + variant = packRoundedCornerSettings(value as RoundedCornerSettings); + } else if (key === 'custom-rounded-corner-settings') { + variant = packCustomRoundedCornerSettings( + value as CustomRoundedCornerSettings, + ); + } else { + variant = new GLib.Variant(Schema[key], value); } - /** Fix RoundedCornersCfg when this type has been updated */ - private _fix_rounded_corners_cfg( - default_val: RoundedCornersCfg & {[prop: string]: undefined}, - val: RoundedCornersCfg & {[prop: string]: undefined}, - ) { - // Added missing props - for (const k in default_val) { - if (val[k] === undefined) { - val[k] = default_val[k]; - } - } - - // keep_rounded_corners has been update to object type in v5 - if (typeof val.keep_rounded_corners === 'boolean') { - const keep_rounded_corners = { - ...default_val.keep_rounded_corners, - maximized: val.keep_rounded_corners, - }; - val.keep_rounded_corners = keep_rounded_corners; - } - } + prefs.set_value(key, variant); +} - /** Port Settings to newer version in here when changed 'a{sv}' types */ - private _fix() { - const VERSION = 5; - if (this.settings_version === VERSION) { - return; - } - this.settings_version = VERSION; - - type _Cfg = RoundedCornersCfg & {[p: string]: undefined}; - - const key: SchemasKeys = 'global-rounded-corner-settings'; - const default_val = this.g_settings - .get_default_value(key) - ?.recursiveUnpack() as _Cfg; - - // Fix global-rounded-corners-settings - const global_cfg = this.global_rounded_corner_settings as _Cfg; - this._fix_rounded_corners_cfg(default_val, global_cfg); - this.global_rounded_corner_settings = global_cfg; - - // Fix custom-rounded-corner-settings - const custom_cfg = this.custom_rounded_corner_settings; - for (const k in custom_cfg) { - this._fix_rounded_corners_cfg(default_val, custom_cfg[k] as _Cfg); - } - this.custom_rounded_corner_settings = custom_cfg; - - log(`[RoundedWindowCorners] Update Settings to v${VERSION}`); - } +/** A simple type-checked wrapper around {@link prefs.bind} */ +export function bindPref( + key: SchemaKey, + object: GObject.Object, + property: string, + flags: Gio.SettingsBindFlags, +) { + prefs.bind(key, object, property, flags); +} - _disable() { - (this.g_settings as Gio.Settings | null) = null; +/** + * Reset setting keys that changed their type between releases + * to avoid conflicts. + * + * @param prefs the GSettings object to clean. + */ +function resetOutdated(prefs: Gio.Settings) { + if (prefs.get_uint('settings-version') < 5) { + prefs.reset('settings-version'); + prefs.reset('black-list'); + prefs.reset('global-rounded-corner-settings'); + prefs.reset('focused-shadow'); + prefs.reset('unfocused-shadow'); } } -/** A singleton instance of Settings */ -let _settings!: Settings; - -export const init_settings = (g_settings: Gio.Settings) => { - _settings = new Settings(g_settings); -}; +/** + * Pack rounded corner settings into a GLib Variant object. + * + * Since rounded corner settings are stored as a dictionary where the values + * are of different types, it can't be automatically packed into a variant. + * Instead, we need to pack each of the values into the correct variant + * type, and only then pack the entire dictionary into a variant with type + * "a{sv}" (dictionary with string keys and arbitrary variant values). + * + * @param settings - The rounded corner settings to pack. + * @returns The packed GLib Variant object. + */ +function packRoundedCornerSettings(settings: RoundedCornerSettings) { + const padding = new GLib.Variant('a{su}', settings.padding); + const keepRoundedCorners = new GLib.Variant( + 'a{sb}', + settings.keepRoundedCorners, + ); + const borderRadius = GLib.Variant.new_uint32(settings.borderRadius); + const smoothing = GLib.Variant.new_double(settings.smoothing); + const enabled = GLib.Variant.new_boolean(settings.enabled); + + const variantObject = { + padding: padding, + keepRoundedCorners: keepRoundedCorners, + borderRadius: borderRadius, + smoothing: smoothing, + enabled: enabled, + }; + + return new GLib.Variant('a{sv}', variantObject); +} -export const uninit_settings = () => { - _settings?._disable(); - (_settings as Settings | null) = null; -}; +/** + * Pack custom rounded corner overrides into a GLib Variant object. + * + * Custom rounded corner settings are stored as a dictionary from window + * wm_class to {@link RoundedCornerSettings} objects. See the documentation for + * {@link packRoundedCornerSettings} for more information on why manual packing + * is needed here. + * + * @param settings - The custom rounded corner setting overrides to pack. + * @returns The packed GLib Variant object. + */ +function packCustomRoundedCornerSettings( + settings: CustomRoundedCornerSettings, +) { + const packedSettings: Record> = {}; + for (const [wmClass, windowSettings] of Object.entries(settings)) { + packedSettings[wmClass] = packRoundedCornerSettings(windowSettings); + } -/** Access _settings by this method */ -export const settings = () => { - return _settings; -}; + const variant = new GLib.Variant('a{sv}', packedSettings); + return variant; +} diff --git a/src/utils/types.ts b/src/utils/types.ts index 98076fb..6a4d04d 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -1,63 +1,55 @@ -import type Clutter from 'gi://Clutter'; -import type GObject from 'gi://GObject'; +/** @file Provides types used throughout the codebase, mostly for storing settings. */ + import type Meta from 'gi://Meta'; import type St from 'gi://St'; /** Bounds of rounded corners */ -export class Bounds { - x1 = 0; - y1 = 0; - x2 = 0; - y2 = 0; -} - -export class Padding { - left = 0; - right = 0; - top = 0; - bottom = 0; -} +export type Bounds = { + x1: number; + y1: number; + x2: number; + y2: number; +}; -/** Store into settings, rounded corners configuration */ -export interface RoundedCornersCfg { - keep_rounded_corners: { +/** Settings for corner rounding. */ +export type RoundedCornerSettings = { + keepRoundedCorners: { maximized: boolean; fullscreen: boolean; }; - border_radius: number; + borderRadius: number; smoothing: number; - padding: Padding; + padding: { + left: number; + right: number; + top: number; + bottom: number; + }; enabled: boolean; -} +}; -export interface CustomRoundedCornersCfg { - [wm_class_instance: string]: RoundedCornersCfg; -} +/** Rounded corner settings exceptions for specific windows. */ +export type CustomRoundedCornerSettings = { + [wmClass: string]: RoundedCornerSettings; +}; -export interface BoxShadow { +/** Window shadow properties. */ +export type BoxShadow = { opacity: number; - spread_radius: number; - blur_offset: number; - vertical_offset: number; - horizontal_offset: number; -} - -export const box_shadow_css = (box_shadow: BoxShadow, scale = 1) => { - return `box-shadow: ${box_shadow.horizontal_offset * scale}px - ${box_shadow.vertical_offset * scale}px - ${box_shadow.blur_offset * scale}px - ${box_shadow.spread_radius * scale}px - rgba(0,0,0, ${box_shadow.opacity / 100})`; + spreadRadius: number; + blurOffset: number; + verticalOffset: number; + horizontalOffset: number; }; -export type ExtensionsWindowActor = Meta.WindowActor & { - __rwcRoundedWindowInfo?: { +/** + * A window actor with rounded corners. + * + * This type is needed to store extra custom properties on a window actor. + */ +export type RoundedWindowActor = Meta.WindowActor & { + rwcCustomData?: { shadow: St.Bin; unminimizedTimeoutId: number; }; - __rwc_blurred_window_info?: { - blur_actor: Clutter.Actor; - visible_binding: GObject.Binding; - }; - shadow_mode?: Meta.ShadowMode; }; diff --git a/src/utils/ui.ts b/src/utils/ui.ts deleted file mode 100644 index f0b1caf..0000000 --- a/src/utils/ui.ts +++ /dev/null @@ -1,246 +0,0 @@ -// imports.gi -import Gio from 'gi://Gio'; -import Meta from 'gi://Meta'; - -// gnome modules -import { - Extension, - gettext as _, -} from 'resource:///org/gnome/shell/extensions/extension.js'; - -// local modules -import {ROUNDED_CORNERS_EFFECT} from './constants.js'; -import {load} from './io.js'; -import {_log, _logError} from './log.js'; - -// types -import type Clutter from 'gi://Clutter'; -import type {RoundedCornersEffect} from '../effect/rounded_corners_effect.js'; -import {settings} from './settings.js'; -import type * as types from './types.js'; - -// --------------------------------------------------------------- [end imports] - -export const computeWindowContentsOffset = ( - meta_window: Meta.Window, -): [number, number, number, number] => { - const bufferRect = meta_window.get_buffer_rect(); - const frameRect = meta_window.get_frame_rect(); - return [ - frameRect.x - bufferRect.x, - frameRect.y - bufferRect.y, - frameRect.width - bufferRect.width, - frameRect.height - bufferRect.height, - ]; -}; - -export enum AppType { - LibHandy = 'LibHandy', - LibAdwaita = 'LibAdwaita', - Other = 'Other', -} - -/** - * Query application type for a Meta.Window, used to skip add rounded - * corners effect to some window. - * @returns Application Type: LibHandy | LibAdwaita | Other - */ -export const getAppType = (meta_window: Meta.Window) => { - try { - // May cause Permission error - const contents = load(`/proc/${meta_window.get_pid()}/maps`); - if (contents.match(/libhandy-1.so/)) { - return AppType.LibHandy; - } - - if (contents.match(/libadwaita-1.so/)) { - return AppType.LibAdwaita; - } - - return AppType.Other; - } catch (e) { - _logError(e as Error); - return AppType.Other; - } -}; - -/** - * Get scale factor of a Meta.window, if win is undefined, return - * scale factor of current monitor - */ -export const WindowScaleFactor = (win?: Meta.Window) => { - const features = Gio.Settings.new('org.gnome.mutter').get_strv( - 'experimental-features', - ); - - // When enable fractional scale in Wayland, return 1 - if ( - Meta.is_wayland_compositor() && - features.includes('scale-monitor-framebuffer') - ) { - return 1; - } - - const monitor_index = win - ? win.get_monitor() - : global.display.get_current_monitor(); - return global.display.get_monitor_scale(monitor_index); -}; - -type BackgroundMenu = { - _getMenuItems: () => {label?: {text: string}}[]; - addAction: (label: string, action: () => void) => void; - moveMenuItem(item: {label?: {text: string}}, index: number): void; -}; -type BackgroundExtra = { - _backgroundMenu: BackgroundMenu; -}; - -/** - * Add Item into background menu, now we can open preferences page by right - * click in background - * @param menu - BackgroundMenu to add - */ -export const AddBackgroundMenuItem = (menu: BackgroundMenu) => { - const openprefs_item = _('Rounded Corners Settings...'); - for (const item of menu._getMenuItems()) { - if (item.label?.text === openprefs_item) { - return; - } - } - - menu.addAction(openprefs_item, () => { - const extension = Extension.lookupByURL(import.meta.url) as Extension; - try { - extension.openPreferences(); - } catch { - extension.openPreferences(); - } - }); -}; - -/** Find all Background menu, then add extra item to it */ -export const SetupBackgroundMenu = () => { - for (const _bg of global.windowGroup.firstChild.get_children()) { - _log('Found Desktop Background obj', _bg); - const menu = (_bg as typeof _bg & BackgroundExtra)._backgroundMenu; - AddBackgroundMenuItem(menu); - } -}; - -export const RestoreBackgroundMenu = () => { - const remove_menu_item = (menu: BackgroundMenu) => { - const items = menu._getMenuItems(); - const openprefs_item = _('Rounded Corners Settings...'); - for (const i of items) { - if (i?.label?.text === openprefs_item) { - (i as Clutter.Actor).destroy(); - break; - } - } - }; - - for (const _bg of global.windowGroup.firstChild.get_children()) { - const menu = (_bg as typeof _bg & BackgroundExtra)._backgroundMenu; - remove_menu_item(menu); - _log(`Added Item of ${menu}Removed`); - } -}; - -/** - * Get the correct settings object for a window. - * - * @param win - The window to get the settings for. - */ -export function getRoundedCornersCfg( - win: Meta.Window, -): types.RoundedCornersCfg { - const global_cfg = settings().global_rounded_corner_settings; - const custom_cfg_list = settings().custom_rounded_corner_settings; - - const k = win.get_wm_class_instance(); - if (k == null || !custom_cfg_list[k] || !custom_cfg_list[k].enabled) { - return global_cfg; - } - - const custom_cfg = custom_cfg_list[k]; - // Need to skip border radius item from custom settings - custom_cfg.border_radius = global_cfg.border_radius; - return custom_cfg; -} - -/** - * Check whether a window should have rounded corners. - * - * @param win - The window to check. - * @returns Whether the window should have rounded corners. - */ -export function shouldEnableEffect( - win: Meta.Window & {_appType?: AppType}, -): boolean { - // DING (Desktop Icons NG) is a extensions that create a gtk - // application to show desktop grid on background, we need to - // skip it coercively. - // https://extensions.gnome.org/extension/2087/desktop-icons-ng-ding/ - if (win.gtkApplicationId === 'com.rastersoft.ding') { - return false; - } - - // Skip applications in black list. - const wmClassInstance = win.get_wm_class_instance(); - if (wmClassInstance == null) { - _log(`Warning: wm_class_instance of ${win}: ${win.title} is null`); - return false; - } - if (settings().black_list.includes(wmClassInstance)) { - return false; - } - - // Check type of window, only need to add rounded corners to normal - // window and dialog. - const normalType = [ - Meta.WindowType.NORMAL, - Meta.WindowType.DIALOG, - Meta.WindowType.MODAL_DIALOG, - ].includes(win.windowType); - if (!normalType) { - return false; - } - - // Skip libhandy / libadwaita applications according to settings. - const appType = win._appType ?? getAppType(win); - win._appType = appType; // cache result - _log(`Check Type of window:${win.title} => ${AppType[appType]}`); - - if (settings().skip_libadwaita_app && appType === AppType.LibAdwaita) { - return false; - } - if (settings().skip_libhandy_app && appType === AppType.LibHandy) { - return false; - } - - // Skip maximized / fullscreen windows according to settings. - const maximized = win.maximizedHorizontally || win.maximizedVertically; - const fullscreen = win.fullscreen; - const cfg = getRoundedCornersCfg(win); - return ( - !(maximized || fullscreen) || - (maximized && cfg.keep_rounded_corners.maximized) || - (fullscreen && cfg.keep_rounded_corners.fullscreen) - ); -} - -type RoundedCornersEffectType = InstanceType; - -/** - * Get Rounded corners effect from a window actor - */ -export function get_rounded_corners_effect( - actor: Meta.WindowActor, -): RoundedCornersEffectType | null { - const win = actor.metaWindow; - const name = ROUNDED_CORNERS_EFFECT; - return win.get_client_type() === Meta.WindowClientType.X11 - ? (actor.firstChild.get_effect(name) as RoundedCornersEffectType) - : (actor.get_effect(name) as RoundedCornersEffectType); -}