1112 lines
34 KiB
JavaScript
Executable File
1112 lines
34 KiB
JavaScript
Executable File
// 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);
|
|
}
|
|
}
|
|
});
|