// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect // // SPDX-License-Identifier: GPL-2.0-or-later import Gio from 'gi://Gio'; import GLib from 'gi://GLib'; import GObject from 'gi://GObject'; import Gtk from 'gi://Gtk'; import Pango from 'gi://Pango'; import Config from '../config.js'; import plugins from '../service/plugins/index.js'; import * as Keybindings from './keybindings.js'; // Build a list of plugins and shortcuts for devices const DEVICE_PLUGINS = []; const DEVICE_SHORTCUTS = {}; for (const name in plugins) { const module = plugins[name]; if (module.Metadata === undefined) continue; // Plugins DEVICE_PLUGINS.push(name); // Shortcuts (GActions without parameters) for (const [name, action] of Object.entries(module.Metadata.actions)) { if (action.parameter_type === null) DEVICE_SHORTCUTS[name] = [action.icon_name, action.label]; } } /** * A Gtk.ListBoxHeaderFunc for sections that adds separators between each row. * * @param {Gtk.ListBoxRow} row - The current row * @param {Gtk.ListBoxRow} before - The previous row */ export function rowSeparators(row, before) { const header = row.get_header(); if (before === null) { if (header !== null) header.destroy(); return; } if (header === null) row.set_header(new Gtk.Separator({visible: true})); } /** * A Gtk.ListBoxSortFunc for SectionRow rows * * @param {Gtk.ListBoxRow} row1 - The first row * @param {Gtk.ListBoxRow} row2 - The second row * @return {number} -1, 0 or 1 */ export function titleSortFunc(row1, row2) { if (!row1.title || !row2.title) return 0; return row1.title.localeCompare(row2.title); } /** * A row for a section of settings */ const SectionRow = GObject.registerClass({ GTypeName: 'GSConnectPreferencesSectionRow', Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-section-row.ui', Children: ['icon-image', 'title-label', 'subtitle-label'], Properties: { 'gicon': GObject.ParamSpec.object( 'gicon', 'GIcon', 'A GIcon for the row', GObject.ParamFlags.READWRITE, Gio.Icon.$gtype ), 'icon-name': GObject.ParamSpec.string( 'icon-name', 'Icon Name', 'An icon name for the row', GObject.ParamFlags.READWRITE, null ), 'subtitle': GObject.ParamSpec.string( 'subtitle', 'Subtitle', 'A subtitle for the row', GObject.ParamFlags.READWRITE, null ), 'title': GObject.ParamSpec.string( 'title', 'Title', 'A title for the row', GObject.ParamFlags.READWRITE, null ), 'widget': GObject.ParamSpec.object( 'widget', 'Widget', 'An action widget for the row', GObject.ParamFlags.READWRITE, Gtk.Widget.$gtype ), }, }, class SectionRow extends Gtk.ListBoxRow { _init(params = {}) { super._init(); // NOTE: we can't pass construct properties to _init() because the // template children are not assigned until after it runs. this.freeze_notify(); Object.assign(this, params); this.thaw_notify(); } get icon_name() { return this.icon_image.icon_name; } set icon_name(icon_name) { if (this.icon_name === icon_name) return; this.icon_image.visible = !!icon_name; this.icon_image.icon_name = icon_name; this.notify('icon-name'); } get gicon() { return this.icon_image.gicon; } set gicon(gicon) { if (this.gicon === gicon) return; this.icon_image.visible = !!gicon; this.icon_image.gicon = gicon; this.notify('gicon'); } get title() { return this.title_label.label; } set title(text) { if (this.title === text) return; this.title_label.visible = !!text; this.title_label.label = text; this.notify('title'); } get subtitle() { return this.subtitle_label.label; } set subtitle(text) { if (this.subtitle === text) return; this.subtitle_label.visible = !!text; this.subtitle_label.label = text; this.notify('subtitle'); } get widget() { if (this._widget === undefined) this._widget = null; return this._widget; } set widget(widget) { if (this.widget === widget) return; if (this.widget instanceof Gtk.Widget) this.widget.destroy(); // Add the widget this._widget = widget; this.get_child().attach(widget, 2, 0, 1, 2); this.notify('widget'); } }); /** * Command Editor Dialog */ const CommandEditor = GObject.registerClass({ GTypeName: 'GSConnectPreferencesCommandEditor', Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-command-editor.ui', Children: [ 'cancel-button', 'save-button', 'command-entry', 'name-entry', 'command-chooser', ], }, class CommandEditor extends Gtk.Dialog { _onBrowseCommand(entry, icon_pos, event) { this.command_chooser.present(); } _onCommandChosen(dialog, response_id) { if (response_id === Gtk.ResponseType.OK) this.command_entry.text = dialog.get_filename(); dialog.hide(); } _onEntryChanged(entry, pspec) { this.save_button.sensitive = (this.command_name && this.command_line); } get command_line() { return this.command_entry.text; } set command_line(text) { this.command_entry.text = text; } get command_name() { return this.name_entry.text; } set command_name(text) { this.name_entry.text = text; } }); /** * A widget for configuring a remote device. */ export const Panel = GObject.registerClass({ GTypeName: 'GSConnectPreferencesDevicePanel', Properties: { 'device': GObject.ParamSpec.object( 'device', 'Device', 'The device being configured', GObject.ParamFlags.READWRITE, GObject.Object.$gtype ), }, Template: 'resource:///org/gnome/Shell/Extensions/GSConnect/ui/preferences-device-panel.ui', Children: [ 'sidebar', 'stack', 'infobar', // Sharing 'sharing', 'sharing-page', 'desktop-list', 'clipboard', 'clipboard-sync', 'mousepad', 'mpris', 'systemvolume', 'share', 'share-list', 'receive-files', 'receive-directory', // Battery 'battery', 'battery-device-label', 'battery-device', 'battery-device-list', 'battery-system-label', 'battery-system', 'battery-system-list', 'battery-custom-notification-value', // RunCommand 'runcommand', 'runcommand-page', 'command-list', 'command-add', // Notifications 'notification', 'notification-page', 'notification-list', 'notification-apps', // Telephony 'telephony', 'telephony-page', 'ringing-list', 'ringing-volume', 'talking-list', 'talking-volume', // Shortcuts 'shortcuts-page', 'shortcuts-actions', 'shortcuts-actions-title', 'shortcuts-actions-list', // Advanced 'advanced-page', 'plugin-list', 'experimental-list', 'device-menu', ], }, class Panel extends Gtk.Grid { _init(device) { super._init({ device: device, }); // GSettings this.settings = new Gio.Settings({ settings_schema: Config.GSCHEMA.lookup( 'org.gnome.Shell.Extensions.GSConnect.Device', true ), path: `/org/gnome/shell/extensions/gsconnect/device/${device.id}/`, }); // Infobar this.device.bind_property( 'paired', this.infobar, 'reveal-child', (GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN) ); this._setupActions(); // Settings Pages this._sharingSettings(); this._batterySettings(); this._runcommandSettings(); this._notificationSettings(); this._telephonySettings(); // -------------------------- this._keybindingSettings(); this._advancedSettings(); // Separate plugins and other settings this.sidebar.set_header_func((row, before) => { if (row.get_name() === 'shortcuts') row.set_header(new Gtk.Separator({visible: true})); }); } get menu() { if (this._menu === undefined) { this._menu = this.device_menu; this._menu.prepend_section(null, this.device.menu); this.insert_action_group('device', this.device.action_group); } return this._menu; } get_incoming_supported(type) { const incoming = this.settings.get_strv('incoming-capabilities'); return incoming.includes(`kdeconnect.${type}`); } get_outgoing_supported(type) { const outgoing = this.settings.get_strv('outgoing-capabilities'); return outgoing.includes(`kdeconnect.${type}`); } _onKeynavFailed(widget, direction) { if (direction === Gtk.DirectionType.UP && widget.prev) widget.prev.child_focus(direction); else if (direction === Gtk.DirectionType.DOWN && widget.next) widget.next.child_focus(direction); return true; } _onSwitcherRowSelected(box, row) { this.stack.set_visible_child_name(row.get_name()); } _onSectionRowActivated(box, row) { if (row.widget !== undefined) row.widget.active = !row.widget.active; } _onToggleRowActivated(box, row) { const widget = row.get_child().get_child_at(1, 0); widget.active = !widget.active; } _onEncryptionInfo() { const dialog = new Gtk.MessageDialog({ buttons: Gtk.ButtonsType.OK, text: _('Encryption Info'), secondary_text: this.device.encryption_info, modal: true, transient_for: this.get_toplevel(), }); dialog.connect('response', (dialog) => dialog.destroy()); dialog.present(); } _deviceAction(action, parameter) { this.action_group.activate_action(action.name, parameter); } dispose() { if (this._commandEditor !== undefined) this._commandEditor.destroy(); // Device signals this.device.action_group.disconnect(this._actionAddedId); this.device.action_group.disconnect(this._actionRemovedId); // GSettings for (const settings of Object.values(this._pluginSettings)) settings.run_dispose(); this.settings.disconnect(this._keybindingsId); this.settings.disconnect(this._disabledPluginsId); this.settings.disconnect(this._supportedPluginsId); this.settings.run_dispose(); } pluginSettings(name) { if (this._pluginSettings === undefined) this._pluginSettings = {}; if (!this._pluginSettings.hasOwnProperty(name)) { const meta = plugins[name].Metadata; this._pluginSettings[name] = new Gio.Settings({ settings_schema: Config.GSCHEMA.lookup(meta.id, -1), path: `${this.settings.path}plugin/${name}/`, }); } return this._pluginSettings[name]; } _setupActions() { this.actions = new Gio.SimpleActionGroup(); this.insert_action_group('settings', this.actions); let settings = this.pluginSettings('battery'); this.actions.add_action(settings.create_action('send-statistics')); this.actions.add_action(settings.create_action('low-battery-notification')); this.actions.add_action(settings.create_action('custom-battery-notification')); this.actions.add_action(settings.create_action('custom-battery-notification-value')); this.actions.add_action(settings.create_action('full-battery-notification')); settings = this.pluginSettings('clipboard'); this.actions.add_action(settings.create_action('send-content')); this.actions.add_action(settings.create_action('receive-content')); settings = this.pluginSettings('contacts'); this.actions.add_action(settings.create_action('contacts-source')); settings = this.pluginSettings('mousepad'); this.actions.add_action(settings.create_action('share-control')); settings = this.pluginSettings('mpris'); this.actions.add_action(settings.create_action('share-players')); settings = this.pluginSettings('notification'); this.actions.add_action(settings.create_action('send-notifications')); this.actions.add_action(settings.create_action('send-active')); settings = this.pluginSettings('sftp'); this.actions.add_action(settings.create_action('automount')); settings = this.pluginSettings('share'); this.actions.add_action(settings.create_action('receive-files')); settings = this.pluginSettings('sms'); this.actions.add_action(settings.create_action('legacy-sms')); settings = this.pluginSettings('systemvolume'); this.actions.add_action(settings.create_action('share-sinks')); settings = this.pluginSettings('telephony'); this.actions.add_action(settings.create_action('ringing-volume')); this.actions.add_action(settings.create_action('ringing-pause')); this.actions.add_action(settings.create_action('talking-volume')); this.actions.add_action(settings.create_action('talking-pause')); this.actions.add_action(settings.create_action('talking-microphone')); // Pair Actions const encryption_info = new Gio.SimpleAction({name: 'encryption-info'}); encryption_info.connect('activate', this._onEncryptionInfo.bind(this)); this.actions.add_action(encryption_info); const status_pair = new Gio.SimpleAction({name: 'pair'}); status_pair.connect('activate', this._deviceAction.bind(this.device)); this.settings.bind('paired', status_pair, 'enabled', 16); this.actions.add_action(status_pair); const status_unpair = new Gio.SimpleAction({name: 'unpair'}); status_unpair.connect('activate', this._deviceAction.bind(this.device)); this.settings.bind('paired', status_unpair, 'enabled', 0); this.actions.add_action(status_unpair); } /** * Sharing Settings */ _sharingSettings() { // Share Plugin const settings = this.pluginSettings('share'); settings.connect( 'changed::receive-directory', this._onReceiveDirectoryChanged.bind(this) ); this._onReceiveDirectoryChanged(settings, 'receive-directory'); // Visibility this.desktop_list.foreach(row => { const name = row.get_name(); row.visible = this.get_outgoing_supported(`${name}.request`); }); // Separators & Sorting this.desktop_list.set_header_func(rowSeparators); this.desktop_list.set_sort_func((row1, row2) => { row1 = row1.get_child().get_child_at(0, 0); row2 = row2.get_child().get_child_at(0, 0); return row1.label.localeCompare(row2.label); }); this.share_list.set_header_func(rowSeparators); // Scroll with keyboard focus const sharing_box = this.sharing_page.get_child().get_child(); sharing_box.set_focus_vadjustment(this.sharing_page.vadjustment); // Continue focus chain between lists this.desktop_list.next = this.share_list; this.share_list.prev = this.desktop_list; } _onReceiveDirectoryChanged(settings, key) { let receiveDir = settings.get_string(key); if (receiveDir.length === 0) { receiveDir = GLib.get_user_special_dir( GLib.UserDirectory.DIRECTORY_DOWNLOAD ); // Account for some corner cases with a fallback const homeDir = GLib.get_home_dir(); if (!receiveDir || receiveDir === homeDir) receiveDir = GLib.build_filenamev([homeDir, 'Downloads']); settings.set_string(key, receiveDir); } if (this.receive_directory.get_filename() !== receiveDir) this.receive_directory.set_filename(receiveDir); } _onReceiveDirectorySet(button) { const settings = this.pluginSettings('share'); const receiveDir = settings.get_string('receive-directory'); const filename = button.get_filename(); if (filename !== receiveDir) settings.set_string('receive-directory', filename); } /** * Battery Settings */ async _batterySettings() { try { this.battery_device_list.set_header_func(rowSeparators); this.battery_system_list.set_header_func(rowSeparators); const settings = this.pluginSettings('battery'); const oldLevel = settings.get_uint('custom-battery-notification-value'); this.battery_custom_notification_value.set_value(oldLevel); // If the device can't handle statistics we're done if (!this.get_incoming_supported('battery')) { this.battery_system_label.visible = false; this.battery_system.visible = false; return; } // Check UPower for a battery const hasBattery = await new Promise((resolve, reject) => { Gio.DBus.system.call( 'org.freedesktop.UPower', '/org/freedesktop/UPower/devices/DisplayDevice', 'org.freedesktop.DBus.Properties', 'Get', new GLib.Variant('(ss)', [ 'org.freedesktop.UPower.Device', 'IsPresent', ]), null, Gio.DBusCallFlags.NONE, -1, null, (connection, res) => { try { const variant = connection.call_finish(res); const value = variant.deepUnpack()[0]; const isPresent = value.get_boolean(); resolve(isPresent); } catch (e) { resolve(false); } } ); }); this.battery_system_label.visible = hasBattery; this.battery_system.visible = hasBattery; } catch (e) { this.battery_system_label.visible = false; this.battery_system.visible = false; } } _setCustomChargeLevel(spin) { const settings = this.pluginSettings('battery'); settings.set_uint('custom-battery-notification-value', spin.get_value_as_int()); } /** * RunCommand Page */ _runcommandSettings() { // Scroll with keyboard focus const runcommand_box = this.runcommand_page.get_child().get_child(); runcommand_box.set_focus_vadjustment(this.runcommand_page.vadjustment); // Local Command List const settings = this.pluginSettings('runcommand'); this._commands = settings.get_value('command-list').recursiveUnpack(); this.command_list.set_sort_func(this._sortCommands); this.command_list.set_header_func(rowSeparators); for (const uuid of Object.keys(this._commands)) this._insertCommand(uuid); } _sortCommands(row1, row2) { if (!row1.title || !row2.title) return 1; return row1.title.localeCompare(row2.title); } _insertCommand(uuid) { const row = new SectionRow({ title: this._commands[uuid].name, subtitle: this._commands[uuid].command, activatable: false, }); row.set_name(uuid); row.subtitle_label.ellipsize = Pango.EllipsizeMode.MIDDLE; const editButton = new Gtk.Button({ image: new Gtk.Image({ icon_name: 'document-edit-symbolic', pixel_size: 16, visible: true, }), tooltip_text: _('Edit'), valign: Gtk.Align.CENTER, vexpand: true, visible: true, }); editButton.connect('clicked', this._onEditCommand.bind(this)); editButton.get_accessible().set_name(_('Edit')); row.get_child().attach(editButton, 2, 0, 1, 2); const deleteButton = new Gtk.Button({ image: new Gtk.Image({ icon_name: 'edit-delete-symbolic', pixel_size: 16, visible: true, }), tooltip_text: _('Remove'), valign: Gtk.Align.CENTER, vexpand: true, visible: true, }); deleteButton.connect('clicked', this._onDeleteCommand.bind(this)); deleteButton.get_accessible().set_name(_('Remove')); row.get_child().attach(deleteButton, 3, 0, 1, 2); this.command_list.add(row); } _onEditCommand(widget) { if (this._commandEditor === undefined) { this._commandEditor = new CommandEditor({ modal: true, transient_for: this.get_toplevel(), use_header_bar: true, }); this._commandEditor.connect( 'response', this._onSaveCommand.bind(this) ); this._commandEditor.resize(1, 1); } if (widget instanceof Gtk.Button) { const row = widget.get_ancestor(Gtk.ListBoxRow.$gtype); const uuid = row.get_name(); this._commandEditor.uuid = uuid; this._commandEditor.command_name = this._commands[uuid].name; this._commandEditor.command_line = this._commands[uuid].command; } else { this._commandEditor.uuid = GLib.uuid_string_random(); this._commandEditor.command_name = ''; this._commandEditor.command_line = ''; } this._commandEditor.present(); } _storeCommands() { const variant = {}; for (const [uuid, command] of Object.entries(this._commands)) variant[uuid] = new GLib.Variant('a{ss}', command); this.pluginSettings('runcommand').set_value( 'command-list', new GLib.Variant('a{sv}', variant) ); } _onDeleteCommand(button) { const row = button.get_ancestor(Gtk.ListBoxRow.$gtype); delete this._commands[row.get_name()]; row.destroy(); this._storeCommands(); } _onSaveCommand(dialog, response_id) { if (response_id === Gtk.ResponseType.ACCEPT) { this._commands[dialog.uuid] = { name: dialog.command_name, command: dialog.command_line, }; this._storeCommands(); // let row = null; for (const child of this.command_list.get_children()) { if (child.get_name() === dialog.uuid) { row = child; break; } } if (row === null) { this._insertCommand(dialog.uuid); } else { row.set_name(dialog.uuid); row.title = dialog.command_name; row.subtitle = dialog.command_line; } } dialog.hide(); } /** * Notification Settings */ _notificationSettings() { const settings = this.pluginSettings('notification'); settings.bind( 'send-notifications', this.notification_apps, 'sensitive', Gio.SettingsBindFlags.DEFAULT ); // Separators & Sorting this.notification_list.set_header_func(rowSeparators); // Scroll with keyboard focus const notification_box = this.notification_page.get_child().get_child(); notification_box.set_focus_vadjustment(this.notification_page.vadjustment); // Continue focus chain between lists this.notification_list.next = this.notification_apps; this.notification_apps.prev = this.notification_list; this.notification_apps.set_sort_func(titleSortFunc); this.notification_apps.set_header_func(rowSeparators); this._populateApplications(settings); } _toggleNotification(widget) { try { const row = widget.get_ancestor(Gtk.ListBoxRow.$gtype); const settings = this.pluginSettings('notification'); let applications = {}; try { applications = JSON.parse(settings.get_string('applications')); } catch (e) { applications = {}; } applications[row.title].enabled = !applications[row.title].enabled; row.widget.state = applications[row.title].enabled; settings.set_string('applications', JSON.stringify(applications)); } catch (e) { logError(e); } } _populateApplications(settings) { const applications = this._queryApplications(settings); for (const name in applications) { const row = new SectionRow({ gicon: Gio.Icon.new_for_string(applications[name].iconName), title: name, height_request: 48, widget: new Gtk.Switch({ state: applications[name].enabled, margin_start: 12, margin_end: 12, halign: Gtk.Align.END, valign: Gtk.Align.CENTER, vexpand: true, visible: true, }), }); row.widget.connect('notify::active', this._toggleNotification.bind(this)); this.notification_apps.add(row); } } _queryApplications(settings) { let applications = {}; try { applications = JSON.parse(settings.get_string('applications')); } catch (e) { applications = {}; } // Scan applications that statically declare to show notifications const ignoreId = 'org.gnome.Shell.Extensions.GSConnect.desktop'; for (const appInfo of Gio.AppInfo.get_all()) { if (appInfo.get_id() === ignoreId) continue; if (!appInfo.get_boolean('X-GNOME-UsesNotifications')) continue; const appName = appInfo.get_name(); if (appName === null || applications.hasOwnProperty(appName)) continue; let icon = appInfo.get_icon(); icon = (icon) ? icon.to_string() : 'application-x-executable'; applications[appName] = { iconName: icon, enabled: true, }; } settings.set_string('applications', JSON.stringify(applications)); return applications; } /** * Telephony Settings */ _telephonySettings() { // Continue focus chain between lists this.ringing_list.next = this.talking_list; this.talking_list.prev = this.ringing_list; this.ringing_list.set_header_func(rowSeparators); this.talking_list.set_header_func(rowSeparators); } /** * Keyboard Shortcuts */ _keybindingSettings() { // Scroll with keyboard focus const shortcuts_box = this.shortcuts_page.get_child().get_child(); shortcuts_box.set_focus_vadjustment(this.shortcuts_page.vadjustment); // Filter & Sort this.shortcuts_actions_list.set_filter_func(this._filterPluginKeybindings.bind(this)); this.shortcuts_actions_list.set_header_func(rowSeparators); this.shortcuts_actions_list.set_sort_func(titleSortFunc); // Init for (const name in DEVICE_SHORTCUTS) this._addPluginKeybinding(name); this._setPluginKeybindings(); // Watch for GAction and Keybinding changes this._actionAddedId = this.device.action_group.connect( 'action-added', () => this.shortcuts_actions_list.invalidate_filter() ); this._actionRemovedId = this.device.action_group.connect( 'action-removed', () => this.shortcuts_actions_list.invalidate_filter() ); this._keybindingsId = this.settings.connect( 'changed::keybindings', this._setPluginKeybindings.bind(this) ); } _addPluginKeybinding(name) { const [icon_name, label] = DEVICE_SHORTCUTS[name]; const widget = new Gtk.Label({ label: _('Disabled'), visible: true, }); widget.get_style_context().add_class('dim-label'); const row = new SectionRow({ height_request: 48, icon_name: icon_name, title: label, widget: widget, }); row.icon_image.pixel_size = 16; row.action = name; this.shortcuts_actions_list.add(row); } _filterPluginKeybindings(row) { return this.device.action_group.has_action(row.action); } _setPluginKeybindings() { const keybindings = this.settings.get_value('keybindings').deepUnpack(); this.shortcuts_actions_list.foreach(row => { if (keybindings[row.action]) { const accel = Gtk.accelerator_parse(keybindings[row.action]); row.widget.label = Gtk.accelerator_get_label(...accel); } else { row.widget.label = _('Disabled'); } }); } _onResetActionShortcuts(button) { const keybindings = this.settings.get_value('keybindings').deepUnpack(); for (const action in keybindings) { // Don't reset remote command shortcuts if (!action.includes('::')) delete keybindings[action]; } this.settings.set_value( 'keybindings', new GLib.Variant('a{ss}', keybindings) ); } async _onShortcutRowActivated(box, row) { try { const keybindings = this.settings.get_value('keybindings').deepUnpack(); let accel = keybindings[row.action] || null; accel = await Keybindings.getAccelerator(row.title, accel); if (accel) keybindings[row.action] = accel; else delete keybindings[row.action]; this.settings.set_value( 'keybindings', new GLib.Variant('a{ss}', keybindings) ); } catch (e) { logError(e); } } /** * Advanced Page */ _advancedSettings() { // Scroll with keyboard focus const advanced_box = this.advanced_page.get_child().get_child(); advanced_box.set_focus_vadjustment(this.advanced_page.vadjustment); // Sort & Separate this.plugin_list.set_header_func(rowSeparators); this.plugin_list.set_sort_func(titleSortFunc); this.experimental_list.set_header_func(rowSeparators); // Continue focus chain between lists this.plugin_list.next = this.experimental_list; this.experimental_list.prev = this.plugin_list; this._disabledPluginsId = this.settings.connect( 'changed::disabled-plugins', this._onPluginsChanged.bind(this) ); this._supportedPluginsId = this.settings.connect( 'changed::supported-plugins', this._onPluginsChanged.bind(this) ); this._onPluginsChanged(this.settings, null); for (const name of DEVICE_PLUGINS) this._addPlugin(name); } _onPluginsChanged(settings, key) { if (key === 'disabled-plugins' || this._disabledPlugins === undefined) this._disabledPlugins = settings.get_strv('disabled-plugins'); if (key === 'supported-plugins' || this._supportedPlugins === undefined) this._supportedPlugins = settings.get_strv('supported-plugins'); this._enabledPlugins = this._supportedPlugins.filter(name => { return !this._disabledPlugins.includes(name); }); if (key !== null) this._updatePlugins(); } _addPlugin(name) { const plugin = plugins[name]; const row = new SectionRow({ height_request: 48, title: plugin.Metadata.label, subtitle: plugin.Metadata.description || '', visible: this._supportedPlugins.includes(name), widget: new Gtk.Switch({ active: this._enabledPlugins.includes(name), valign: Gtk.Align.CENTER, vexpand: true, visible: true, }), }); row.widget.connect('notify::active', this._togglePlugin.bind(this)); row.set_name(name); if (this.hasOwnProperty(name)) this[name].visible = row.widget.active; this.plugin_list.add(row); } _updatePlugins(settings, key) { for (const row of this.plugin_list.get_children()) { const name = row.get_name(); row.visible = this._supportedPlugins.includes(name); row.widget.active = this._enabledPlugins.includes(name); if (this.hasOwnProperty(name)) this[name].visible = row.widget.active; } } _togglePlugin(widget) { try { const name = widget.get_ancestor(Gtk.ListBoxRow.$gtype).get_name(); const index = this._disabledPlugins.indexOf(name); // Either add or remove the plugin from the disabled list if (index > -1) this._disabledPlugins.splice(index, 1); else this._disabledPlugins.push(name); this.settings.set_strv('disabled-plugins', this._disabledPlugins); } catch (e) { logError(e); } } });