From b68f962f1b40f3112f1d194b2765a9015481ca5c Mon Sep 17 00:00:00 2001 From: Juraj Fiala Date: Tue, 7 Nov 2023 17:02:19 +0100 Subject: [PATCH 1/2] feat: Port extension to GNOME 45 --- .eslintrc.yml | 15 ++-- extension.js | 212 ++++++++++++++++---------------------------------- metadata.json | 2 +- prefs.js | 100 ++++++++++-------------- 4 files changed, 117 insertions(+), 212 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 81541c3..d4e03b7 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -3,7 +3,7 @@ # SPDX-FileCopyrightText: 2018 Claudio André env: es2021: true -extends: 'eslint:recommended' +extends: "eslint:recommended" plugins: - jsdoc rules: @@ -58,11 +58,11 @@ rules: - error - 4 - ignoredNodes: - # Allow not indenting the body of GObject.registerClass, since in the - # future it's intended to be a decorator - - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' + # Allow not indenting the body of GObject.registerClass, since in the + # future it's intended to be a decorator + - "CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child" # Allow dedenting chained member expressions - MemberExpression: 'off' + MemberExpression: "off" jsdoc/check-alignment: error jsdoc/check-param-names: error jsdoc/check-tag-names: error @@ -115,7 +115,7 @@ rules: no-implicit-coercion: - error - allow: - - '!!' + - "!!" no-invalid-this: error no-iterator: error no-label-var: error @@ -126,7 +126,7 @@ rules: no-new-wrappers: error no-octal-escape: error no-proto: error - no-prototype-builtins: 'off' + no-prototype-builtins: "off" no-restricted-globals: [error, window] no-restricted-properties: - error @@ -265,3 +265,4 @@ globals: clearInterval: readonly parserOptions: ecmaVersion: 2022 + sourceType: module diff --git a/extension.js b/extension.js index 65d21e2..e27cf62 100644 --- a/extension.js +++ b/extension.js @@ -20,169 +20,111 @@ 'use strict'; -const {Gio, GLib, GObject} = imports.gi; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; -const ExtensionUtils = imports.misc.extensionUtils; -const Me = ExtensionUtils.getCurrentExtension(); -const Main = imports.ui.main; -const QuickSettings = imports.ui.quickSettings; -const _ = ExtensionUtils.gettext; +import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js'; +import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import * as QuickSettings from 'resource:///org/gnome/shell/ui/quickSettings.js'; -// This is the live instance of the Quick Settings menu -const QuickSettingsMenu = imports.ui.main.panel.statusArea.quickSettings; const KMonadToggle = GObject.registerClass( - class FeatureToggle extends QuickSettings.QuickToggle { - _init() { - super._init({ - title: _('KMonad'), - gicon: getIcon(), - toggleMode: true, - }); - - // NOTE: In GNOME 44, the `label` property must be set after - // construction. The newer `title` property can be set at construction. - this.label = _('KMonad'); - - this._settings = ExtensionUtils.getSettings(); +class FeatureToggle extends QuickSettings.QuickToggle { + _init(extensionObject, icon) { + super._init({ + title: _('KMonad'), + gicon: icon, + toggleMode: true, + }); - this._settings.bind( - 'kmonad-running', - this, - 'checked', - Gio.SettingsBindFlags.DEFAULT - ); - } - } + this._settings = extensionObject.getSettings(); + this._settings.bind( + 'kmonad-running', + this, + 'checked', + Gio.SettingsBindFlags.DEFAULT + ); + } +} ); const KMonadIndicator = GObject.registerClass( - class FeatureIndicator extends QuickSettings.SystemIndicator { - _init() { - super._init(); - - // Create the icon for the indicator - this._indicator = this._addIndicator(); - this._indicator.gicon = getIcon(); - - // Showing the indicator when the feature is enabled - this._settings = ExtensionUtils.getSettings(); - - this._settings.bind( - 'kmonad-running', - this._indicator, - 'visible', - Gio.SettingsBindFlags.DEFAULT - ); - - // Create the toggle and associate it with the indicator, being sure to - // destroy it along with the indicator - this.quickSettingsItems.push(new KMonadToggle()); - - this.connect('destroy', () => { - this.quickSettingsItems.forEach(item => item.destroy()); - }); - - // Add the indicator to the panel and the toggle to the menu - QuickSettingsMenu._indicators.insert_child_at_index(this, 0); - addQuickSettingsItems(this.quickSettingsItems); - } - } -); - -/** - * Returns the KMonad symbolic icon - */ -function getIcon() { - return Gio.icon_new_for_string( - GLib.build_filenamev([Me.path, 'icons', 'kmonad-symbolic.svg']) - ); -} - -/** - * Adds the items to the Quick Settings menu above the Background Apps menu - * - * @param {Array} items - The items to add - */ -function addQuickSettingsItems(items) { - // Add the items with the built-in function - QuickSettingsMenu._addItems(items); - - // Ensure the tile(s) are above the background apps menu - for (const item of items) { - QuickSettingsMenu.menu._grid.set_child_below_sibling( - item, - QuickSettingsMenu._backgroundApps.quickSettingsItems[0] +class FeatureIndicator extends QuickSettings.SystemIndicator { + _init(extensionObject, icon) { + super._init(); + + // Create the icon for the indicator + this._indicator = this._addIndicator(); + this._indicator.gicon = icon; + + // Showing the indicator when the feature is enabled + this._settings = extensionObject.getSettings(); + this._settings.bind( + 'kmonad-running', + this._indicator, + 'visible', + Gio.SettingsBindFlags.DEFAULT ); } } +); -class Extension { - constructor(uuid) { - this._uuid = uuid; - - this._indicator = null; - - this._settings = ExtensionUtils.getSettings(); +export default class KMonadToggleExtension extends Extension { + enable() { this._cancellable = new Gio.Cancellable(); + this._settings = this.getSettings(); // Watch for changes to a specific setting - this._settings.connect('changed::kmonad-running', (settings, key) => { + this._settings.connect('changed::kmonad-running', async (settings, key) => { const isEnabled = settings.get_boolean(key); - if (isEnabled) - this.startKmonad(); - else + if (isEnabled) { + await this.startKmonad(); + } else { this._cancellable.cancel(); + this._cancellable.reset(); + } }); - } - /** - * This function is called when your extension is enabled, which could be - * done in GNOME Extensions, when you log in or when the screen is unlocked. - * - * This is when you should setup any UI for your extension, change existing - * widgets, connect signals or modify GNOME Shell's behaviour. - */ - enable() { - this._settings.set_boolean('kmonad-running', false); - this._indicator = new KMonadIndicator(); + this._indicator = new KMonadIndicator(this, this.getIcon()); + this._indicator.quickSettingsItems.push(new KMonadToggle(this, this.getIcon())); + Main.panel.statusArea.quickSettings.addExternalIndicator(this._indicator); + if (this._settings.get_boolean('autostart-kmonad')) this._settings.set_boolean('kmonad-running', true); } - /** - * This function is called when your extension is uninstalled, disabled in - * GNOME Extensions, when you log out or when the screen locks. - * - * Anything you created, modified or setup in enable() MUST be undone here. - * Not doing so is the most common reason extensions are rejected in review! - */ disable() { + this._indicator.quickSettingsItems.forEach(item => item.destroy()); this._indicator.destroy(); this._indicator = null; this._cancellable.cancel(); + this._cancellable = null; + this._settings = null; } /** * Starts the kmonad process */ - startKmonad() { - this._cancellable.cancel(); - this._cancellable.reset(); + async startKmonad() { const command = GLib.shell_parse_argv( this._settings.get_string('kmonad-command') )[1]; - execCommunicate(command, null, this._cancellable) - .catch(e => { - if (!this._cancellable.is_cancelled()) { - logError(e); - Main.notifyError(_('KMonad failed'), e.message.trim()); - } - }) - .then(() => { - this._settings.set_boolean('kmonad-running', false); - }); + try { + await execCommunicate(command, null, this._cancellable); + } catch (e) { + if (!this._cancellable.is_cancelled()) + Main.notifyError(_('KMonad failed'), e.message.trim()); + } finally { + this._settings.set_boolean('kmonad-running', false); + } + } + + getIcon() { + return Gio.icon_new_for_string( + GLib.build_filenamev([this.path, 'icons', 'kmonad-symbolic.svg']) + ); } } @@ -238,19 +180,3 @@ function execCommunicate(argv, input = null, cancellable = null) { }); }); } - -/** - * This function is called once when your extension is loaded, not enabled. This - * is a good time to setup translations or anything else you only do once. - * - * You MUST NOT make any changes to GNOME Shell, connect any signals or add any - * MainLoop sources here. - * - * @param {ExtensionMeta} meta - An extension meta object, described below. - * @returns {object} an object with enable() and disable() methods - */ -function init(meta) { - ExtensionUtils.initTranslations(); - - return new Extension(meta.uuid); -} diff --git a/metadata.json b/metadata.json index 9965a8c..e4555d7 100644 --- a/metadata.json +++ b/metadata.json @@ -2,7 +2,7 @@ "name": "KMonad Toggle", "description": "Control KMonad directly from GNOME Shell!\n\nThis extension allows you to:\n • Autostart KMonad on login\n • Quickly check if KMonad is running from the top bar\n • Toggle KMonad on or off with a quick setting\n • Easily configure the KMonad launch command\n\nNote: This extension does not manage the KMonad installation. See the installation guide and FAQ in the KMonad repo for instructions on how to set it up. https://github.com/kmonad/kmonad", "uuid": "kmonad-toggle@jurf.github.io", - "shell-version": ["44"], + "shell-version": ["45"], "settings-schema": "org.gnome.shell.extensions.kmonad-toggle", "gettext-domain": "kmonad-toggle", "url": "https://github.com/jurf/gnome-kmonad-toggle", diff --git a/prefs.js b/prefs.js index 458c55f..8765ec1 100644 --- a/prefs.js +++ b/prefs.js @@ -20,59 +20,41 @@ 'use strict'; -const {Adw, Gio, Gtk} = imports.gi; - -const ExtensionUtils = imports.misc.extensionUtils; - -const _ = ExtensionUtils.gettext; - -/** - * Like `extension.js` this is used for any one-time setup like translations. - */ -function init() { - ExtensionUtils.initTranslations(); -} - -/** - * This function is called when the preferences window is first created to fill - * the `Adw.PreferencesWindow`. - * - * This function will only be called by GNOME 42 and later. If this function is - * present, `buildPrefsWidget()` will NOT be called. - * - * @param {Adw.PreferencesWindow} window - The preferences window - */ -function fillPreferencesWindow(window) { - const settings = ExtensionUtils.getSettings(); - - const page = new Adw.PreferencesPage(); - window.add(page); - - const group = new Adw.PreferencesGroup({ - description: _( - 'This extension does not manage the KMonad installation.' + - ' See the Installation guide' + - ' and FAQ' + - ' for instructions on how to set it up.' - ), - }); - page.add(group); - - group.add(createSettingsToggle(settings, 'kmonad-running', 'Start KMonad now')); - group.add(createSettingsToggle(settings, 'autostart-kmonad', 'Autostart KMonad')); - - const kmonadGroup = new Adw.PreferencesGroup({ - title: _('KMonad configuration'), - description: _( - 'Shell expansion is not supported, so please use absolute paths.' - ), - }); - page.add(kmonadGroup); - - kmonadGroup.add(createSettingsEntry(settings, 'kmonad-command', 'Custom command')); - - // Make sure the window doesn't outlive the settings object - window._settings = settings; +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; + +import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; + +export default class MyExtensionPreferences extends ExtensionPreferences { + fillPreferencesWindow(window) { + window._settings = this.getSettings(); + + const page = new Adw.PreferencesPage(); + window.add(page); + + const group = new Adw.PreferencesGroup({ + description: _( + 'This extension does not manage the KMonad installation.' + + ' See the Installation guide' + + ' and FAQ' + + ' for instructions on how to set it up.' + ), + }); + page.add(group); + + group.add(createSettingsToggle(window._settings, 'kmonad-running', 'Start KMonad now')); + group.add(createSettingsToggle(window._settings, 'autostart-kmonad', 'Autostart KMonad')); + + const kmonadGroup = new Adw.PreferencesGroup({ + title: _('KMonad configuration'), + description: _( + 'Shell expansion is not supported, so please use absolute paths.' + ), + }); + page.add(kmonadGroup); + + kmonadGroup.add(createSettingsEntry(window._settings, 'kmonad-command', 'Custom command')); + } } /** @@ -85,16 +67,12 @@ function fillPreferencesWindow(window) { * @returns {Adw.ActionRow} The row */ function createSettingsToggle(settings, key, title) { - const row = new Adw.ActionRow({title: _(title)}); - - const toggle = new Gtk.Switch({ - active: settings.get_boolean(key), - valign: Gtk.Align.CENTER, + const row = new Adw.SwitchRow({ + title, }); - settings.bind(key, toggle, 'active', + + settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT); - row.add_suffix(toggle); - row.activatable_widget = toggle; return row; } From 2154ab1aa1e1686bfc8d98aa2383a17d1031bf6e Mon Sep 17 00:00:00 2001 From: Juraj Fiala Date: Fri, 10 Nov 2023 13:48:30 +0100 Subject: [PATCH 2/2] fix: Fix race condition, clean up signals --- extension.js | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/extension.js b/extension.js index e27cf62..dc69b1b 100644 --- a/extension.js +++ b/extension.js @@ -73,17 +73,16 @@ class FeatureIndicator extends QuickSettings.SystemIndicator { export default class KMonadToggleExtension extends Extension { enable() { - this._cancellable = new Gio.Cancellable(); - this._settings = this.getSettings(); // Watch for changes to a specific setting - this._settings.connect('changed::kmonad-running', async (settings, key) => { + this._handlerId = this._settings.connect('changed::kmonad-running', async (settings, key) => { const isEnabled = settings.get_boolean(key); if (isEnabled) { + this._cancellable = new Gio.Cancellable(); await this.startKmonad(); - } else { + } else if (this._cancellable) { this._cancellable.cancel(); - this._cancellable.reset(); + this._cancellable = null; } }); @@ -91,17 +90,25 @@ export default class KMonadToggleExtension extends Extension { this._indicator.quickSettingsItems.push(new KMonadToggle(this, this.getIcon())); Main.panel.statusArea.quickSettings.addExternalIndicator(this._indicator); + this._settings.set_boolean('kmonad-running', false); if (this._settings.get_boolean('autostart-kmonad')) this._settings.set_boolean('kmonad-running', true); } disable() { - this._indicator.quickSettingsItems.forEach(item => item.destroy()); - this._indicator.destroy(); - this._indicator = null; - this._cancellable.cancel(); - this._cancellable = null; - this._settings = null; + if (this._indicator) { + this._indicator.quickSettingsItems.forEach(item => item.destroy()); + this._indicator.destroy(); + this._indicator = null; + } + if (this._cancellable) { + this._cancellable.cancel(); + this._cancellable = null; + } + if (this._handlerId) { + this._settings.disconnect(this._handlerId); + this._handlerId = null; + } } /** @@ -114,7 +121,7 @@ export default class KMonadToggleExtension extends Extension { try { await execCommunicate(command, null, this._cancellable); } catch (e) { - if (!this._cancellable.is_cancelled()) + if (this._cancellable?.is_cancelled() === false) Main.notifyError(_('KMonad failed'), e.message.trim()); } finally { this._settings.set_boolean('kmonad-running', false);