diff --git a/prefs.js b/prefs.js index 37e6c68..7154ce5 100644 --- a/prefs.js +++ b/prefs.js @@ -29,6 +29,7 @@ const _ = imports.gettext.domain('burn-my-windows').gettext; const ExtensionUtils = imports.misc.extensionUtils; const Me = imports.misc.extensionUtils.getCurrentExtension(); const utils = Me.imports.src.utils; +const ProfileManager = Me.imports.src.ProfileManager.ProfileManager; // This template widget class is defined at the bottom of this file. var BurnMyWindowsEffectPage = null; @@ -102,10 +103,13 @@ var PreferencesDialog = class PreferencesDialog { // Store a reference to the general settings object. this._settings = ExtensionUtils.getSettings(); + // This tracks all available effect profiles. + this._profileManager = new ProfileManager(); + this._profileSettingsConnections = []; + // Load the current effect profile. If there is none, we create a default profile. this._settings.connect('changed::active-profile', () => { this._loadActiveProfile(); - this._updateProfileButton(); }); // Check whether the power profiles daemon is available - if not, we hide the @@ -266,7 +270,7 @@ var PreferencesDialog = class PreferencesDialog { window.set_title(`Burn-My-Windows ${Me.metadata.version}`); this._loadActiveProfile(); - this._updateProfileButton(); + this._updateProfileMenu(); // Populate the menu with actions. const group = Gio.SimpleActionGroup.new(); @@ -417,14 +421,21 @@ var PreferencesDialog = class PreferencesDialog { // Setup all the profile-related actions. { - const profileAction = this._settings.create_action('active-profile'); - group.add_action(profileAction); + const activeProfileAction = this._settings.create_action('active-profile'); + group.add_action(activeProfileAction); const newProfileAction = Gio.SimpleAction.new('profile-new', null); newProfileAction.connect('activate', () => { - const profile = utils.createProfile(); - this._settings.set_string('active-profile', profile); + // Create a new profile and make it the active one. + const profile = this._profileManager.createProfile(); + this._settings.set_string('active-profile', profile.path); + + // Start editing the profile. this._builder.get_object('edit-profile-button').active = true; + + // The number of profiles has changed, so we have to update the profile + // selection menu. + this._updateProfileMenu(); }); group.add_action(newProfileAction); @@ -437,11 +448,19 @@ var PreferencesDialog = class PreferencesDialog { deleteProfileDialog.set_transient_for(window); deleteProfileDialog.connect('response', (d, response) => { if (response == 'delete') { + // Stop editing the current profile. this._builder.get_object('edit-profile-button').active = false; - const profile = this._settings.get_string('active-profile'); - utils.deleteProfile(profile); - this._loadActiveProfile(); - this._updateProfileButton(); + + // Delete the currently active profile. + this._profileManager.deleteProfile(this._activeProfile.path); + + // Select another one. + this._settings.set_string('active-profile', + this._profileManager.getProfiles()[0].path); + + // The number of profiles has changed, so we have to update the profile + // selection menu. + this._updateProfileMenu(); } }); @@ -472,7 +491,7 @@ var PreferencesDialog = class PreferencesDialog { // Returns a Gio.Settings object for the current effect profile. getProfileSettings() { - return this._profileSettings; + return this._activeProfile.settings; } // Returns the widget used for the settings of this extension. @@ -508,18 +527,23 @@ var PreferencesDialog = class PreferencesDialog { if (button) { // Update the settings when the color is modified. - button.connect('color-set', () => { - this._profileSettings.set_string(settingsKey, button.get_rgba().to_string()); - }); + if (!button._isConnected) { + button.connect('color-set', () => { + this.getProfileSettings().set_string(settingsKey, + button.get_rgba().to_string()); + }); + + button._isConnected = true; + } // Update the button state when the settings change. const settingSignalHandler = () => { const rgba = new Gdk.RGBA(); - rgba.parse(this._profileSettings.get_string(settingsKey)); + rgba.parse(this.getProfileSettings().get_string(settingsKey)); button.rgba = rgba; }; - this._profileSettings.connect('changed::' + settingsKey, settingSignalHandler); + this._connectProfileSetting('changed::' + settingsKey, settingSignalHandler); // Initialize the button with the state in the settings. settingSignalHandler(); @@ -543,26 +567,27 @@ var PreferencesDialog = class PreferencesDialog { _loadActiveProfile() { - let profiles = utils.listProfiles(); - if (profiles.length == 0) { - profiles = [utils.createProfile()]; - } + this._disconnectProfileSettings(); - let activeProfile = this._settings.get_string('active-profile'); - if (!profiles.includes(activeProfile)) { - activeProfile = profiles[0]; - this._settings.set_string('active-profile', activeProfile); + const allProfiles = this._profileManager.getProfiles(); + const path = this._settings.get_string('active-profile'); + this._activeProfile = allProfiles.find(p => p.path == path); + + if (!this._activeProfile) { + utils.debug('active profile not found!!!'); + this._activeProfile = allProfiles[0]; + this._settings.set_string('active-profile', this._activeProfile.path); return; } - utils.debug('loading ' + activeProfile); - - this._profileSettings = utils.getProfileSettings(activeProfile); + utils.debug('loading ' + this._activeProfile.path); const setupProfileOption = (settingsKey) => { this.bindComboRow(settingsKey); - this._profileSettings.connect('changed::' + settingsKey, - () => this._updateProfileButton()); + this._connectProfileSetting('changed::' + settingsKey, () => { + this._updateProfileButton(); + this._updateProfileMenu(); + }); }; setupProfileOption('profile-animation-type'); @@ -579,36 +604,51 @@ var PreferencesDialog = class PreferencesDialog { } }); + this._updateProfileButton(); + utils.debug('loading done'); } _updateProfileButton() { - const activeProfile = this._settings.get_string('active-profile'); - + utils.debug('updating profile button'); this._builder.get_object('choose-profile-button').label = - utils.getProfileName(activeProfile); + this._profileManager.getProfileName(this.getProfileSettings()); + } + _updateProfileMenu() { + utils.debug('updating profile menu'); const profileSection = this._builder.get_object('profile-section'); profileSection.remove_all(); - const profiles = utils.listProfiles(); + const profiles = this._profileManager.getProfiles(); profiles.forEach(profile => { - const label = utils.getProfileName(profile); + const label = this._profileManager.getProfileName(profile.settings); const item = Gio.MenuItem.new(label, 'prefs.active-profile'); item.set_action_and_target_value('prefs.active-profile', - GLib.Variant.new_string(profile)); + GLib.Variant.new_string(profile.path)); profileSection.append_item(item); }); } + _connectProfileSetting(key, callback) { + this._profileSettingsConnections.push( + this.getProfileSettings().connect(key, callback)); + } + + _disconnectProfileSettings() { + this._profileSettingsConnections.forEach(c => + this.getProfileSettings().disconnect(c)); + this._profileSettingsConnections = []; + } + // Searches for a reset button for the given settings key and make it reset the settings // key when clicked. _bindResetButton(settingsKey) { const resetButton = this._builder.get_object('reset-' + settingsKey); if (resetButton && !resetButton._isConnected) { resetButton.connect('clicked', () => { - this._profileSettings.reset(settingsKey); + this.getProfileSettings().reset(settingsKey); }); resetButton._isConnected = true; } @@ -621,8 +661,8 @@ var PreferencesDialog = class PreferencesDialog { if (object) { Gio.Settings.unbind(object, property); - this._profileSettings.bind(settingsKey, object, property, - Gio.SettingsBindFlags.DEFAULT); + this.getProfileSettings().bind(settingsKey, object, property, + Gio.SettingsBindFlags.DEFAULT); } this._bindResetButton(settingsKey); diff --git a/schemas/org.gnome.shell.extensions.burn-my-windows.gschema.xml b/schemas/org.gnome.shell.extensions.burn-my-windows.gschema.xml index de978d0..c8a7593 100644 --- a/schemas/org.gnome.shell.extensions.burn-my-windows.gschema.xml +++ b/schemas/org.gnome.shell.extensions.burn-my-windows.gschema.xml @@ -15,7 +15,7 @@ SPDX-License-Identifier: CC0-1.0 "" - Profile + Active Profile The currently active effect profile. diff --git a/src/ProfileManager.js b/src/ProfileManager.js new file mode 100644 index 0000000..ce1ea3f --- /dev/null +++ b/src/ProfileManager.js @@ -0,0 +1,190 @@ +////////////////////////////////////////////////////////////////////////////////////////// +// ) ( // +// ( /( ( ( ) ( ( ( ( )\ ) ( ( // +// )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( // +// ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ // +// | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) // +// | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< // +// |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ // +// |__/ // +////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: Simon Schneegans +// SPDX-License-Identifier: GPL-3.0-or-later + +'use strict'; + +const {Gio, GLib, GObject} = imports.gi; + +const _ = imports.gettext.domain('burn-my-windows').gettext; + +const ExtensionUtils = imports.misc.extensionUtils; +const Me = imports.misc.extensionUtils.getCurrentExtension(); +const utils = Me.imports.src.utils; + +// extension.js: +// - get a list of profile settings objects + +// prefs.js: +// - get a (sorted) list of profile settings objects +// - add a new profile +// - delete a profile +// - create a name for a given profile + +var ProfileManager = class { + // ------------------------------------------------------------ constructor / destructor + + constructor() { + this._makeProfilesDir(); + this._loadProfiles(); + } + + // -------------------------------------------------------------------- public interface + + // Returns an array of effect profiles. Each effect profile is an object with two + // properties: The 'path' points to the file which contains the effect profile + // settings. The 'settings' property contains a Gio.Settings object for the profile. + // The former can be used to uniquely identify the profile, the latter can be used to + // access the profile's settings. + // This method will always return at least one profile. If none is configured, a default + // profile will be created. + getProfiles() { + if (this._profiles.length == 0) { + this._profiles.push(this._createProfile()); + } + + return this._profiles; + } + + // This will create a new profile with all values initialized to their defaults. The + // method will return a effect profile object (as described above). It will also + // trigger an emission of the 'profiles-updated' signal. + createProfile() { + const path = `${GLib.get_user_config_dir()}/burn-my-windows/profiles/${ + GLib.get_real_time()}.conf`; + const file = Gio.File.new_for_path(path); + file.create(Gio.FileCreateFlags.NONE, null); + + const profile = {'path': path, 'settings': this._getProfileSettings(path)}; + + this._profiles.push(profile); + + return profile; + } + + deleteProfile(profilePath) { + const profileDir = `${GLib.get_user_config_dir()}/burn-my-windows/profiles`; + const file = Gio.File.new_for_path(profilePath); + if (file.has_prefix(Gio.File.new_for_path(profileDir)) && file.query_exists(null)) { + file.delete(null); + } + + this._profiles = this._profiles.filter(p => p.path != profilePath); + } + + getProfileName(settings) { + let items = []; + + const addComponent = (settingsKey, options) => { + const option = settings.get_int(settingsKey); + if (option > 0) { + items.push(options[option - 1]); + } + }; + + // clang-format off + addComponent('profile-animation-type', [_('Opening Windows'), + _('Closing Windows')]); + addComponent('profile-window-type', [_('Normal Windows'), + _('Dialog Windows')]); + addComponent('profile-desktop-style', [_('Bright Mode'), + _('Dark Mode')]); + addComponent('profile-power-mode', [_('On Battery'), + _('Plugged In')]); + addComponent('profile-power-profile', [_('Power-Saver Mode'), + _('Balanced Mode'), + _('Performance Mode'), + _('Power Saver or Balanced'), + _('Balanced or Performance')]); + // clang-format on + + let label = ''; + + if (items.length == 0) { + label = _('Default Profile'); + } else { + label = items.join(' · '); + } + + return label; + } + + // ----------------------------------------------------------------------- private stuff + + _makeProfilesDir() { + const path = `${GLib.get_user_config_dir()}/burn-my-windows/profiles`; + const dir = Gio.File.new_for_path(path); + + if (!dir.query_exists(null)) { + dir.make_directory_with_parents(null); + } + } + + _loadProfiles() { + const dir = + Gio.File.new_for_path(GLib.get_user_config_dir() + '/burn-my-windows/profiles'); + + const files = + dir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null); + + let profilePaths = []; + + while (true) { + const presetInfo = files.next_file(null); + + if (presetInfo == null) { + break; + } + + if (presetInfo.get_file_type() == Gio.FileType.REGULAR) { + const suffixPos = presetInfo.get_display_name().indexOf('.conf'); + if (suffixPos > 0) { + profilePaths.push(dir.get_child(presetInfo.get_name()).get_path()); + } + } + } + + + profilePaths.sort(); + + this._profiles = []; + + profilePaths.forEach(path => { + this._profiles.push({'path': path, 'settings': this._getProfileSettings(path)}); + }); + } + + // Based on + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/misc/extensionUtils.js#L213 + _getProfileSettings(profilePath) { + + // Expect USER extensions to have a schemas/ subfolder, otherwise assume a + // SYSTEM extension that has been installed in the same prefix as the shell + const schemaDir = Me.dir.get_child('schemas'); + let source; + if (schemaDir.query_exists(null)) { + source = Gio.SettingsSchemaSource.new_from_directory( + schemaDir.get_path(), Gio.SettingsSchemaSource.get_default(), false); + } else { + source = Gio.SettingsSchemaSource.get_default(); + } + + const schema = + source.lookup('org.gnome.shell.extensions.burn-my-windows-profile', true); + + const backend = + Gio.keyfile_settings_backend_new(profilePath, '/org/gnome/shell/extensions/', null); + + return new Gio.Settings({settings_schema: schema, backend: backend}); + } +} \ No newline at end of file diff --git a/src/utils.js b/src/utils.js index 6c1e0c8..01853af 100644 --- a/src/utils.js +++ b/src/utils.js @@ -14,8 +14,8 @@ 'use strict'; -const {Gtk, Gio, GLib} = imports.gi; -const ByteArray = imports.byteArray; +const {Gtk, Gio} = imports.gi; +const ByteArray = imports.byteArray; // libadwaita is available starting with GNOME Shell 42. let Adw = null; @@ -110,121 +110,3 @@ function getStringResource(path) { const data = Gio.resources_lookup_data(path, 0); return ByteArray.toString(ByteArray.fromGBytes(data)); } - -function createProfile() { - const path = - GLib.get_user_config_dir() + `/burn-my-windows/profiles/${GLib.get_real_time()}.conf`; - const file = Gio.File.new_for_path(path); - - if (!file.get_parent().query_exists(null)) { - file.get_parent().make_directory_with_parents(null); - } - - file.create(Gio.FileCreateFlags.NONE, null); - - return path; -} - -function deleteProfile(profilePath) { - const profileDir = GLib.get_user_config_dir() + `/burn-my-windows/profiles`; - const file = Gio.File.new_for_path(profilePath); - if (file.has_prefix(Gio.File.new_for_path(profileDir)) && file.query_exists(null)) { - file.delete(null); - } -} - - -function listProfiles() { - - const profileDir = - Gio.File.new_for_path(GLib.get_user_config_dir() + '/burn-my-windows/profiles'); - - if (!profileDir.query_exists(null)) { - return []; - } - - const files = - profileDir.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null); - - let profiles = []; - - while (true) { - const presetInfo = files.next_file(null); - - if (presetInfo == null) { - break; - } - - if (presetInfo.get_file_type() == Gio.FileType.REGULAR) { - const suffixPos = presetInfo.get_display_name().indexOf('.conf'); - if (suffixPos > 0) { - profiles.push(profileDir.get_child(presetInfo.get_name()).get_path()); - } - } - } - - - return profiles.sort(); -} - -// Based on -// https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/misc/extensionUtils.js#L213 -function getProfileSettings(profilePath) { - - // Expect USER extensions to have a schemas/ subfolder, otherwise assume a - // SYSTEM extension that has been installed in the same prefix as the shell - const schemaDir = Me.dir.get_child('schemas'); - let source; - if (schemaDir.query_exists(null)) { - source = Gio.SettingsSchemaSource.new_from_directory( - schemaDir.get_path(), Gio.SettingsSchemaSource.get_default(), false); - } else { - source = Gio.SettingsSchemaSource.get_default(); - } - - const schema = - source.lookup('org.gnome.shell.extensions.burn-my-windows-profile', true); - - const backend = - Gio.keyfile_settings_backend_new(profilePath, '/org/gnome/shell/extensions/', null); - - return new Gio.Settings({settings_schema: schema, backend: backend}); -} - -function getProfileName(profilePath) { - let items = []; - const settings = getProfileSettings(profilePath); - - const addItems = (settingsKey, options) => { - const option = settings.get_int(settingsKey); - if (option > 0) { - items.push(options[option - 1]); - } - }; - - // clang-format off - addItems('profile-animation-type', [_('Opening Windows'), - _('Closing Windows')]); - addItems('profile-window-type', [_('Normal Windows'), - _('Dialog Windows')]); - addItems('profile-desktop-style', [_('Bright Mode'), - _('Dark Mode')]); - addItems('profile-power-mode', [_('On Battery'), - _('Plugged In')]); - addItems('profile-power-profile', [_('Power-Saver Mode'), - _('Balanced Mode'), - _('Performance Mode'), - _('Power Saver or Balanced'), - _('Balanced or Performance')]); - // clang-format on - - let label = ''; - - if (items.length == 0) { - label = _('Default Profile'); - } else { - label = items.join(' · '); - } - - return label; -} \ No newline at end of file