From 99b90377c553bea6af0cb9493251263e7288302e Mon Sep 17 00:00:00 2001 From: Simon Schneegans Date: Mon, 30 Jan 2023 13:57:33 +0100 Subject: [PATCH] :tada: Implement profile selection --- extension.js | 107 +++++++++++++----- prefs.js | 56 +++++++-- resources/ui/adw/prefs.ui | 35 ++++-- ...nsions.burn-my-windows-profile.gschema.xml | 18 ++- src/ProfileManager.js | 21 +++- 5 files changed, 185 insertions(+), 52 deletions(-) diff --git a/extension.js b/extension.js index 7ab215f..431c11c 100644 --- a/extension.js +++ b/extension.js @@ -32,6 +32,7 @@ const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; const ProfileManager = Me.imports.src.ProfileManager.ProfileManager; +const WindowPicker = Me.imports.src.WindowPicker.WindowPicker; ////////////////////////////////////////////////////////////////////////////////////////// // This extensions modifies the window-close and window-open animations with all kinds // @@ -90,6 +91,13 @@ class Extension { this._loadProfiles(); + // This is used to get the desktop's color scheme. + this._shellSettings = new Gio.Settings({schema: 'org.gnome.desktop.interface'}); + + // Enable the window-picking D-Bus API for the preferences dialog. + this._windowPicker = new WindowPicker(); + this._windowPicker.export(); + // We will use extensionThis to refer to the extension inside the patched methods. const extensionThis = this; @@ -348,6 +356,9 @@ class Extension { // Unregister our resources. Gio.resources_unregister(this._resources); + // Disable the window-picking D-Bus API. + this._windowPicker.unexport(); + // Restore the original window-open and window-close animations. global.window_manager.disconnect(this._destroyConnection); global.display.disconnect(this._windowCreatedConnection); @@ -373,41 +384,50 @@ class Extension { // ----------------------------------------------------------------------- private stuff + // This loads all effect profiles and assigns a priority to each profile. Whenever a + // window is opened or closed, the matching effect profile with the highest priority + // will be chosen. + // This method is called whenever the currently edited profile in the/ preferences + // dialog changes. This is most likely a bit too often, but it will also happen whenever + // a new profile is created and whenever an old profile is deleted. _loadProfiles() { + + // Get all currently available profiles. const profileManager = new ProfileManager(); + this._profiles = profileManager.getProfiles(); - utils.debug('load profiles!'); - + // Whenever the properties of a profile is changed in the settings, we may have to + // resort all profiles according to their priority. const updatePriority = (p) => { - utils.debug('sort profiles!'); p.priority = profileManager.getProfilePriority(p.settings); this._profiles.sort((a, b) => b.priority - a.priority); }; - this._profiles = profileManager.getProfiles(); + // For each profile, assign an initial priority and update the priority whenever a + // related setting changes. this._profiles.forEach(p => { + p.priority = profileManager.getProfilePriority(p.settings); + p.settings.connect('changed::profile-app', () => updatePriority(p)); p.settings.connect('changed::profile-animation-type', () => updatePriority(p)); p.settings.connect('changed::profile-window-type', () => updatePriority(p)); - p.settings.connect('changed::profile-desktop-style', () => updatePriority(p)); + p.settings.connect('changed::profile-color-scheme', () => updatePriority(p)); p.settings.connect('changed::profile-power-mode', () => updatePriority(p)); p.settings.connect('changed::profile-power-profile', () => updatePriority(p)); - - p.priority = profileManager.getProfilePriority(p.settings); }); - utils.debug('sort profiles!'); + // Sort all profiles initially according to their initial priority. this._profiles.sort((a, b) => b.priority - a.priority); } - // This method adds one of the configured effects to the given actor. If forOpening is - // set to true, a effect from the enabled window-open animations is chosen, else an - // enabled window-close animation is used. This will also tweak the transitions of the - // given actor (e.g. scale it up if required). + // This method adds one of the configured effects to the given actor. First, a profile + // matching the current circumstances is chosen. Then a random effect from its enabled + // effects will be selected. This will also tweak the transitions of the given actor + // (e.g. scale it up if required). _setupEffect(actor, forOpening) { // In case we return early, make sure that the animation times are reset properly. this._fixAnimationTimes(forOpening, null); - // Only add effects to normal windows and dialog windows. + // For now, we only add effects to normal windows and dialog windows. const isNormalWindow = actor.meta_window.window_type == Meta.WindowType.NORMAL; const isDialogWindow = actor.meta_window.window_type == Meta.WindowType.MODAL_DIALOG || @@ -417,20 +437,6 @@ class Extension { return; } - - // TODO!. - // We may have to do nothing if running on battery power or if in power-save mode. - // let disableOnBattery = - // this._settings.get_boolean('disable-on-battery') && this._upowerProxy.OnBattery; - // let disableOnPowerSave = this._settings.get_boolean('disable-on-power-save') && - // this._powerProfilesProxy && this._powerProfilesProxy.ActiveProfile == - // 'power-saver'; - - // if (!skip! && previewNick == '') { - // return; - // } - - // There is the weird case where an animation is already ongoing. This happens when a // window is closed which has been created before the session was started (e.g. when // GNOME Shell has been restarted in the meantime). @@ -441,7 +447,7 @@ class Extension { oldShader.returnToFactory(); } - // ------------------------------------------------------------------ choose an effect + // ----------------------------------------------- choose a profile and then an effect // Usually, we use the effect profile with the highest priority which matches the // current circumstances. From this profile, we choose a random effect. However, if an @@ -468,22 +474,62 @@ class Extension { // effect. else { + // These numbers match the indices in the Gtk.StringLists defined in the UI files + // (e.g. resources/ui/adw/prefs.ui). const animationType = forOpening ? 1 : 2; const windowType = isNormalWindow ? 1 : 2; const powerMode = this._upowerProxy.OnBattery ? 1 : 2; + // Get the first profile whose constraints match the circumstances. The list is + // sorted by priority, so we are good to take the first match. profile = this._profiles.find(p => { + const profileApp = p.settings.get_string('profile-app'); const profileAnimationType = p.settings.get_int('profile-animation-type'); const profileWindowType = p.settings.get_int('profile-window-type'); - const profileDesktopStyle = p.settings.get_int('profile-desktop-style'); const profilePowerMode = p.settings.get_int('profile-power-mode'); + const profileColorScheme = p.settings.get_int('profile-color-scheme'); const profilePowerProfile = p.settings.get_int('profile-power-profile'); - return (profileAnimationType == 0 || profileAnimationType == animationType) && + // First we check whether the animation type, window type, and power mode are + // matching. + let matches = + (profileAnimationType == 0 || profileAnimationType == animationType) && (profileWindowType == 0 || profileWindowType == windowType) && (profilePowerMode == 0 || profilePowerMode == powerMode); + + // If that was the case, we also check the application name. + if (matches && profileApp != '') { + const app = actor.meta_window.get_wm_class().toLowerCase(); + matches = app == profileApp.toLowerCase(); + } + + // If the profile is still matching, we also check the color scheme. + if (matches && profileColorScheme != 0 && utils.shellVersionIsAtLeast(42, 0)) { + const colorScheme = this._shellSettings.get_string('color-scheme'); + matches &= (profileColorScheme == 1 && colorScheme == 'default') || + (profileColorScheme == 2 && colorScheme == 'prefer-dark'); + } + + // Finally, we may also have to check the power profile. + if (matches && profilePowerProfile != 0 && this._powerProfilesProxy) { + const powerProfile = this._powerProfilesProxy.ActiveProfile; + + // To understand the numbers, please refer to the indices in the Gtk.StringList + // of the profile-power-profile Adw.ComboRow in resources/ui/adw/prefs.ui. + if (powerProfile == 'power-saver') { + matches &= profilePowerProfile == 1 || profilePowerProfile == 4; + } else if (powerProfile == 'balanced') { + matches &= profilePowerProfile == 2 || profilePowerProfile == 4 || + profilePowerProfile == 5; + } else { + matches &= profilePowerProfile == 3 || profilePowerProfile == 5; + } + } + + return matches; }); + // If we found a matching profile, choose a random effect from it. if (profile) { // Create a list of all enabled effects of this profile. @@ -503,6 +549,7 @@ class Extension { return; } + // ----------------------------------------------------------- tweak actor transitions // If we are currently performing integration test, all animations are set to a fixed diff --git a/prefs.js b/prefs.js index 40f889a..61d2ac0 100644 --- a/prefs.js +++ b/prefs.js @@ -131,6 +131,29 @@ var PreferencesDialog = class PreferencesDialog { this._builder.get_object('profile-power-profile').set_visible(hasPowerProfiles); + // The global color scheme is only available starting with GNOME Shell 42. We hide the + // corresponding settings row on older versions. + this._builder.get_object('profile-color-scheme') + .set_visible(utils.shellVersionIsAtLeast(42, 0)); + + this._builder.get_object('profile-choose-app-button').connect('clicked', () => { + Gio.DBus.session.call('org.gnome.Shell', + '/org/gnome/shell/extensions/BurnMyWindows', + 'org.gnome.shell.extensions.BurnMyWindows', 'PickWindow', + null, null, Gio.DBusCallFlags.NO_AUTO_START, -1, null, null); + }); + + this._dbusConnection = Gio.DBus.session.signal_subscribe( + 'org.gnome.Shell', 'org.gnome.shell.extensions.BurnMyWindows', 'WindowPicked', + '/org/gnome/shell/extensions/BurnMyWindows', null, Gio.DBusSignalFlags.NONE, + (conn, sender, obj_path, iface, signal, params) => { + const val = params.get_child_value(0).get_string()[0]; + + if (val != 'window-not-found') { + this._builder.get_object('profile-app').text = val; + } + }); + // Starting with GNOME Shell 42, the settings dialog uses libadwaita. We use this to // create a completely different layout for the settings dialog. if (utils.isADW()) { @@ -479,6 +502,9 @@ var PreferencesDialog = class PreferencesDialog { this._widget.connect('destroy', () => { // Unregister our resources. Gio.resources_unregister(this._resources); + + // Disconnect from the window-picker API. + Gio.DBus.session.signal_unsubscribe(this._dbusConnection); }); // Show the widgets on GTK3. @@ -507,10 +533,16 @@ var PreferencesDialog = class PreferencesDialog { // Connects a Gtk.ComboBox (or anything else which has an 'selected' property) to a // settings key. It also binds the corresponding reset button. - bindComboRow(settingsKey) { + bindComboBox(settingsKey) { this._bind(settingsKey, 'selected'); } + // Connects a Gtk.Entry (or anything else which has an 'text' property) to a + // settings key. It also binds the corresponding reset button. + bindEntry(settingsKey) { + this._bind(settingsKey, 'text'); + } + // Connects a Gtk.Adjustment (or anything else which has a 'value' property) to a // settings key. It also binds the corresponding reset button. bindAdjustment(settingsKey) { @@ -591,19 +623,27 @@ var PreferencesDialog = class PreferencesDialog { } // Connect all profile options. - const setupProfileOption = (settingsKey) => { - this.bindComboRow(settingsKey); + const updateProfileButtonMenuIfChanged = (settingsKey) => { this._connectProfileSetting('changed::' + settingsKey, () => { this._updateProfileButton(); this._updateProfileMenu(); }); }; - setupProfileOption('profile-animation-type'); - setupProfileOption('profile-window-type'); - setupProfileOption('profile-desktop-style'); - setupProfileOption('profile-power-mode'); - setupProfileOption('profile-power-profile'); + updateProfileButtonMenuIfChanged('profile-app'); + updateProfileButtonMenuIfChanged('profile-animation-type'); + updateProfileButtonMenuIfChanged('profile-window-type'); + updateProfileButtonMenuIfChanged('profile-color-scheme'); + updateProfileButtonMenuIfChanged('profile-power-mode'); + updateProfileButtonMenuIfChanged('profile-power-profile'); + + this.bindEntry('profile-app'); + this.bindComboBox('profile-animation-type'); + this.bindComboBox('profile-window-type'); + this.bindComboBox('profile-color-scheme'); + this.bindComboBox('profile-power-mode'); + this.bindComboBox('profile-power-profile'); + this.bindSwitch('profile-high-priority'); // Connect all effect settings. this._ALL_EFFECTS.forEach(effect => { diff --git a/resources/ui/adw/prefs.ui b/resources/ui/adw/prefs.ui index f56fd50..c16d58c 100644 --- a/resources/ui/adw/prefs.ui +++ b/resources/ui/adw/prefs.ui @@ -70,12 +70,12 @@ SPDX-License-Identifier: GPL-3.0-or-later - Edit Effect Profile - Here you can specify under which circumstances the current effect settings should be applied. + Effect Profile Constraints + Define when the current effect settings should be used. - Choose an Application + Application Name bmw-select-app-symbolic @@ -116,14 +116,14 @@ SPDX-License-Identifier: GPL-3.0-or-later - - Desktop Style + + Color Scheme Any - Bright Mode - Dark Mode + Default Color Scheme + Dark Color Scheme @@ -166,6 +166,27 @@ SPDX-License-Identifier: GPL-3.0-or-later + + + Advanced Profile Options + + + + High Priority + Force using this profile even if it has less constraints than another matching profile. + 1 + profile-high-priority + + + center + + + + + + + + prefs.profile-delete diff --git a/schemas/org.gnome.shell.extensions.burn-my-windows-profile.gschema.xml b/schemas/org.gnome.shell.extensions.burn-my-windows-profile.gschema.xml index 917cc36..639191d 100644 --- a/schemas/org.gnome.shell.extensions.burn-my-windows-profile.gschema.xml +++ b/schemas/org.gnome.shell.extensions.burn-my-windows-profile.gschema.xml @@ -13,6 +13,12 @@ SPDX-License-Identifier: CC0-1.0 + + "" + Profile App + The application name for which the current effect settings should be used. + + 0 @@ -27,11 +33,11 @@ SPDX-License-Identifier: CC0-1.0 The window types for which the current effect settings should be used. - + 0 - Profile Desktop Style - The desktop styles for which the current effect settings should be used. + Profile Color Scheme + The desktop color schemes for which the current effect settings should be used. @@ -48,6 +54,12 @@ SPDX-License-Identifier: CC0-1.0 The power profiles in which the current effect settings should be used. + + false + Profile High Priority + Force using this profile even if it has less constraints than another matching profile. + + diff --git a/src/ProfileManager.js b/src/ProfileManager.js index 3152386..6801df7 100644 --- a/src/ProfileManager.js +++ b/src/ProfileManager.js @@ -32,7 +32,7 @@ const utils = Me.imports.src.utils; ////////////////////////////////////////////////////////////////////////////////////////// var ProfileManager = class { - // ------------------------------------------------------------ constructor / destructor + // ------------------------------------------------------------------------- constructor constructor() { this._makeProfilesDir(); @@ -128,12 +128,17 @@ var ProfileManager = class { getProfilePriority(settings) { let priority = 0; + // Each setting which is not set to its default value increases the priority by one. + if (settings.get_string('profile-app') != '') ++priority; if (settings.get_int('profile-animation-type') > 0) ++priority; if (settings.get_int('profile-window-type') > 0) ++priority; - if (settings.get_int('profile-desktop-style') > 0) ++priority; + if (settings.get_int('profile-color-scheme') > 0) ++priority; if (settings.get_int('profile-power-mode') > 0) ++priority; if (settings.get_int('profile-power-profile') > 0) ++priority; + // Increase the priority significantly for high-priority profiles. + if (settings.get_boolean('profile-high-priority')) priority += 100; + return priority; } @@ -142,6 +147,14 @@ var ProfileManager = class { getProfileName(settings) { let items = []; + // If an app is configured, use it as first component for the profile's name. + const app = settings.get_string('profile-app'); + if (app != '') { + items.push(app); + } + + // Now add components to the name for each non-default profile option. Make sure that + // these are the same strings as used in the UI files! const addComponent = (settingsKey, options) => { const option = settings.get_int(settingsKey); if (option > 0) { @@ -154,8 +167,8 @@ var ProfileManager = class { _('Closing Windows')]); addComponent('profile-window-type', [_('Normal Windows'), _('Dialog Windows')]); - addComponent('profile-desktop-style', [_('Bright Mode'), - _('Dark Mode')]); + addComponent('profile-color-scheme', [_('Default Color Scheme'), + _('Dark Color Scheme')]); addComponent('profile-power-mode', [_('On Battery'), _('Plugged In')]); addComponent('profile-power-profile', [_('Power-Saver Mode'),