////////////////////////////////////////////////////////////////////////////////////////// // ) ( // // ( /( ( ( ) ( ( ( ( )\ ) ( ( // // )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( // // ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ // // | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) // // | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< // // |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ // // |__/ // ////////////////////////////////////////////////////////////////////////////////////////// // SPDX-FileCopyrightText: Simon Schneegans // SPDX-License-Identifier: GPL-3.0-or-later 'use strict'; import Gio from 'gi://Gio'; import Gtk from 'gi://Gtk'; import GLib from 'gi://GLib'; import Gdk from 'gi://Gdk'; import Adw from 'gi://Adw'; import * as utils from './src/utils.js'; import {ProfileManager} from './src/ProfileManager.js'; import Apparition from './src/effects/Apparition.js'; import AuraGlow from './src/effects/AuraGlow.js'; import BrokenGlass from './src/effects/BrokenGlass.js'; import Doom from './src/effects/Doom.js'; import EnergizeA from './src/effects/EnergizeA.js'; import EnergizeB from './src/effects/EnergizeB.js'; import Fire from './src/effects/Fire.js'; import Focus from './src/effects/Focus.js'; import Glide from './src/effects/Glide.js'; import Glitch from './src/effects/Glitch.js'; import Hexagon from './src/effects/Hexagon.js'; import Incinerate from './src/effects/Incinerate.js'; import Matrix from './src/effects/Matrix.js'; import Mushroom from './src/effects/Mushroom.js'; import PaintBrush from './src/effects/PaintBrush.js'; import Pixelate from './src/effects/Pixelate.js'; import PixelWheel from './src/effects/PixelWheel.js'; import PixelWipe from './src/effects/PixelWipe.js'; import Portal from './src/effects/Portal.js'; import RGBWarp from './src/effects/RGBWarp.js'; import SnapOfDisintegration from './src/effects/SnapOfDisintegration.js'; import TeamRocket from './src/effects/TeamRocket.js'; import TRexAttack from './src/effects/TRexAttack.js'; import TVEffect from './src/effects/TVEffect.js'; import TVGlitch from './src/effects/TVGlitch.js'; import Wisps from './src/effects/Wisps.js'; import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; // Currently, the extension supports only one set of UI files. In the past, there were // three different sets for GTK3, GTK4, and Adwaita. This method returns the name of the // current UI directory. It's still here since it might be useful in the future. function getUIDir() { return 'adw'; } ////////////////////////////////////////////////////////////////////////////////////////// // The preferences dialog is organized in pages, each of which is loaded from a // // separate ui file. There's one page with general options, all other paged are loaded // // from the respective effects. // ////////////////////////////////////////////////////////////////////////////////////////// export default class BurnMyWindowsPreferences extends ExtensionPreferences { // ------------------------------------------------------------ constructor / destructor fillPreferencesWindow(window) { window.set_default_size(650, 750); // New effects must be registered here and in extension.js. this._ALL_EFFECTS = [ Apparition, AuraGlow, BrokenGlass, Doom, EnergizeA, EnergizeB, Fire, Focus, Glide, Glitch, Hexagon, Incinerate, Matrix, Mushroom, PaintBrush, Pixelate, PixelWheel, PixelWipe, Portal, RGBWarp, SnapOfDisintegration, TeamRocket, TRexAttack, TVEffect, TVGlitch, Wisps, ]; // Load all of our resources. this._resources = Gio.Resource.load(this.path + '/resources/burn-my-windows.gresource'); Gio.resources_register(this._resources); // Make sure custom icons are found. Gtk.IconTheme.get_for_display(Gdk.Display.get_default()).add_resource_path('/img'); // Load the general user interface files. this._builder = new Gtk.Builder(); this._builder.add_from_resource(`/ui/common/menus.ui`); this._builder.add_from_resource(`/ui/${getUIDir()}/prefs.ui`); // Store a reference to the general settings object. this._settings = this.getSettings(); // This tracks all available effect profiles. this._profileManager = new ProfileManager(this.metadata); // This is used to track all connections to properties of the current effect profile. // Whenever the current effect profile changes, all connections are disconnected. this._profileConnections = []; // Load the current effect profile. If there is none, we create a default profile. this._settings.connect('changed::active-profile', () => { this._loadActiveProfile(); }); // Check whether the power profiles daemon is available - if not, we hide the // corresponding settings row. let hasPowerProfiles = false; try { const PowerProfilesProxy = Gio.DBusProxy.makeProxyWrapper( utils.getStringResource('/interfaces/net.hadess.PowerProfiles.xml')); let powerProfilesProxy = new PowerProfilesProxy( Gio.DBus.system, 'net.hadess.PowerProfiles', '/net/hadess/PowerProfiles'); hasPowerProfiles = powerProfilesProxy.get_name_owner() != null; } catch (e) { // Maybe the service is masked... } let powerProfileRow = this._builder.get_object('profile-power-profile'); powerProfileRow.set_visible(hasPowerProfiles); // Wire up the window picker of the profile editor. Whenever the app-chooser button is // clicked, we call a method of the extension which will initiate the window picking. 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); }); // If the window picking was successful, this D-Bus signal will be emitted. If a // window was picked, we add it to the list of window name. 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') { const entry = this._builder.get_object('profile-app'); // Split the current value at each |, trim spaces, and remove empty components. let apps = entry.text.split('|').map(item => item.trim()).filter(v => v != ''); // If the list is empty, we simply use the name of the picked window. if (apps.length == 0) { entry.text = val; } else { // Else we append the new application name, remove any duplicates, and sort // the resulting list alphabetically. apps.push(val); apps = [...new Set(apps)].sort(); entry.text = apps.join(' | '); } } }); // This is our top-level widget which we will return later. this._widget = this._builder.get_object('general-prefs'); // Add the functionality to the choose-all and choose-none buttons. this._builder.get_object('choose-all-effects-button').connect('clicked', () => { this._ALL_EFFECTS.forEach(effect => { this.getProfileSettings().set_boolean(`${effect.getNick()}-enable-effect`, true); }); }); this._builder.get_object('choose-no-effects-button').connect('clicked', () => { this._ALL_EFFECTS.forEach(effect => { this.getProfileSettings().set_boolean(`${effect.getNick()}-enable-effect`, false); }); }); // search for effect feature this._searchEntry = this._builder.get_object('search_entry'); this._searchEntry.connect('search-changed', () => { const query = this._searchEntry.get_text().toLowerCase(); // If only one effect is left, we will expand it automatically. let lastRow = null; let numRows = 0; this._effectRows.forEach(row => { row.set_expanded(false); if (query === '') { row.show(); // Show all effects if query is empty } else { // Show or hide each effect based on query match const showEffect = row.name.toLowerCase().includes(query); if (showEffect) { row.show(); lastRow = row; numRows++; } else { row.hide(); } } }); // If only one effect is left, we expand it automatically. if (numRows === 1) { lastRow.set_expanded(true); } }); // Then add a preferences group for the effect expander rows. const group = this._builder.get_object('effects-group'); // This stores all expander rows for the effects. We use this to implement the // accordion-like behavior of the effect settings. this._effectRows = []; // Now add all the rows. this._ALL_EFFECTS.forEach(effect => { const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { const uiFile = `/ui/${getUIDir()}/${effect.getNick()}.ui`; // Is there a better way to test for the existence of a resource file? let hasPrefs = false; try { Gio.resources_get_info(uiFile, 0); hasPrefs = true; } catch (e) { // Nothing todo, there } // Add the settings page to the builder. if (hasPrefs) { this._builder.add_from_resource(uiFile); } // The preview button. let previewButton = Gtk.Button.new_from_icon_name('bmw-preview-symbolic'); previewButton.get_style_context().add_class('circular'); previewButton.get_style_context().add_class('flat'); previewButton.set_tooltip_text(_('Preview this effect')); previewButton.set_valign(Gtk.Align.CENTER); previewButton.connect('clicked', () => { this._previewEffect(effect); }); // The toggle button for enabling and disabling the effect. const button = Gtk.Switch.new(); button.set_valign(Gtk.Align.CENTER); this._builder.expose_object(`${effect.getNick()}-enable-effect`, button); let row; if (hasPrefs) { // Add the effect's preferences (if any). row = this._builder.get_object(`${effect.getNick()}-prefs`); } else { row = Adw.ActionRow.new(); } // On older versions of Adw (e.g. on GNOME Shell <43), the set_use_markup() does // not yet exist. if (row.set_use_markup) { row.set_title('' + effect.getLabel() + ''); row.set_use_markup(true); } else { row.set_title(effect.getLabel()); } // this is the fix for the merge issue when an effect doesn't have a discription row.name = effect.getLabel(); // Un-expand any previously expanded effect row. This way we ensure that there // is only one expanded row at any time. if (hasPrefs) { row.connect('notify::expanded', currentRow => { if (currentRow.get_expanded()) { this._effectRows.forEach(row => { if (row != currentRow) { row.set_expanded(false); } }); } }); this._effectRows.push(row); row.add_action(button); } else { row.add_suffix(button); } row.add_prefix(previewButton); group.add(row); } }); // Some things can only be done once the widget is shown as we do not have access to // the toplevel widget before. this._widget.connect('realize', (widget) => { const window = widget.get_root(); // Show the version number in the title bar. window.set_title(`Burn-My-Windows ${this.metadata.version}`); this._loadActiveProfile(); this._updateProfileMenu(); // Show an Adw.Toast or a Gtk.InfoBar whenever Burn-My-Windows was updated. We use a // small timeout so that it is not shown instantaneously. const lastVersion = this._settings.get_int('last-prefs-version'); if (lastVersion < this.metadata.version) { this._showUpdateInfoTimeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 2000, () => { this._showUpdateInfoTimeout = 0; const toast = Adw.Toast.new(_('Burn-My-Windows has been updated!')); toast.set_button_label(_('View Changelog')); toast.set_action_name('prefs.changelog'); toast.set_timeout(0); toast.connect( 'dismissed', () => this._settings.set_int('last-prefs-version', this.metadata.version)); window.add_toast(toast); return false; }); } // Count the number of times the user has opened the preferences window. Every now // and then, we show a dialog asking the user to support the extension. We connect // to the notify::visible signal to ensure that the dialog can be a modal dialog. window.connect('notify::visible', (window) => { // Do not show the dialog when the window is hidden. if (!window.get_visible()) { return; } // Do not show the dialog when the user has disabled it. if (!this._settings.get_boolean('show-support-dialog')) { return; } const count = this._settings.get_int('prefs-open-count') + 1; this._settings.set_int('prefs-open-count', count); // Show the dialog every 10th time. if (count % 10 == 0) { const dialog = this._createMessageDialog( '❤️ Do you love Burn-My-Windows?', `Even the smallest donation can have a big impact! If just one of ten Burn-My-Windows users donated $1 per month, I could dedicate myself to creating awesome open-source projects full-time! Ko-fi: https://ko-fi.com/schneegans GitHub: https://github.com/sponsors/schneegans`, window, [ { label: 'Do not show this again!', destructive: true, default: false, action: () => { this._settings.set_boolean('show-support-dialog', false); } }, { label: 'Remind me later.', destructive: false, default: true, } ]); dialog.show(); } }); // Populate the menu with actions. const group = Gio.SimpleActionGroup.new(); window.insert_action_group('prefs', group); // Add the main menu to the title bar. { // Add the menu button to the title bar. const menu = this._builder.get_object('menu-button'); // Starting with GNOME Shell 42, we have to hack our way through the widget tree // of the Adw.PreferencesWindow... const header = this._findWidgetByType(window.get_content(), Adw.HeaderBar); header.pack_start(menu); header.set_title_widget(this._builder.get_object('profile-button')); // GNOME Shell extensions are forced to use a AdwPreferencesWindow. This already // includes a Gtk.ScrolledWindow as well as an Adw.Clamp. For the profile // editing, we want to use an Adw.Flap which reveals itself from the top. This // needs to be inserted in the widget hierarchy above the Adw.Clamp, else it // would look ugly. Therefore, we fiddle around with the internal widgets... const flap = this._builder.get_object('profile-editor-flap'); const clamp = this._findWidgetByType(window.get_content(), Adw.Clamp); const viewport = clamp.get_parent(); viewport.get_parent().set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.NEVER); viewport.set_child(flap); const scrolledWindow = Gtk.ScrolledWindow.new(); scrolledWindow.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC); scrolledWindow.set_propagate_natural_height(true); scrolledWindow.set_vexpand(true); scrolledWindow.set_child(clamp); flap.set_content(scrolledWindow); const addURIAction = (name, uri) => { const action = Gio.SimpleAction.new(name, null); action.connect('activate', () => Gtk.show_uri(null, uri, Gdk.CURRENT_TIME)); group.add_action(action); }; // clang-format off addURIAction('homepage', 'https://github.com/Schneegans/Burn-My-Windows'); addURIAction('bugs', 'https://github.com/Schneegans/Burn-My-Windows/issues'); addURIAction('new-effect', 'https://github.com/Schneegans/Burn-My-Windows/blob/main/docs/how-to-create-new-effects.md'); addURIAction('translate', 'https://hosted.weblate.org/engage/burn-my-windows/'); addURIAction('donate-kofi', 'https://ko-fi.com/schneegans'); addURIAction('donate-github', 'https://github.com/sponsors/Schneegans'); addURIAction('donate-paypal', 'https://www.paypal.me/simonschneegans'); addURIAction('donate-crypto', 'https://schneegans.cb.id'); addURIAction('show-sponsors', 'https://schneegans.github.io/sponsors'); addURIAction('wallpapers', 'https://github.com/Schneegans/ai-wallpapers'); addURIAction('profile-dir', `file://${GLib.get_user_config_dir()}/burn-my-windows/profiles`); // clang-format on // The changelog action is a bit different, as it may get activated when the // view-changelog button is pressed which is shown in an Adw.Toast or a // Gtk.InfoBar whenever Burn-My-Windows gets updated. To ensure that the // notification is shown only once, we have to store the current version of // Burn-My-Windows in the settings. const changelogAction = Gio.SimpleAction.new('changelog', null); changelogAction.connect('activate', () => { Gtk.show_uri( null, 'https://github.com/Schneegans/Burn-My-Windows/blob/main/docs/changelog.md', Gdk.CURRENT_TIME); this._settings.set_int('last-prefs-version', this.metadata.version) }); group.add_action(changelogAction); // Add the about dialog. const aboutAction = Gio.SimpleAction.new('about', null); aboutAction.connect('activate', () => { // The JSON report format from weblate is a bit weird. Here we extract all // unique names from the translation report. const translators = new Set(); this._getJSONResource('/credits/translators.json').forEach(i => { for (const j of Object.values(i)) { j.forEach(k => translators.add(k.full_name)); } }); const dialog = new Adw.AboutWindow({transient_for: window, modal: true}); dialog.set_application_icon('burn-my-windows-symbolic'); dialog.set_application_name('Burn-My-Windows'); dialog.set_version(`${this.metadata.version}`); dialog.set_developer_name('Simon Schneegans'); dialog.set_issue_url('https://github.com/Schneegans/Burn-My-Windows/issues'); dialog.set_translator_credits([...translators].join('\n')); dialog.set_copyright('© 2023 Simon Schneegans'); dialog.set_website('https://github.com/Schneegans/Burn-My-Windows'); dialog.set_license_type(Gtk.License.GPL_3_0); dialog.show(); }); group.add_action(aboutAction); } // Setup all the profile-related actions. { // Allow switching the current profile. const activeProfileAction = this._settings.create_action('active-profile'); group.add_action(activeProfileAction); // This action creates a new profile and makes it the active one. const newProfileAction = Gio.SimpleAction.new('profile-new', null); newProfileAction.connect('activate', () => { 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); // This action shows a confirmation dialog. Upon user approval, the currently // active profile is deleted. We use the more beautiful Adw.MessageDialog if it is // available (usually on GNOME 43 and beyond). const deleteProfileDialog = this._createMessageDialog( _('Delete this Profile?'), _('The current effect profile with all its effect settings will be permanently lost.'), window, [ { label: _('Cancel'), destructive: false, default: true, }, { label: _('Delete'), destructive: true, default: false, action: () => { // Stop editing the current profile. this._builder.get_object('edit-profile-button').active = false; // 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(); } } ]); // This action deletes the currently active profile. const deleteProfileAction = Gio.SimpleAction.new('profile-delete', null); deleteProfileAction.connect('activate', () => { deleteProfileDialog.show(); }); group.add_action(deleteProfileAction); } }); // As we do not have something like a destructor, we just listen for the destroy // signal of our main widget. 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); // If the settings dialog got closed too quickly, this may not have been shown. if (this._showUpdateInfoTimeout > 0) { GLib.source_remove(this._showUpdateInfoTimeout); } }); window.add(this._widget); } // -------------------------------------------------------------------- public interface // Returns the internally used Gtk.Builder. Effects can use this to modify the UI of the // preferences dialog. getBuilder() { return this._builder; } // Returns a Gio.Settings object for the current effect profile. getProfileSettings() { return this._activeProfile.settings; } // Connects an Adw.ComboRow (or anything else which has an 'selected' property) to a // settings key. It also binds the corresponding reset button. bindComboRow(settingsKey) { this._bind(settingsKey, 'selected'); } // Connects a Gtk.ComboBox (or anything else which has an 'active' property) to a // settings key. It also binds the corresponding reset button. bindComboBox(settingsKey) { this._bind(settingsKey, 'active'); } // 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) { this._bind(settingsKey, 'value'); } // Connects a Gtk.Switch (or anything else which has an 'active' property) to a settings // key. It also binds the corresponding reset button. bindSwitch(settingsKey) { this._bind(settingsKey, 'active'); } // Colors are stored as strings like 'rgb(1, 0.5, 0)'. As Gio.Settings.bind_with_mapping // is not available yet, we need to do the color conversion manually. It also binds the // corresponding reset button. bindColorButton(settingsKey) { const button = this._builder.get_object(settingsKey); if (button) { // Update the settings when the color is modified. 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.getProfileSettings().get_string(settingsKey)); button.rgba = rgba; }; this._connectProfileSetting('changed::' + settingsKey, settingSignalHandler); // Initialize the button with the state in the settings. settingSignalHandler(); } this._bindResetButton(settingsKey); } // ----------------------------------------------------------------------- private stuff // This loads all settings from the profile given in the settings key 'active-profile'. _loadActiveProfile() { // Disconnect any connection to the previously active profile. this._disconnectProfileSettings(); // Find the active profile in the list of all profiles. const allProfiles = this._profileManager.getProfiles(); const path = this._settings.get_string('active-profile'); this._activeProfile = allProfiles.find(p => p.path == path); // If, for whatever reason, it is not found, select the first one. Setting the // settings key will trigger this method again. if (!this._activeProfile) { this._activeProfile = allProfiles[0]; this._settings.set_string('active-profile', this._activeProfile.path); return; } // Connect all profile options. const updateProfileButtonMenuIfChanged = (settingsKey) => { this._connectProfileSetting('changed::' + settingsKey, () => { this._updateProfileButton(); this._updateProfileMenu(); }); }; updateProfileButtonMenuIfChanged('profile-app'); updateProfileButtonMenuIfChanged('profile-animation-type'); updateProfileButtonMenuIfChanged('profile-window-type'); updateProfileButtonMenuIfChanged('profile-color-scheme'); updateProfileButtonMenuIfChanged('profile-power-mode'); updateProfileButtonMenuIfChanged('profile-power-profile'); updateProfileButtonMenuIfChanged('profile-high-priority'); this.bindEntry('profile-app'); this.bindComboRow('profile-animation-type'); this.bindComboRow('profile-window-type'); this.bindComboRow('profile-color-scheme'); this.bindComboRow('profile-power-mode'); this.bindComboRow('profile-power-profile'); this.bindSwitch('profile-high-priority'); // Connect all effect settings. this._ALL_EFFECTS.forEach(effect => { const [minMajor, minMinor] = effect.getMinShellVersion(); if (utils.shellVersionIsAtLeast(minMajor, minMinor)) { this.bindSwitch(`${effect.getNick()}-enable-effect`); effect.bindPreferences(this); } }); // Update the label in the profile-select button. this._updateProfileButton(); } // This updates the label of the profile-selection button according to the currently // active profile. It's called whenever the active profile is changed or whenever a // configuration option of the profile is changed. _updateProfileButton() { this._builder.get_object('choose-profile-button').label = this._getProfileName(this.getProfileSettings()); } // This updates the list of available profiles in the dropdown of the profile-selection // button. It's called whenever one of the configuration option of the currently active // profile is changed, or whenever a profile is added / removed. _updateProfileMenu() { // First, clear the menu section. const profileSection = this._builder.get_object('profile-section'); profileSection.remove_all(); // Then add an entry for each available profile. this._profileManager.getProfiles().forEach(profile => { const label = this._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.path)); profileSection.append_item(item); }); } // Profiles are named according to their configuration. This method returns a localized // string describing the settings of the profile. _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) { 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-color-scheme', [_('Default Color Scheme'), _('Dark Color Scheme')]); 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 // If the profile is a high-priority profile, also add this.. if (settings.get_boolean('profile-high-priority')) { items.push(_('High Priority')); } let name = ''; if (items.length == 0) { name = _('Standard Profile'); } else { name = items.join(' · '); } return name; } // This wrapper connects a given callback to a profile settings key. Use this instead of // the direct connect() call on the profile settings to make sure that the callback is // disconnected when the active profile changes. _connectProfileSetting(key, callback) { this._profileConnections.push(this.getProfileSettings().connect(key, callback)); } // This is called whenever the currently edited effect profile changes. It unbinds all // callbacks bound with the above method before. _disconnectProfileSettings() { this._profileConnections.forEach(c => this.getProfileSettings().disconnect(c)); this._profileConnections = []; } // 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.getProfileSettings().reset(settingsKey); }); resetButton._isConnected = true; } } // Connects any widget's property to a settings key. The widget must have the same ID as // the settings key. It also binds the corresponding reset button. _bind(settingsKey, property) { const object = this._builder.get_object(settingsKey); if (object) { // Unbind any previous binding (potentially to a previously edited profile). Gio.Settings.unbind(object, property); this.getProfileSettings().bind(settingsKey, object, property, Gio.SettingsBindFlags.DEFAULT); } this._bindResetButton(settingsKey); } // Reads the contents of a JSON file contained in the global resources archive. The data // is parsed and returned as a JavaScript object / array. _getJSONResource(path) { return JSON.parse(utils.getStringResource(path)); } // This traverses the widget tree below the given parent recursively and returns the // first widget of the given type. _findWidgetByType(parent, type) { for (const child of [...parent]) { if (child instanceof type) return child; const match = this._findWidgetByType(child, type); if (match) return match; } return null; } // Opens a modal window using the given effect. _previewEffect(effect) { // Set the to-be-previewed effect. this._settings.set_string('preview-effect', effect.getNick()); // Make sure that the window.show() firther below "sees" this change. Gio.Settings.sync(); // Create the preview-window. const window = new Gtk.Window({ // Translators: %s will be replaced by the effect's name. title: _('Preview for %s').replace('%s', effect.getLabel()), default_width: 800, default_height: 450, modal: true, transient_for: this._widget.get_root() }); // Add a header bar to the window. const header = Gtk.HeaderBar.new(); window.set_titlebar(header); const box = new Gtk.Box({ orientation: Gtk.Orientation.VERTICAL, valign: Gtk.Align.CENTER, spacing: 10, margin_start: 50, margin_end: 50 }); const label = Gtk.Label.new(_('Close this Window to Preview the Effect!')); label.wrap = true; label.justify = Gtk.Justification.CENTER; label.get_style_context().add_class('large-title'); const image = new Gtk.Image({ icon_name: 'burn-my-windows-symbolic', pixel_size: 128, }); box.append(image); box.append(label); window.set_child(box); window.show(); } // Helper function to show a message dialog. The dialog is modal and has a title, a // message and a list of buttons. Each button is an object with a label, a default flag // and a destructive flag. Each button object can also have an "action" callback that is // called when the button is clicked. // This method works on GTK3, GTK4, and libadwaita. _createMessageDialog(title, message, window, buttons) { let dialog = new Adw.MessageDialog({ heading: title, body: message, body_use_markup: true, modal: true, }); buttons.forEach((button, i) => { const response = i.toString(); dialog.add_response(response, button.label); if (button.default) { dialog.set_default_response(response); dialog.set_close_response(response); } if (button.destructive) { dialog.set_response_appearance(response, Adw.ResponseAppearance.DESTRUCTIVE); } }); dialog.set_hide_on_close(true); dialog.set_transient_for(window); // If the dialog is closed or a button is clicked, we hide the dialog and call the // button's action callback if it exists. dialog.connect('response', (dialog, response) => { const i = parseInt(response); const button = buttons[i]; if (button && button.action) { button.action(); } dialog.hide(); }); return dialog; } }