diff options
| author | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
|---|---|---|
| committer | AlexanderCurl <alexc@alexc.hu> | 2026-04-18 17:46:06 +0100 |
| commit | 70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch) | |
| tree | ab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-gnome-theme/theme-data/extensions/installed/burn-my-windows@schneegans.github.com/prefs.js | |
| parent | 5d94c0a7d44a2255b81815a52a7056a94a39842d (diff) | |
| download | RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip | |
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-gnome-theme/theme-data/extensions/installed/burn-my-windows@schneegans.github.com/prefs.js')
| -rw-r--r-- | raveos-gnome-theme/theme-data/extensions/installed/burn-my-windows@schneegans.github.com/prefs.js | 941 |
1 files changed, 941 insertions, 0 deletions
diff --git a/raveos-gnome-theme/theme-data/extensions/installed/burn-my-windows@schneegans.github.com/prefs.js b/raveos-gnome-theme/theme-data/extensions/installed/burn-my-windows@schneegans.github.com/prefs.js new file mode 100644 index 0000000..8476964 --- /dev/null +++ b/raveos-gnome-theme/theme-data/extensions/installed/burn-my-windows@schneegans.github.com/prefs.js @@ -0,0 +1,941 @@ +////////////////////////////////////////////////////////////////////////////////////////// +// ) ( // +// ( /( ( ( ) ( ( ( ( )\ ) ( ( // +// )\()) ))\ )( ( ( )\ ) )\))( )\ ( (()/( ( )\))( ( // +// ((_)\ /((_|()\ )\ ) )\ '(()/( ((_)()((_) )\ ) ((_)))\((_)()\ )\ // +// | |(_|_))( ((_)_(_/( _((_)) )(_)) _(()((_|_)_(_/( _| |((_)(()((_|(_) // +// | '_ \ || | '_| ' \)) | ' \()| || | \ V V / | ' \)) _` / _ \ V V (_-< // +// |_.__/\_,_|_| |_||_| |_|_|_| \_, | \_/\_/|_|_||_|\__,_\___/\_/\_//__/ // +// |__/ // +////////////////////////////////////////////////////////////////////////////////////////// + +// SPDX-FileCopyrightText: Simon Schneegans <code@simonschneegans.de> +// 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('<b>' + effect.getLabel() + '</b>'); + 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: <a href='https://ko-fi.com/schneegans'>https://ko-fi.com/schneegans</a> +GitHub: <a href='https://github.com/sponsors/schneegans'>https://github.com/sponsors/schneegans</a>`, + 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; + } +} |