summaryrefslogtreecommitdiffstats
path: root/js/ui/status
diff options
context:
space:
mode:
Diffstat (limited to 'js/ui/status')
-rw-r--r--js/ui/status/accessibility.js200
-rw-r--r--js/ui/status/bluetooth.js158
-rw-r--r--js/ui/status/brightness.js73
-rw-r--r--js/ui/status/dwellClick.js86
-rw-r--r--js/ui/status/keyboard.js1079
-rw-r--r--js/ui/status/location.js387
-rw-r--r--js/ui/status/network.js2101
-rw-r--r--js/ui/status/nightLight.js70
-rw-r--r--js/ui/status/power.js155
-rw-r--r--js/ui/status/remoteAccess.js97
-rw-r--r--js/ui/status/rfkill.js112
-rw-r--r--js/ui/status/system.js178
-rw-r--r--js/ui/status/thunderbolt.js340
-rw-r--r--js/ui/status/volume.js430
14 files changed, 5466 insertions, 0 deletions
diff --git a/js/ui/status/accessibility.js b/js/ui/status/accessibility.js
new file mode 100644
index 0000000..04406e2
--- /dev/null
+++ b/js/ui/status/accessibility.js
@@ -0,0 +1,200 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported ATIndicator */
+
+const { Gio, GLib, GObject, St } = imports.gi;
+
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const A11Y_SCHEMA = 'org.gnome.desktop.a11y';
+const KEY_ALWAYS_SHOW = 'always-show-universal-access-status';
+
+const A11Y_KEYBOARD_SCHEMA = 'org.gnome.desktop.a11y.keyboard';
+const KEY_STICKY_KEYS_ENABLED = 'stickykeys-enable';
+const KEY_BOUNCE_KEYS_ENABLED = 'bouncekeys-enable';
+const KEY_SLOW_KEYS_ENABLED = 'slowkeys-enable';
+const KEY_MOUSE_KEYS_ENABLED = 'mousekeys-enable';
+
+const APPLICATIONS_SCHEMA = 'org.gnome.desktop.a11y.applications';
+
+var DPI_FACTOR_LARGE = 1.25;
+
+const WM_SCHEMA = 'org.gnome.desktop.wm.preferences';
+const KEY_VISUAL_BELL = 'visual-bell';
+
+const DESKTOP_INTERFACE_SCHEMA = 'org.gnome.desktop.interface';
+const KEY_GTK_THEME = 'gtk-theme';
+const KEY_ICON_THEME = 'icon-theme';
+const KEY_TEXT_SCALING_FACTOR = 'text-scaling-factor';
+
+const HIGH_CONTRAST_THEME = 'HighContrast';
+
+var ATIndicator = GObject.registerClass(
+class ATIndicator extends PanelMenu.Button {
+ _init() {
+ super._init(0.5, _("Accessibility"));
+
+ this._hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
+ this._hbox.add_child(new St.Icon({ style_class: 'system-status-icon',
+ icon_name: 'preferences-desktop-accessibility-symbolic' }));
+ this._hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
+
+ this.add_child(this._hbox);
+
+ this._a11ySettings = new Gio.Settings({ schema_id: A11Y_SCHEMA });
+ this._a11ySettings.connect('changed::%s'.format(KEY_ALWAYS_SHOW), this._queueSyncMenuVisibility.bind(this));
+
+ let highContrast = this._buildHCItem();
+ this.menu.addMenuItem(highContrast);
+
+ let magnifier = this._buildItem(_("Zoom"), APPLICATIONS_SCHEMA,
+ 'screen-magnifier-enabled');
+ this.menu.addMenuItem(magnifier);
+
+ let textZoom = this._buildFontItem();
+ this.menu.addMenuItem(textZoom);
+
+ let screenReader = this._buildItem(_("Screen Reader"), APPLICATIONS_SCHEMA,
+ 'screen-reader-enabled');
+ this.menu.addMenuItem(screenReader);
+
+ let screenKeyboard = this._buildItem(_("Screen Keyboard"), APPLICATIONS_SCHEMA,
+ 'screen-keyboard-enabled');
+ this.menu.addMenuItem(screenKeyboard);
+
+ let visualBell = this._buildItem(_("Visual Alerts"), WM_SCHEMA, KEY_VISUAL_BELL);
+ this.menu.addMenuItem(visualBell);
+
+ let stickyKeys = this._buildItem(_("Sticky Keys"), A11Y_KEYBOARD_SCHEMA, KEY_STICKY_KEYS_ENABLED);
+ this.menu.addMenuItem(stickyKeys);
+
+ let slowKeys = this._buildItem(_("Slow Keys"), A11Y_KEYBOARD_SCHEMA, KEY_SLOW_KEYS_ENABLED);
+ this.menu.addMenuItem(slowKeys);
+
+ let bounceKeys = this._buildItem(_("Bounce Keys"), A11Y_KEYBOARD_SCHEMA, KEY_BOUNCE_KEYS_ENABLED);
+ this.menu.addMenuItem(bounceKeys);
+
+ let mouseKeys = this._buildItem(_("Mouse Keys"), A11Y_KEYBOARD_SCHEMA, KEY_MOUSE_KEYS_ENABLED);
+ this.menu.addMenuItem(mouseKeys);
+
+ this._syncMenuVisibility();
+ }
+
+ _syncMenuVisibility() {
+ this._syncMenuVisibilityIdle = 0;
+
+ let alwaysShow = this._a11ySettings.get_boolean(KEY_ALWAYS_SHOW);
+ let items = this.menu._getMenuItems();
+
+ this.visible = alwaysShow || items.some(f => !!f.state);
+
+ return GLib.SOURCE_REMOVE;
+ }
+
+ _queueSyncMenuVisibility() {
+ if (this._syncMenuVisibilityIdle)
+ return;
+
+ this._syncMenuVisibilityIdle = GLib.idle_add(GLib.PRIORITY_DEFAULT, this._syncMenuVisibility.bind(this));
+ GLib.Source.set_name_by_id(this._syncMenuVisibilityIdle, '[gnome-shell] this._syncMenuVisibility');
+ }
+
+ _buildItemExtended(string, initialValue, writable, onSet) {
+ let widget = new PopupMenu.PopupSwitchMenuItem(string, initialValue);
+ if (!writable) {
+ widget.reactive = false;
+ } else {
+ widget.connect('toggled', item => {
+ onSet(item.state);
+ });
+ }
+ return widget;
+ }
+
+ _buildItem(string, schema, key) {
+ let settings = new Gio.Settings({ schema_id: schema });
+ let widget = this._buildItemExtended(string,
+ settings.get_boolean(key),
+ settings.is_writable(key),
+ enabled => settings.set_boolean(key, enabled));
+
+ settings.connect('changed::%s'.format(key), () => {
+ widget.setToggleState(settings.get_boolean(key));
+
+ this._queueSyncMenuVisibility();
+ });
+
+ return widget;
+ }
+
+ _buildHCItem() {
+ let interfaceSettings = new Gio.Settings({ schema_id: DESKTOP_INTERFACE_SCHEMA });
+ let gtkTheme = interfaceSettings.get_string(KEY_GTK_THEME);
+ let iconTheme = interfaceSettings.get_string(KEY_ICON_THEME);
+ let hasHC = gtkTheme == HIGH_CONTRAST_THEME;
+ let highContrast = this._buildItemExtended(
+ _("High Contrast"),
+ hasHC,
+ interfaceSettings.is_writable(KEY_GTK_THEME) &&
+ interfaceSettings.is_writable(KEY_ICON_THEME),
+ enabled => {
+ if (enabled) {
+ interfaceSettings.set_string(KEY_ICON_THEME, HIGH_CONTRAST_THEME);
+ interfaceSettings.set_string(KEY_GTK_THEME, HIGH_CONTRAST_THEME);
+ } else if (!hasHC) {
+ interfaceSettings.set_string(KEY_ICON_THEME, iconTheme);
+ interfaceSettings.set_string(KEY_GTK_THEME, gtkTheme);
+ } else {
+ interfaceSettings.reset(KEY_ICON_THEME);
+ interfaceSettings.reset(KEY_GTK_THEME);
+ }
+ });
+
+ interfaceSettings.connect('changed::%s'.format(KEY_GTK_THEME), () => {
+ let value = interfaceSettings.get_string(KEY_GTK_THEME);
+ if (value == HIGH_CONTRAST_THEME) {
+ highContrast.setToggleState(true);
+ } else {
+ highContrast.setToggleState(false);
+ gtkTheme = value;
+ }
+
+ this._queueSyncMenuVisibility();
+ });
+
+ interfaceSettings.connect('changed::%s'.format(KEY_ICON_THEME), () => {
+ let value = interfaceSettings.get_string(KEY_ICON_THEME);
+ if (value != HIGH_CONTRAST_THEME)
+ iconTheme = value;
+ });
+
+ return highContrast;
+ }
+
+ _buildFontItem() {
+ let settings = new Gio.Settings({ schema_id: DESKTOP_INTERFACE_SCHEMA });
+ let factor = settings.get_double(KEY_TEXT_SCALING_FACTOR);
+ let initialSetting = factor > 1.0;
+ let widget = this._buildItemExtended(_("Large Text"),
+ initialSetting,
+ settings.is_writable(KEY_TEXT_SCALING_FACTOR),
+ enabled => {
+ if (enabled) {
+ settings.set_double(
+ KEY_TEXT_SCALING_FACTOR, DPI_FACTOR_LARGE);
+ } else {
+ settings.reset(KEY_TEXT_SCALING_FACTOR);
+ }
+ });
+
+ settings.connect('changed::%s'.format(KEY_TEXT_SCALING_FACTOR), () => {
+ factor = settings.get_double(KEY_TEXT_SCALING_FACTOR);
+ let active = factor > 1.0;
+ widget.setToggleState(active);
+
+ this._queueSyncMenuVisibility();
+ });
+
+ return widget;
+ }
+});
diff --git a/js/ui/status/bluetooth.js b/js/ui/status/bluetooth.js
new file mode 100644
index 0000000..98ccc3d
--- /dev/null
+++ b/js/ui/status/bluetooth.js
@@ -0,0 +1,158 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Gio, GLib, GnomeBluetooth, GObject } = imports.gi;
+
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
+const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';
+
+const RfkillManagerInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Rfkill');
+const RfkillManagerProxy = Gio.DBusProxy.makeProxyWrapper(RfkillManagerInterface);
+
+const HAD_BLUETOOTH_DEVICES_SETUP = 'had-bluetooth-devices-setup';
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._indicator = this._addIndicator();
+ this._indicator.icon_name = 'bluetooth-active-symbolic';
+ this._hadSetupDevices = global.settings.get_boolean(HAD_BLUETOOTH_DEVICES_SETUP);
+
+ this._proxy = new RfkillManagerProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH,
+ (proxy, error) => {
+ if (error) {
+ log(error.message);
+ return;
+ }
+
+ this._sync();
+ });
+ this._proxy.connect('g-properties-changed', this._queueSync.bind(this));
+
+ this._item = new PopupMenu.PopupSubMenuMenuItem(_("Bluetooth"), true);
+ this._item.icon.icon_name = 'bluetooth-active-symbolic';
+
+ this._toggleItem = new PopupMenu.PopupMenuItem('');
+ this._toggleItem.connect('activate', () => {
+ this._proxy.BluetoothAirplaneMode = !this._proxy.BluetoothAirplaneMode;
+ });
+ this._item.menu.addMenuItem(this._toggleItem);
+
+ this._item.menu.addSettingsAction(_("Bluetooth Settings"), 'gnome-bluetooth-panel.desktop');
+ this.menu.addMenuItem(this._item);
+
+ this._syncId = 0;
+ this._adapter = null;
+
+ this._client = new GnomeBluetooth.Client();
+ this._model = this._client.get_model();
+ this._model.connect('row-deleted', this._queueSync.bind(this));
+ this._model.connect('row-changed', this._queueSync.bind(this));
+ this._model.connect('row-inserted', this._sync.bind(this));
+ Main.sessionMode.connect('updated', this._sync.bind(this));
+ this._sync();
+ }
+
+ _setHadSetupDevices(value) {
+ if (this._hadSetupDevices === value)
+ return;
+
+ this._hadSetupDevices = value;
+ global.settings.set_boolean(
+ HAD_BLUETOOTH_DEVICES_SETUP, this._hadSetupDevices);
+ }
+
+ _getDefaultAdapter() {
+ let [ret, iter] = this._model.get_iter_first();
+ while (ret) {
+ let isDefault = this._model.get_value(iter,
+ GnomeBluetooth.Column.DEFAULT);
+ let isPowered = this._model.get_value(iter,
+ GnomeBluetooth.Column.POWERED);
+ if (isDefault && isPowered)
+ return iter;
+ ret = this._model.iter_next(iter);
+ }
+ return null;
+ }
+
+ _getDeviceInfos(adapter) {
+ if (!adapter)
+ return [];
+
+ let deviceInfos = [];
+ let [ret, iter] = this._model.iter_children(adapter);
+ while (ret) {
+ const isPaired = this._model.get_value(iter,
+ GnomeBluetooth.Column.PAIRED);
+ const isTrusted = this._model.get_value(iter,
+ GnomeBluetooth.Column.TRUSTED);
+
+ if (isPaired || isTrusted) {
+ deviceInfos.push({
+ connected: this._model.get_value(iter,
+ GnomeBluetooth.Column.CONNECTED),
+ name: this._model.get_value(iter,
+ GnomeBluetooth.Column.ALIAS),
+ });
+ }
+
+ ret = this._model.iter_next(iter);
+ }
+
+ return deviceInfos;
+ }
+
+ _queueSync() {
+ if (this._syncId)
+ return;
+ this._syncId = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+ this._syncId = 0;
+ this._sync();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ _sync() {
+ let adapter = this._getDefaultAdapter();
+ let devices = this._getDeviceInfos(adapter);
+ const connectedDevices = devices.filter(dev => dev.connected);
+ const nConnectedDevices = connectedDevices.length;
+
+ if (adapter && this._adapter)
+ this._setHadSetupDevices(devices.length > 0);
+ this._adapter = adapter;
+
+ let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+
+ this.menu.setSensitive(sensitive);
+ this._indicator.visible = nConnectedDevices > 0;
+
+ // Remember if there were setup devices and show the menu
+ // if we've seen setup devices and we're not hard blocked
+ if (this._hadSetupDevices)
+ this._item.visible = !this._proxy.BluetoothHardwareAirplaneMode;
+ else
+ this._item.visible = this._proxy.BluetoothHasAirplaneMode && !this._proxy.BluetoothAirplaneMode;
+
+ if (nConnectedDevices > 1)
+ /* Translators: this is the number of connected bluetooth devices */
+ this._item.label.text = ngettext('%d Connected', '%d Connected', nConnectedDevices).format(nConnectedDevices);
+ else if (nConnectedDevices === 1)
+ this._item.label.text = connectedDevices[0].name;
+ else if (adapter === null)
+ this._item.label.text = _('Bluetooth Off');
+ else
+ this._item.label.text = _('Bluetooth On');
+
+ this._toggleItem.label.text = this._proxy.BluetoothAirplaneMode ? _('Turn On') : _('Turn Off');
+ }
+});
diff --git a/js/ui/status/brightness.js b/js/ui/status/brightness.js
new file mode 100644
index 0000000..1724788
--- /dev/null
+++ b/js/ui/status/brightness.js
@@ -0,0 +1,73 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Gio, GObject, St } = imports.gi;
+
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+const Slider = imports.ui.slider;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const BUS_NAME = 'org.gnome.SettingsDaemon.Power';
+const OBJECT_PATH = '/org/gnome/SettingsDaemon/Power';
+
+const BrightnessInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Power.Screen');
+const BrightnessProxy = Gio.DBusProxy.makeProxyWrapper(BrightnessInterface);
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+ this._proxy = new BrightnessProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH,
+ (proxy, error) => {
+ if (error) {
+ log(error.message);
+ return;
+ }
+
+ this._proxy.connect('g-properties-changed', this._sync.bind(this));
+ this._sync();
+ });
+
+ this._item = new PopupMenu.PopupBaseMenuItem({ activate: false });
+ this.menu.addMenuItem(this._item);
+
+ this._slider = new Slider.Slider(0);
+ this._sliderChangedId = this._slider.connect('notify::value',
+ this._sliderChanged.bind(this));
+ this._slider.accessible_name = _("Brightness");
+
+ let icon = new St.Icon({ icon_name: 'display-brightness-symbolic',
+ style_class: 'popup-menu-icon' });
+ this._item.add(icon);
+ this._item.add_child(this._slider);
+ this._item.connect('button-press-event', (actor, event) => {
+ return this._slider.startDragging(event);
+ });
+ this._item.connect('key-press-event', (actor, event) => {
+ return this._slider.emit('key-press-event', event);
+ });
+ this._item.connect('scroll-event', (actor, event) => {
+ return this._slider.emit('scroll-event', event);
+ });
+ }
+
+ _sliderChanged() {
+ let percent = this._slider.value * 100;
+ this._proxy.Brightness = percent;
+ }
+
+ _changeSlider(value) {
+ this._slider.block_signal_handler(this._sliderChangedId);
+ this._slider.value = value;
+ this._slider.unblock_signal_handler(this._sliderChangedId);
+ }
+
+ _sync() {
+ let visible = this._proxy.Brightness >= 0;
+ this._item.visible = visible;
+ if (visible)
+ this._changeSlider(this._proxy.Brightness / 100.0);
+ }
+});
diff --git a/js/ui/status/dwellClick.js b/js/ui/status/dwellClick.js
new file mode 100644
index 0000000..ce13f73
--- /dev/null
+++ b/js/ui/status/dwellClick.js
@@ -0,0 +1,86 @@
+/* exported DwellClickIndicator */
+const { Clutter, Gio, GLib, GObject, St } = imports.gi;
+
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const MOUSE_A11Y_SCHEMA = 'org.gnome.desktop.a11y.mouse';
+const KEY_DWELL_CLICK_ENABLED = 'dwell-click-enabled';
+const KEY_DWELL_MODE = 'dwell-mode';
+const DWELL_MODE_WINDOW = 'window';
+const DWELL_CLICK_MODES = {
+ primary: {
+ name: _("Single Click"),
+ icon: 'pointer-primary-click-symbolic',
+ type: Clutter.PointerA11yDwellClickType.PRIMARY,
+ },
+ double: {
+ name: _("Double Click"),
+ icon: 'pointer-double-click-symbolic',
+ type: Clutter.PointerA11yDwellClickType.DOUBLE,
+ },
+ drag: {
+ name: _("Drag"),
+ icon: 'pointer-drag-symbolic',
+ type: Clutter.PointerA11yDwellClickType.DRAG,
+ },
+ secondary: {
+ name: _("Secondary Click"),
+ icon: 'pointer-secondary-click-symbolic',
+ type: Clutter.PointerA11yDwellClickType.SECONDARY,
+ },
+};
+
+var DwellClickIndicator = GObject.registerClass(
+class DwellClickIndicator extends PanelMenu.Button {
+ _init() {
+ super._init(0.5, _("Dwell Click"));
+
+ this._hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
+ this._icon = new St.Icon({ style_class: 'system-status-icon',
+ icon_name: 'pointer-primary-click-symbolic' });
+ this._hbox.add_child(this._icon);
+ this._hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
+
+ this.add_child(this._hbox);
+
+ this._a11ySettings = new Gio.Settings({ schema_id: MOUSE_A11Y_SCHEMA });
+ this._a11ySettings.connect('changed::%s'.format(KEY_DWELL_CLICK_ENABLED), this._syncMenuVisibility.bind(this));
+ this._a11ySettings.connect('changed::%s'.format(KEY_DWELL_MODE), this._syncMenuVisibility.bind(this));
+
+ this._seat = Clutter.get_default_backend().get_default_seat();
+ this._seat.connect('ptr-a11y-dwell-click-type-changed', this._updateClickType.bind(this));
+
+ this._addDwellAction(DWELL_CLICK_MODES.primary);
+ this._addDwellAction(DWELL_CLICK_MODES.double);
+ this._addDwellAction(DWELL_CLICK_MODES.drag);
+ this._addDwellAction(DWELL_CLICK_MODES.secondary);
+
+ this._setClickType(DWELL_CLICK_MODES.primary);
+ this._syncMenuVisibility();
+ }
+
+ _syncMenuVisibility() {
+ this.visible =
+ this._a11ySettings.get_boolean(KEY_DWELL_CLICK_ENABLED) &&
+ this._a11ySettings.get_string(KEY_DWELL_MODE) == DWELL_MODE_WINDOW;
+
+ return GLib.SOURCE_REMOVE;
+ }
+
+ _addDwellAction(mode) {
+ this.menu.addAction(mode.name, this._setClickType.bind(this, mode), mode.icon);
+ }
+
+ _updateClickType(manager, clickType) {
+ for (let mode in DWELL_CLICK_MODES) {
+ if (DWELL_CLICK_MODES[mode].type == clickType)
+ this._icon.icon_name = DWELL_CLICK_MODES[mode].icon;
+ }
+ }
+
+ _setClickType(mode) {
+ this._seat.set_pointer_a11y_dwell_click_type(mode.type);
+ this._icon.icon_name = mode.icon;
+ }
+});
diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js
new file mode 100644
index 0000000..43fd89d
--- /dev/null
+++ b/js/ui/status/keyboard.js
@@ -0,0 +1,1079 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported InputSourceIndicator */
+
+const { Clutter, Gio, GLib, GObject, IBus, Meta, Shell, St } = imports.gi;
+const Gettext = imports.gettext;
+const Signals = imports.signals;
+
+const IBusManager = imports.misc.ibusManager;
+const KeyboardManager = imports.misc.keyboardManager;
+const Main = imports.ui.main;
+const PopupMenu = imports.ui.popupMenu;
+const PanelMenu = imports.ui.panelMenu;
+const SwitcherPopup = imports.ui.switcherPopup;
+const Util = imports.misc.util;
+
+var INPUT_SOURCE_TYPE_XKB = 'xkb';
+var INPUT_SOURCE_TYPE_IBUS = 'ibus';
+
+var LayoutMenuItem = GObject.registerClass(
+class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem {
+ _init(displayName, shortName) {
+ super._init();
+
+ this.label = new St.Label({
+ text: displayName,
+ x_expand: true,
+ });
+ this.indicator = new St.Label({ text: shortName });
+ this.add_child(this.label);
+ this.add(this.indicator);
+ this.label_actor = this.label;
+ }
+});
+
+var InputSource = class {
+ constructor(type, id, displayName, shortName, index) {
+ this.type = type;
+ this.id = id;
+ this.displayName = displayName;
+ this._shortName = shortName;
+ this.index = index;
+
+ this.properties = null;
+
+ this.xkbId = this._getXkbId();
+ }
+
+ get shortName() {
+ return this._shortName;
+ }
+
+ set shortName(v) {
+ this._shortName = v;
+ this.emit('changed');
+ }
+
+ activate(interactive) {
+ this.emit('activate', !!interactive);
+ }
+
+ _getXkbId() {
+ let engineDesc = IBusManager.getIBusManager().getEngineDesc(this.id);
+ if (!engineDesc)
+ return this.id;
+
+ if (engineDesc.variant && engineDesc.variant.length > 0)
+ return '%s+%s'.format(engineDesc.layout, engineDesc.variant);
+ else
+ return engineDesc.layout;
+ }
+};
+Signals.addSignalMethods(InputSource.prototype);
+
+var InputSourcePopup = GObject.registerClass(
+class InputSourcePopup extends SwitcherPopup.SwitcherPopup {
+ _init(items, action, actionBackward) {
+ super._init(items);
+
+ this._action = action;
+ this._actionBackward = actionBackward;
+
+ this._switcherList = new InputSourceSwitcher(this._items);
+ }
+
+ _keyPressHandler(keysym, action) {
+ if (action == this._action)
+ this._select(this._next());
+ else if (action == this._actionBackward)
+ this._select(this._previous());
+ else if (keysym == Clutter.KEY_Left)
+ this._select(this._previous());
+ else if (keysym == Clutter.KEY_Right)
+ this._select(this._next());
+ else
+ return Clutter.EVENT_PROPAGATE;
+
+ return Clutter.EVENT_STOP;
+ }
+
+ _finish() {
+ super._finish();
+
+ this._items[this._selectedIndex].activate(true);
+ }
+});
+
+var InputSourceSwitcher = GObject.registerClass(
+class InputSourceSwitcher extends SwitcherPopup.SwitcherList {
+ _init(items) {
+ super._init(true);
+
+ for (let i = 0; i < items.length; i++)
+ this._addIcon(items[i]);
+ }
+
+ _addIcon(item) {
+ let box = new St.BoxLayout({ vertical: true });
+
+ let bin = new St.Bin({ style_class: 'input-source-switcher-symbol' });
+ let symbol = new St.Label({
+ text: item.shortName,
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ bin.set_child(symbol);
+ box.add_child(bin);
+
+ let text = new St.Label({
+ text: item.displayName,
+ x_align: Clutter.ActorAlign.CENTER,
+ });
+ box.add_child(text);
+
+ this.addItem(box, text);
+ }
+});
+
+var InputSourceSettings = class {
+ constructor() {
+ if (this.constructor === InputSourceSettings)
+ throw new TypeError('Cannot instantiate abstract class %s'.format(this.constructor.name));
+ }
+
+ _emitInputSourcesChanged() {
+ this.emit('input-sources-changed');
+ }
+
+ _emitKeyboardOptionsChanged() {
+ this.emit('keyboard-options-changed');
+ }
+
+ _emitPerWindowChanged() {
+ this.emit('per-window-changed');
+ }
+
+ get inputSources() {
+ return [];
+ }
+
+ get mruSources() {
+ return [];
+ }
+
+ set mruSources(sourcesList) {
+ // do nothing
+ }
+
+ get keyboardOptions() {
+ return [];
+ }
+
+ get perWindow() {
+ return false;
+ }
+};
+Signals.addSignalMethods(InputSourceSettings.prototype);
+
+var InputSourceSystemSettings = class extends InputSourceSettings {
+ constructor() {
+ super();
+
+ this._BUS_NAME = 'org.freedesktop.locale1';
+ this._BUS_PATH = '/org/freedesktop/locale1';
+ this._BUS_IFACE = 'org.freedesktop.locale1';
+ this._BUS_PROPS_IFACE = 'org.freedesktop.DBus.Properties';
+
+ this._layouts = '';
+ this._variants = '';
+ this._options = '';
+
+ this._reload();
+
+ Gio.DBus.system.signal_subscribe(this._BUS_NAME,
+ this._BUS_PROPS_IFACE,
+ 'PropertiesChanged',
+ this._BUS_PATH,
+ null,
+ Gio.DBusSignalFlags.NONE,
+ this._reload.bind(this));
+ }
+
+ async _reload() {
+ let props;
+ try {
+ const result = await Gio.DBus.system.call(
+ this._BUS_NAME,
+ this._BUS_PATH,
+ this._BUS_PROPS_IFACE,
+ 'GetAll',
+ new GLib.Variant('(s)', [this._BUS_IFACE]),
+ null, Gio.DBusCallFlags.NONE, -1, null);
+ [props] = result.deep_unpack();
+ } catch (e) {
+ log('Could not get properties from %s'.format(this._BUS_NAME));
+ return;
+ }
+
+ const layouts = props['X11Layout'].unpack();
+ const variants = props['X11Variant'].unpack();
+ const options = props['X11Options'].unpack();
+
+ if (layouts !== this._layouts ||
+ variants !== this._variants) {
+ this._layouts = layouts;
+ this._variants = variants;
+ this._emitInputSourcesChanged();
+ }
+ if (options !== this._options) {
+ this._options = options;
+ this._emitKeyboardOptionsChanged();
+ }
+ }
+
+ get inputSources() {
+ let sourcesList = [];
+ let layouts = this._layouts.split(',');
+ let variants = this._variants.split(',');
+
+ for (let i = 0; i < layouts.length && !!layouts[i]; i++) {
+ let id = layouts[i];
+ if (variants[i])
+ id += '+%s'.format(variants[i]);
+ sourcesList.push({ type: INPUT_SOURCE_TYPE_XKB, id });
+ }
+ return sourcesList;
+ }
+
+ get keyboardOptions() {
+ return this._options.split(',');
+ }
+};
+
+var InputSourceSessionSettings = class extends InputSourceSettings {
+ constructor() {
+ super();
+
+ this._DESKTOP_INPUT_SOURCES_SCHEMA = 'org.gnome.desktop.input-sources';
+ this._KEY_INPUT_SOURCES = 'sources';
+ this._KEY_MRU_SOURCES = 'mru-sources';
+ this._KEY_KEYBOARD_OPTIONS = 'xkb-options';
+ this._KEY_PER_WINDOW = 'per-window';
+
+ this._settings = new Gio.Settings({ schema_id: this._DESKTOP_INPUT_SOURCES_SCHEMA });
+ this._settings.connect('changed::%s'.format(this._KEY_INPUT_SOURCES), this._emitInputSourcesChanged.bind(this));
+ this._settings.connect('changed::%s'.format(this._KEY_KEYBOARD_OPTIONS), this._emitKeyboardOptionsChanged.bind(this));
+ this._settings.connect('changed::%s'.format(this._KEY_PER_WINDOW), this._emitPerWindowChanged.bind(this));
+ }
+
+ _getSourcesList(key) {
+ let sourcesList = [];
+ let sources = this._settings.get_value(key);
+ let nSources = sources.n_children();
+
+ for (let i = 0; i < nSources; i++) {
+ let [type, id] = sources.get_child_value(i).deep_unpack();
+ sourcesList.push({ type, id });
+ }
+ return sourcesList;
+ }
+
+ get inputSources() {
+ return this._getSourcesList(this._KEY_INPUT_SOURCES);
+ }
+
+ get mruSources() {
+ return this._getSourcesList(this._KEY_MRU_SOURCES);
+ }
+
+ set mruSources(sourcesList) {
+ let sources = GLib.Variant.new('a(ss)', sourcesList);
+ this._settings.set_value(this._KEY_MRU_SOURCES, sources);
+ }
+
+ get keyboardOptions() {
+ return this._settings.get_strv(this._KEY_KEYBOARD_OPTIONS);
+ }
+
+ get perWindow() {
+ return this._settings.get_boolean(this._KEY_PER_WINDOW);
+ }
+};
+
+var InputSourceManager = class {
+ constructor() {
+ // All valid input sources currently in the gsettings
+ // KEY_INPUT_SOURCES list indexed by their index there
+ this._inputSources = {};
+ // All valid input sources currently in the gsettings
+ // KEY_INPUT_SOURCES list of type INPUT_SOURCE_TYPE_IBUS
+ // indexed by the IBus ID
+ this._ibusSources = {};
+
+ this._currentSource = null;
+
+ // All valid input sources currently in the gsettings
+ // KEY_INPUT_SOURCES list ordered by most recently used
+ this._mruSources = [];
+ this._mruSourcesBackup = null;
+ this._keybindingAction =
+ Main.wm.addKeybinding('switch-input-source',
+ new Gio.Settings({ schema_id: "org.gnome.desktop.wm.keybindings" }),
+ Meta.KeyBindingFlags.NONE,
+ Shell.ActionMode.ALL,
+ this._switchInputSource.bind(this));
+ this._keybindingActionBackward =
+ Main.wm.addKeybinding('switch-input-source-backward',
+ new Gio.Settings({ schema_id: "org.gnome.desktop.wm.keybindings" }),
+ Meta.KeyBindingFlags.IS_REVERSED,
+ Shell.ActionMode.ALL,
+ this._switchInputSource.bind(this));
+ if (Main.sessionMode.isGreeter)
+ this._settings = new InputSourceSystemSettings();
+ else
+ this._settings = new InputSourceSessionSettings();
+ this._settings.connect('input-sources-changed', this._inputSourcesChanged.bind(this));
+ this._settings.connect('keyboard-options-changed', this._keyboardOptionsChanged.bind(this));
+
+ this._xkbInfo = KeyboardManager.getXkbInfo();
+ this._keyboardManager = KeyboardManager.getKeyboardManager();
+
+ this._ibusReady = false;
+ this._ibusManager = IBusManager.getIBusManager();
+ this._ibusManager.connect('ready', this._ibusReadyCallback.bind(this));
+ this._ibusManager.connect('properties-registered', this._ibusPropertiesRegistered.bind(this));
+ this._ibusManager.connect('property-updated', this._ibusPropertyUpdated.bind(this));
+ this._ibusManager.connect('set-content-type', this._ibusSetContentType.bind(this));
+
+ global.display.connect('modifiers-accelerator-activated', this._modifiersSwitcher.bind(this));
+
+ this._sourcesPerWindow = false;
+ this._focusWindowNotifyId = 0;
+ this._overviewShowingId = 0;
+ this._overviewHiddenId = 0;
+ this._settings.connect('per-window-changed', this._sourcesPerWindowChanged.bind(this));
+ this._sourcesPerWindowChanged();
+ this._disableIBus = false;
+ this._reloading = false;
+ }
+
+ reload() {
+ this._reloading = true;
+ this._keyboardManager.setKeyboardOptions(this._settings.keyboardOptions);
+ this._inputSourcesChanged();
+ this._reloading = false;
+ }
+
+ _ibusReadyCallback(im, ready) {
+ if (this._ibusReady == ready)
+ return;
+
+ this._ibusReady = ready;
+ this._mruSources = [];
+ this._inputSourcesChanged();
+ }
+
+ _modifiersSwitcher() {
+ let sourceIndexes = Object.keys(this._inputSources);
+ if (sourceIndexes.length == 0) {
+ KeyboardManager.releaseKeyboard();
+ return true;
+ }
+
+ let is = this._currentSource;
+ if (!is)
+ is = this._inputSources[sourceIndexes[0]];
+
+ let nextIndex = is.index + 1;
+ if (nextIndex > sourceIndexes[sourceIndexes.length - 1])
+ nextIndex = 0;
+
+ while (!(is = this._inputSources[nextIndex]))
+ nextIndex += 1;
+
+ is.activate(true);
+ return true;
+ }
+
+ _switchInputSource(display, window, binding) {
+ if (this._mruSources.length < 2)
+ return;
+
+ // HACK: Fall back on simple input source switching since we
+ // can't show a popup switcher while a GrabHelper grab is in
+ // effect without considerable work to consolidate the usage
+ // of pushModal/popModal and grabHelper. See
+ // https://bugzilla.gnome.org/show_bug.cgi?id=695143 .
+ if (Main.actionMode == Shell.ActionMode.POPUP) {
+ this._modifiersSwitcher();
+ return;
+ }
+
+ let popup = new InputSourcePopup(this._mruSources, this._keybindingAction, this._keybindingActionBackward);
+ if (!popup.show(binding.is_reversed(), binding.get_name(), binding.get_mask()))
+ popup.fadeAndDestroy();
+ }
+
+ _keyboardOptionsChanged() {
+ this._keyboardManager.setKeyboardOptions(this._settings.keyboardOptions);
+ this._keyboardManager.reapply();
+ }
+
+ _updateMruSettings() {
+ // If IBus is not ready we don't have a full picture of all
+ // the available sources, so don't update the setting
+ if (!this._ibusReady)
+ return;
+
+ // If IBus is temporarily disabled, don't update the setting
+ if (this._disableIBus)
+ return;
+
+ let sourcesList = [];
+ for (let i = 0; i < this._mruSources.length; ++i) {
+ let source = this._mruSources[i];
+ sourcesList.push([source.type, source.id]);
+ }
+
+ this._settings.mruSources = sourcesList;
+ }
+
+ _currentInputSourceChanged(newSource) {
+ let oldSource;
+ [oldSource, this._currentSource] = [this._currentSource, newSource];
+
+ this.emit('current-source-changed', oldSource);
+
+ for (let i = 1; i < this._mruSources.length; ++i) {
+ if (this._mruSources[i] == newSource) {
+ let currentSource = this._mruSources.splice(i, 1);
+ this._mruSources = currentSource.concat(this._mruSources);
+ break;
+ }
+ }
+ this._changePerWindowSource();
+ }
+
+ activateInputSource(is, interactive) {
+ // The focus changes during holdKeyboard/releaseKeyboard may trick
+ // the client into hiding UI containing the currently focused entry.
+ // So holdKeyboard/releaseKeyboard are not called when
+ // 'set-content-type' signal is received.
+ // E.g. Focusing on a password entry in a popup in Xorg Firefox
+ // will emit 'set-content-type' signal.
+ // https://gitlab.gnome.org/GNOME/gnome-shell/issues/391
+ if (!this._reloading)
+ KeyboardManager.holdKeyboard();
+ this._keyboardManager.apply(is.xkbId);
+
+ // All the "xkb:..." IBus engines simply "echo" back symbols,
+ // despite their naming implying differently, so we always set
+ // one in order for XIM applications to work given that we set
+ // XMODIFIERS=@im=ibus in the first place so that they can
+ // work without restarting when/if the user adds an IBus input
+ // source.
+ let engine;
+ if (is.type == INPUT_SOURCE_TYPE_IBUS)
+ engine = is.id;
+ else
+ engine = 'xkb:us::eng';
+
+ if (!this._reloading)
+ this._ibusManager.setEngine(engine, KeyboardManager.releaseKeyboard);
+ else
+ this._ibusManager.setEngine(engine);
+ this._currentInputSourceChanged(is);
+
+ if (interactive)
+ this._updateMruSettings();
+ }
+
+ _updateMruSources() {
+ let sourcesList = [];
+ for (let i in this._inputSources)
+ sourcesList.push(this._inputSources[i]);
+
+ this._keyboardManager.setUserLayouts(sourcesList.map(x => x.xkbId));
+
+ if (!this._disableIBus && this._mruSourcesBackup) {
+ this._mruSources = this._mruSourcesBackup;
+ this._mruSourcesBackup = null;
+ }
+
+ // Initialize from settings when we have no MRU sources list
+ if (this._mruSources.length == 0) {
+ let mruSettings = this._settings.mruSources;
+ for (let i = 0; i < mruSettings.length; i++) {
+ let mruSettingSource = mruSettings[i];
+ let mruSource = null;
+
+ for (let j = 0; j < sourcesList.length; j++) {
+ let source = sourcesList[j];
+ if (source.type == mruSettingSource.type &&
+ source.id == mruSettingSource.id) {
+ mruSource = source;
+ break;
+ }
+ }
+
+ if (mruSource)
+ this._mruSources.push(mruSource);
+ }
+ }
+
+ let mruSources = [];
+ for (let i = 0; i < this._mruSources.length; i++) {
+ for (let j = 0; j < sourcesList.length; j++) {
+ if (this._mruSources[i].type == sourcesList[j].type &&
+ this._mruSources[i].id == sourcesList[j].id) {
+ mruSources = mruSources.concat(sourcesList.splice(j, 1));
+ break;
+ }
+ }
+ }
+ this._mruSources = mruSources.concat(sourcesList);
+ }
+
+ _inputSourcesChanged() {
+ let sources = this._settings.inputSources;
+ let nSources = sources.length;
+
+ this._currentSource = null;
+ this._inputSources = {};
+ this._ibusSources = {};
+
+ let infosList = [];
+ for (let i = 0; i < nSources; i++) {
+ let displayName;
+ let shortName;
+ let type = sources[i].type;
+ let id = sources[i].id;
+ let exists = false;
+
+ if (type == INPUT_SOURCE_TYPE_XKB) {
+ [exists, displayName, shortName] =
+ this._xkbInfo.get_layout_info(id);
+ } else if (type == INPUT_SOURCE_TYPE_IBUS) {
+ if (this._disableIBus)
+ continue;
+ let engineDesc = this._ibusManager.getEngineDesc(id);
+ if (engineDesc) {
+ let language = IBus.get_language_name(engineDesc.get_language());
+ let longName = engineDesc.get_longname();
+ let textdomain = engineDesc.get_textdomain();
+ if (textdomain != '')
+ longName = Gettext.dgettext(textdomain, longName);
+ exists = true;
+ displayName = '%s (%s)'.format(language, longName);
+ shortName = this._makeEngineShortName(engineDesc);
+ }
+ }
+
+ if (exists)
+ infosList.push({ type, id, displayName, shortName });
+ }
+
+ if (infosList.length == 0) {
+ let type = INPUT_SOURCE_TYPE_XKB;
+ let id = KeyboardManager.DEFAULT_LAYOUT;
+ let [, displayName, shortName] = this._xkbInfo.get_layout_info(id);
+ infosList.push({ type, id, displayName, shortName });
+ }
+
+ let inputSourcesByShortName = {};
+ for (let i = 0; i < infosList.length; i++) {
+ let is = new InputSource(infosList[i].type,
+ infosList[i].id,
+ infosList[i].displayName,
+ infosList[i].shortName,
+ i);
+ is.connect('activate', this.activateInputSource.bind(this));
+
+ if (!(is.shortName in inputSourcesByShortName))
+ inputSourcesByShortName[is.shortName] = [];
+ inputSourcesByShortName[is.shortName].push(is);
+
+ this._inputSources[is.index] = is;
+
+ if (is.type == INPUT_SOURCE_TYPE_IBUS)
+ this._ibusSources[is.id] = is;
+ }
+
+ for (let i in this._inputSources) {
+ let is = this._inputSources[i];
+ if (inputSourcesByShortName[is.shortName].length > 1) {
+ let sub = inputSourcesByShortName[is.shortName].indexOf(is) + 1;
+ is.shortName += String.fromCharCode(0x2080 + sub);
+ }
+ }
+
+ this.emit('sources-changed');
+
+ this._updateMruSources();
+
+ if (this._mruSources.length > 0)
+ this._mruSources[0].activate(false);
+
+ // All ibus engines are preloaded here to reduce the launching time
+ // when users switch the input sources.
+ this._ibusManager.preloadEngines(Object.keys(this._ibusSources));
+ }
+
+ _makeEngineShortName(engineDesc) {
+ let symbol = engineDesc.get_symbol();
+ if (symbol && symbol[0])
+ return symbol;
+
+ let langCode = engineDesc.get_language().split('_', 1)[0];
+ if (langCode.length == 2 || langCode.length == 3)
+ return langCode.toLowerCase();
+
+ return String.fromCharCode(0x2328); // keyboard glyph
+ }
+
+ _ibusPropertiesRegistered(im, engineName, props) {
+ let source = this._ibusSources[engineName];
+ if (!source)
+ return;
+
+ source.properties = props;
+
+ if (source == this._currentSource)
+ this.emit('current-source-changed', null);
+ }
+
+ _ibusPropertyUpdated(im, engineName, prop) {
+ let source = this._ibusSources[engineName];
+ if (!source)
+ return;
+
+ if (this._updateSubProperty(source.properties, prop) &&
+ source == this._currentSource)
+ this.emit('current-source-changed', null);
+ }
+
+ _updateSubProperty(props, prop) {
+ if (!props)
+ return false;
+
+ let p;
+ for (let i = 0; (p = props.get(i)) != null; ++i) {
+ if (p.get_key() == prop.get_key() && p.get_prop_type() == prop.get_prop_type()) {
+ p.update(prop);
+ return true;
+ } else if (p.get_prop_type() == IBus.PropType.MENU) {
+ if (this._updateSubProperty(p.get_sub_props(), prop))
+ return true;
+ }
+ }
+ return false;
+ }
+
+ _ibusSetContentType(im, purpose, _hints) {
+ if (purpose == IBus.InputPurpose.PASSWORD) {
+ if (Object.keys(this._inputSources).length == Object.keys(this._ibusSources).length)
+ return;
+
+ if (this._disableIBus)
+ return;
+ this._disableIBus = true;
+ this._mruSourcesBackup = this._mruSources.slice();
+ } else {
+ if (!this._disableIBus)
+ return;
+ this._disableIBus = false;
+ }
+ this.reload();
+ }
+
+ _getNewInputSource(current) {
+ let sourceIndexes = Object.keys(this._inputSources);
+ if (sourceIndexes.length == 0)
+ return null;
+
+ if (current) {
+ for (let i in this._inputSources) {
+ let is = this._inputSources[i];
+ if (is.type == current.type &&
+ is.id == current.id)
+ return is;
+ }
+ }
+
+ return this._inputSources[sourceIndexes[0]];
+ }
+
+ _getCurrentWindow() {
+ if (Main.overview.visible)
+ return Main.overview;
+ else
+ return global.display.focus_window;
+ }
+
+ _setPerWindowInputSource() {
+ let window = this._getCurrentWindow();
+ if (!window)
+ return;
+
+ if (!window._inputSources ||
+ window._inputSources !== this._inputSources) {
+ window._inputSources = this._inputSources;
+ window._currentSource = this._getNewInputSource(window._currentSource);
+ }
+
+ if (window._currentSource)
+ window._currentSource.activate(false);
+ }
+
+ _sourcesPerWindowChanged() {
+ this._sourcesPerWindow = this._settings.perWindow;
+
+ if (this._sourcesPerWindow && this._focusWindowNotifyId == 0) {
+ this._focusWindowNotifyId = global.display.connect('notify::focus-window',
+ this._setPerWindowInputSource.bind(this));
+ this._overviewShowingId = Main.overview.connect('showing',
+ this._setPerWindowInputSource.bind(this));
+ this._overviewHiddenId = Main.overview.connect('hidden',
+ this._setPerWindowInputSource.bind(this));
+ } else if (!this._sourcesPerWindow && this._focusWindowNotifyId != 0) {
+ global.display.disconnect(this._focusWindowNotifyId);
+ this._focusWindowNotifyId = 0;
+ Main.overview.disconnect(this._overviewShowingId);
+ this._overviewShowingId = 0;
+ Main.overview.disconnect(this._overviewHiddenId);
+ this._overviewHiddenId = 0;
+
+ let windows = global.get_window_actors().map(w => w.meta_window);
+ for (let i = 0; i < windows.length; ++i) {
+ delete windows[i]._inputSources;
+ delete windows[i]._currentSource;
+ }
+ delete Main.overview._inputSources;
+ delete Main.overview._currentSource;
+ }
+ }
+
+ _changePerWindowSource() {
+ if (!this._sourcesPerWindow)
+ return;
+
+ let window = this._getCurrentWindow();
+ if (!window)
+ return;
+
+ window._inputSources = this._inputSources;
+ window._currentSource = this._currentSource;
+ }
+
+ get currentSource() {
+ return this._currentSource;
+ }
+
+ get inputSources() {
+ return this._inputSources;
+ }
+};
+Signals.addSignalMethods(InputSourceManager.prototype);
+
+let _inputSourceManager = null;
+
+function getInputSourceManager() {
+ if (_inputSourceManager == null)
+ _inputSourceManager = new InputSourceManager();
+ return _inputSourceManager;
+}
+
+var InputSourceIndicatorContainer = GObject.registerClass(
+class InputSourceIndicatorContainer extends St.Widget {
+
+ vfunc_get_preferred_width(forHeight) {
+ // Here, and in vfunc_get_preferred_height, we need to query
+ // for the height of all children, but we ignore the results
+ // for those we don't actually display.
+ return this.get_children().reduce((maxWidth, child) => {
+ let width = child.get_preferred_width(forHeight);
+ return [Math.max(maxWidth[0], width[0]),
+ Math.max(maxWidth[1], width[1])];
+ }, [0, 0]);
+ }
+
+ vfunc_get_preferred_height(forWidth) {
+ return this.get_children().reduce((maxHeight, child) => {
+ let height = child.get_preferred_height(forWidth);
+ return [Math.max(maxHeight[0], height[0]),
+ Math.max(maxHeight[1], height[1])];
+ }, [0, 0]);
+ }
+
+ vfunc_allocate(box) {
+ this.set_allocation(box);
+
+ // translate box to (0, 0)
+ box.x2 -= box.x1;
+ box.x1 = 0;
+ box.y2 -= box.y1;
+ box.y1 = 0;
+
+ this.get_children().forEach(c => {
+ c.allocate_align_fill(box, 0.5, 0.5, false, false);
+ });
+ }
+});
+
+var InputSourceIndicator = GObject.registerClass(
+class InputSourceIndicator extends PanelMenu.Button {
+ _init() {
+ super._init(0.5, _("Keyboard"));
+
+ this.connect('destroy', this._onDestroy.bind(this));
+
+ this._menuItems = {};
+ this._indicatorLabels = {};
+
+ this._container = new InputSourceIndicatorContainer();
+
+ this._hbox = new St.BoxLayout({ style_class: 'panel-status-menu-box' });
+ this._hbox.add_child(this._container);
+ this._hbox.add_child(PopupMenu.arrowIcon(St.Side.BOTTOM));
+
+ this.add_child(this._hbox);
+
+ this._propSeparator = new PopupMenu.PopupSeparatorMenuItem();
+ this.menu.addMenuItem(this._propSeparator);
+ this._propSection = new PopupMenu.PopupMenuSection();
+ this.menu.addMenuItem(this._propSection);
+ this._propSection.actor.hide();
+
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+ this._showLayoutItem = this.menu.addAction(_("Show Keyboard Layout"), this._showLayout.bind(this));
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+
+ this._inputSourceManager = getInputSourceManager();
+ this._inputSourceManagerSourcesChangedId =
+ this._inputSourceManager.connect('sources-changed', this._sourcesChanged.bind(this));
+ this._inputSourceManagerCurrentSourceChangedId =
+ this._inputSourceManager.connect('current-source-changed', this._currentSourceChanged.bind(this));
+ this._inputSourceManager.reload();
+ }
+
+ _onDestroy() {
+ if (this._inputSourceManager) {
+ this._inputSourceManager.disconnect(this._inputSourceManagerSourcesChangedId);
+ this._inputSourceManager.disconnect(this._inputSourceManagerCurrentSourceChangedId);
+ this._inputSourceManager = null;
+ }
+ }
+
+ _sessionUpdated() {
+ // re-using "allowSettings" for the keyboard layout is a bit shady,
+ // but at least for now it is used as "allow popping up windows
+ // from shell menus"; we can always add a separate sessionMode
+ // option if need arises.
+ this._showLayoutItem.visible = Main.sessionMode.allowSettings;
+ }
+
+ _sourcesChanged() {
+ for (let i in this._menuItems)
+ this._menuItems[i].destroy();
+ for (let i in this._indicatorLabels)
+ this._indicatorLabels[i].destroy();
+
+ this._menuItems = {};
+ this._indicatorLabels = {};
+
+ let menuIndex = 0;
+ for (let i in this._inputSourceManager.inputSources) {
+ let is = this._inputSourceManager.inputSources[i];
+
+ let menuItem = new LayoutMenuItem(is.displayName, is.shortName);
+ menuItem.connect('activate', () => is.activate(true));
+
+ let indicatorLabel = new St.Label({ text: is.shortName,
+ visible: false });
+
+ this._menuItems[i] = menuItem;
+ this._indicatorLabels[i] = indicatorLabel;
+ is.connect('changed', () => {
+ menuItem.indicator.set_text(is.shortName);
+ indicatorLabel.set_text(is.shortName);
+ });
+
+ this.menu.addMenuItem(menuItem, menuIndex++);
+ this._container.add_actor(indicatorLabel);
+ }
+ }
+
+ _currentSourceChanged(manager, oldSource) {
+ let nVisibleSources = Object.keys(this._inputSourceManager.inputSources).length;
+ let newSource = this._inputSourceManager.currentSource;
+
+ if (oldSource) {
+ this._menuItems[oldSource.index].setOrnament(PopupMenu.Ornament.NONE);
+ this._indicatorLabels[oldSource.index].hide();
+ }
+
+ if (!newSource || (nVisibleSources < 2 && !newSource.properties)) {
+ // This source index might be invalid if we weren't able
+ // to build a menu item for it, so we hide ourselves since
+ // we can't fix it here. *shrug*
+
+ // We also hide if we have only one visible source unless
+ // it's an IBus source with properties.
+ this.menu.close();
+ this.hide();
+ return;
+ }
+
+ this.show();
+
+ this._buildPropSection(newSource.properties);
+
+ this._menuItems[newSource.index].setOrnament(PopupMenu.Ornament.DOT);
+ this._indicatorLabels[newSource.index].show();
+ }
+
+ _buildPropSection(properties) {
+ this._propSeparator.hide();
+ this._propSection.actor.hide();
+ this._propSection.removeAll();
+
+ this._buildPropSubMenu(this._propSection, properties);
+
+ if (!this._propSection.isEmpty()) {
+ this._propSection.actor.show();
+ this._propSeparator.show();
+ }
+ }
+
+ _buildPropSubMenu(menu, props) {
+ if (!props)
+ return;
+
+ let ibusManager = IBusManager.getIBusManager();
+ let radioGroup = [];
+ let p;
+ for (let i = 0; (p = props.get(i)) != null; ++i) {
+ let prop = p;
+
+ if (!prop.get_visible())
+ continue;
+
+ if (prop.get_key() == 'InputMode') {
+ let text;
+ if (prop.get_symbol)
+ text = prop.get_symbol().get_text();
+ else
+ text = prop.get_label().get_text();
+
+ let currentSource = this._inputSourceManager.currentSource;
+ if (currentSource) {
+ let indicatorLabel = this._indicatorLabels[currentSource.index];
+ if (text && text.length > 0 && text.length < 3)
+ indicatorLabel.set_text(text);
+ }
+ }
+
+ let item;
+ let type = prop.get_prop_type();
+ switch (type) {
+ case IBus.PropType.MENU:
+ item = new PopupMenu.PopupSubMenuMenuItem(prop.get_label().get_text());
+ this._buildPropSubMenu(item.menu, prop.get_sub_props());
+ break;
+
+ case IBus.PropType.RADIO:
+ item = new PopupMenu.PopupMenuItem(prop.get_label().get_text());
+ item.prop = prop;
+ radioGroup.push(item);
+ item.radioGroup = radioGroup;
+ item.setOrnament(prop.get_state() == IBus.PropState.CHECKED
+ ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE);
+ item.connect('activate', () => {
+ if (item.prop.get_state() == IBus.PropState.CHECKED)
+ return;
+
+ let group = item.radioGroup;
+ for (let j = 0; j < group.length; ++j) {
+ if (group[j] == item) {
+ item.setOrnament(PopupMenu.Ornament.DOT);
+ item.prop.set_state(IBus.PropState.CHECKED);
+ ibusManager.activateProperty(item.prop.get_key(),
+ IBus.PropState.CHECKED);
+ } else {
+ group[j].setOrnament(PopupMenu.Ornament.NONE);
+ group[j].prop.set_state(IBus.PropState.UNCHECKED);
+ ibusManager.activateProperty(group[j].prop.get_key(),
+ IBus.PropState.UNCHECKED);
+ }
+ }
+ });
+ break;
+
+ case IBus.PropType.TOGGLE:
+ item = new PopupMenu.PopupSwitchMenuItem(prop.get_label().get_text(), prop.get_state() == IBus.PropState.CHECKED);
+ item.prop = prop;
+ item.connect('toggled', () => {
+ if (item.state) {
+ item.prop.set_state(IBus.PropState.CHECKED);
+ ibusManager.activateProperty(item.prop.get_key(),
+ IBus.PropState.CHECKED);
+ } else {
+ item.prop.set_state(IBus.PropState.UNCHECKED);
+ ibusManager.activateProperty(item.prop.get_key(),
+ IBus.PropState.UNCHECKED);
+ }
+ });
+ break;
+
+ case IBus.PropType.NORMAL:
+ item = new PopupMenu.PopupMenuItem(prop.get_label().get_text());
+ item.prop = prop;
+ item.connect('activate', () => {
+ ibusManager.activateProperty(item.prop.get_key(),
+ item.prop.get_state());
+ });
+ break;
+
+ case IBus.PropType.SEPARATOR:
+ item = new PopupMenu.PopupSeparatorMenuItem();
+ break;
+
+ default:
+ log('IBus property %s has invalid type %d'.format(prop.get_key(), type));
+ continue;
+ }
+
+ item.setSensitive(prop.get_sensitive());
+ menu.addMenuItem(item);
+ }
+ }
+
+ _showLayout() {
+ Main.overview.hide();
+
+ let source = this._inputSourceManager.currentSource;
+ let xkbLayout = '';
+ let xkbVariant = '';
+
+ if (source.type == INPUT_SOURCE_TYPE_XKB) {
+ [, , , xkbLayout, xkbVariant] = KeyboardManager.getXkbInfo().get_layout_info(source.id);
+ } else if (source.type == INPUT_SOURCE_TYPE_IBUS) {
+ let engineDesc = IBusManager.getIBusManager().getEngineDesc(source.id);
+ if (engineDesc) {
+ xkbLayout = engineDesc.get_layout();
+ xkbVariant = engineDesc.get_layout_variant();
+ }
+ }
+
+ if (!xkbLayout || xkbLayout.length == 0)
+ return;
+
+ let description = xkbLayout;
+ if (xkbVariant.length > 0)
+ description = '%s\t%s'.format(description, xkbVariant);
+
+ Util.spawn(['gkbd-keyboard-display', '-l', description]);
+ }
+});
diff --git a/js/ui/status/location.js b/js/ui/status/location.js
new file mode 100644
index 0000000..4250ed0
--- /dev/null
+++ b/js/ui/status/location.js
@@ -0,0 +1,387 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
+
+const Dialog = imports.ui.dialog;
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+const ModalDialog = imports.ui.modalDialog;
+const PermissionStore = imports.misc.permissionStore;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const LOCATION_SCHEMA = 'org.gnome.system.location';
+const MAX_ACCURACY_LEVEL = 'max-accuracy-level';
+const ENABLED = 'enabled';
+
+const APP_PERMISSIONS_TABLE = 'gnome';
+const APP_PERMISSIONS_ID = 'geolocation';
+
+var GeoclueAccuracyLevel = {
+ NONE: 0,
+ COUNTRY: 1,
+ CITY: 4,
+ NEIGHBORHOOD: 5,
+ STREET: 6,
+ EXACT: 8,
+};
+
+function accuracyLevelToString(accuracyLevel) {
+ for (let key in GeoclueAccuracyLevel) {
+ if (GeoclueAccuracyLevel[key] == accuracyLevel)
+ return key;
+ }
+
+ return 'NONE';
+}
+
+var GeoclueIface = loadInterfaceXML('org.freedesktop.GeoClue2.Manager');
+const GeoclueManager = Gio.DBusProxy.makeProxyWrapper(GeoclueIface);
+
+var AgentIface = loadInterfaceXML('org.freedesktop.GeoClue2.Agent');
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._settings = new Gio.Settings({ schema_id: LOCATION_SCHEMA });
+ this._settings.connect('changed::%s'.format(ENABLED),
+ this._onMaxAccuracyLevelChanged.bind(this));
+ this._settings.connect('changed::%s'.format(MAX_ACCURACY_LEVEL),
+ this._onMaxAccuracyLevelChanged.bind(this));
+
+ this._indicator = this._addIndicator();
+ this._indicator.icon_name = 'find-location-symbolic';
+
+ this._item = new PopupMenu.PopupSubMenuMenuItem('', true);
+ this._item.icon.icon_name = 'find-location-symbolic';
+
+ this._agent = Gio.DBusExportedObject.wrapJSObject(AgentIface, this);
+ this._agent.export(Gio.DBus.system, '/org/freedesktop/GeoClue2/Agent');
+
+ this._item.label.text = _("Location Enabled");
+ this._onOffAction = this._item.menu.addAction(_("Disable"), this._onOnOffAction.bind(this));
+ this._item.menu.addSettingsAction(_('Privacy Settings'), 'gnome-location-panel.desktop');
+
+ this.menu.addMenuItem(this._item);
+
+ this._watchId = Gio.bus_watch_name(Gio.BusType.SYSTEM,
+ 'org.freedesktop.GeoClue2',
+ 0,
+ this._connectToGeoclue.bind(this),
+ this._onGeoclueVanished.bind(this));
+ Main.sessionMode.connect('updated', this._onSessionUpdated.bind(this));
+ this._onSessionUpdated();
+ this._onMaxAccuracyLevelChanged();
+ this._connectToGeoclue();
+ this._connectToPermissionStore();
+ }
+
+ get MaxAccuracyLevel() {
+ return this._getMaxAccuracyLevel();
+ }
+
+ AuthorizeAppAsync(params, invocation) {
+ let [desktopId, reqAccuracyLevel] = params;
+
+ let authorizer = new AppAuthorizer(desktopId,
+ reqAccuracyLevel,
+ this._permStoreProxy,
+ this._getMaxAccuracyLevel());
+
+ authorizer.authorize(accuracyLevel => {
+ let ret = accuracyLevel != GeoclueAccuracyLevel.NONE;
+ invocation.return_value(GLib.Variant.new('(bu)',
+ [ret, accuracyLevel]));
+ });
+ }
+
+ _syncIndicator() {
+ if (this._managerProxy == null) {
+ this._indicator.visible = false;
+ this._item.visible = false;
+ return;
+ }
+
+ this._indicator.visible = this._managerProxy.InUse;
+ this._item.visible = this._indicator.visible;
+ this._updateMenuLabels();
+ }
+
+ _connectToGeoclue() {
+ if (this._managerProxy != null || this._connecting)
+ return false;
+
+ this._connecting = true;
+ new GeoclueManager(Gio.DBus.system,
+ 'org.freedesktop.GeoClue2',
+ '/org/freedesktop/GeoClue2/Manager',
+ this._onManagerProxyReady.bind(this));
+ return true;
+ }
+
+ _onManagerProxyReady(proxy, error) {
+ if (error != null) {
+ log(error.message);
+ this._connecting = false;
+ return;
+ }
+
+ this._managerProxy = proxy;
+ this._propertiesChangedId = this._managerProxy.connect('g-properties-changed',
+ this._onGeocluePropsChanged.bind(this));
+
+ this._syncIndicator();
+
+ this._managerProxy.AddAgentRemote('gnome-shell', this._onAgentRegistered.bind(this));
+ }
+
+ _onAgentRegistered(result, error) {
+ this._connecting = false;
+ this._notifyMaxAccuracyLevel();
+
+ if (error != null)
+ log(error.message);
+ }
+
+ _onGeoclueVanished() {
+ if (this._propertiesChangedId) {
+ this._managerProxy.disconnect(this._propertiesChangedId);
+ this._propertiesChangedId = 0;
+ }
+ this._managerProxy = null;
+
+ this._syncIndicator();
+ }
+
+ _onOnOffAction() {
+ let enabled = this._settings.get_boolean(ENABLED);
+ this._settings.set_boolean(ENABLED, !enabled);
+ }
+
+ _onSessionUpdated() {
+ let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this.menu.setSensitive(sensitive);
+ }
+
+ _updateMenuLabels() {
+ if (this._settings.get_boolean(ENABLED)) {
+ this._item.label.text = this._indicator.visible
+ ? _("Location In Use")
+ : _("Location Enabled");
+ this._onOffAction.label.text = _("Disable");
+ } else {
+ this._item.label.text = _("Location Disabled");
+ this._onOffAction.label.text = _("Enable");
+ }
+ }
+
+ _onMaxAccuracyLevelChanged() {
+ this._updateMenuLabels();
+
+ // Gotta ensure geoclue is up and we are registered as agent to it
+ // before we emit the notify for this property change.
+ if (!this._connectToGeoclue())
+ this._notifyMaxAccuracyLevel();
+ }
+
+ _getMaxAccuracyLevel() {
+ if (this._settings.get_boolean(ENABLED)) {
+ let level = this._settings.get_string(MAX_ACCURACY_LEVEL);
+
+ return GeoclueAccuracyLevel[level.toUpperCase()] ||
+ GeoclueAccuracyLevel.NONE;
+ } else {
+ return GeoclueAccuracyLevel.NONE;
+ }
+ }
+
+ _notifyMaxAccuracyLevel() {
+ let variant = new GLib.Variant('u', this._getMaxAccuracyLevel());
+ this._agent.emit_property_changed('MaxAccuracyLevel', variant);
+ }
+
+ _onGeocluePropsChanged(proxy, properties) {
+ let unpacked = properties.deep_unpack();
+ if ("InUse" in unpacked)
+ this._syncIndicator();
+ }
+
+ _connectToPermissionStore() {
+ this._permStoreProxy = null;
+ new PermissionStore.PermissionStore(this._onPermStoreProxyReady.bind(this));
+ }
+
+ _onPermStoreProxyReady(proxy, error) {
+ if (error != null) {
+ log(error.message);
+ return;
+ }
+
+ this._permStoreProxy = proxy;
+ }
+});
+
+var AppAuthorizer = class {
+ constructor(desktopId, reqAccuracyLevel, permStoreProxy, maxAccuracyLevel) {
+ this.desktopId = desktopId;
+ this.reqAccuracyLevel = reqAccuracyLevel;
+ this._permStoreProxy = permStoreProxy;
+ this._maxAccuracyLevel = maxAccuracyLevel;
+ this._permissions = {};
+
+ this._accuracyLevel = GeoclueAccuracyLevel.NONE;
+ }
+
+ authorize(onAuthDone) {
+ this._onAuthDone = onAuthDone;
+
+ let appSystem = Shell.AppSystem.get_default();
+ this._app = appSystem.lookup_app('%s.desktop'.format(this.desktopId));
+ if (this._app == null || this._permStoreProxy == null) {
+ this._completeAuth();
+
+ return;
+ }
+
+ this._permStoreProxy.LookupRemote(APP_PERMISSIONS_TABLE,
+ APP_PERMISSIONS_ID,
+ this._onPermLookupDone.bind(this));
+ }
+
+ _onPermLookupDone(result, error) {
+ if (error != null) {
+ if (error.domain == Gio.DBusError) {
+ // Likely no xdg-app installed, just authorize the app
+ this._accuracyLevel = this.reqAccuracyLevel;
+ this._permStoreProxy = null;
+ this._completeAuth();
+ } else {
+ // Currently xdg-app throws an error if we lookup for
+ // unknown ID (which would be the case first time this code
+ // runs) so we continue with user authorization as normal
+ // and ID is added to the store if user says "yes".
+ log(error.message);
+ this._permissions = {};
+ this._userAuthorizeApp();
+ }
+
+ return;
+ }
+
+ [this._permissions] = result;
+ let permission = this._permissions[this.desktopId];
+
+ if (permission == null) {
+ this._userAuthorizeApp();
+ } else {
+ let [levelStr] = permission || ['NONE'];
+ this._accuracyLevel = GeoclueAccuracyLevel[levelStr] ||
+ GeoclueAccuracyLevel.NONE;
+ this._completeAuth();
+ }
+ }
+
+ _userAuthorizeApp() {
+ let name = this._app.get_name();
+ let appInfo = this._app.get_app_info();
+ let reason = appInfo.get_locale_string("X-Geoclue-Reason");
+
+ this._showAppAuthDialog(name, reason);
+ }
+
+ _showAppAuthDialog(name, reason) {
+ this._dialog = new GeolocationDialog(name,
+ reason,
+ this.reqAccuracyLevel);
+
+ let responseId = this._dialog.connect('response', (dialog, level) => {
+ this._dialog.disconnect(responseId);
+ this._accuracyLevel = level;
+ this._completeAuth();
+ });
+
+ this._dialog.open();
+ }
+
+ _completeAuth() {
+ if (this._accuracyLevel != GeoclueAccuracyLevel.NONE) {
+ this._accuracyLevel = Math.clamp(this._accuracyLevel,
+ 0, this._maxAccuracyLevel);
+ }
+ this._saveToPermissionStore();
+
+ this._onAuthDone(this._accuracyLevel);
+ }
+
+ _saveToPermissionStore() {
+ if (this._permStoreProxy == null)
+ return;
+
+ let levelStr = accuracyLevelToString(this._accuracyLevel);
+ let dateStr = Math.round(Date.now() / 1000).toString();
+ this._permissions[this.desktopId] = [levelStr, dateStr];
+
+ let data = GLib.Variant.new('av', {});
+
+ this._permStoreProxy.SetRemote(APP_PERMISSIONS_TABLE,
+ true,
+ APP_PERMISSIONS_ID,
+ this._permissions,
+ data,
+ (result, error) => {
+ if (error != null)
+ log(error.message);
+ });
+ }
+};
+
+var GeolocationDialog = GObject.registerClass({
+ Signals: { 'response': { param_types: [GObject.TYPE_UINT] } },
+}, class GeolocationDialog extends ModalDialog.ModalDialog {
+ _init(name, reason, reqAccuracyLevel) {
+ super._init({ styleClass: 'geolocation-dialog' });
+ this.reqAccuracyLevel = reqAccuracyLevel;
+
+ let content = new Dialog.MessageDialogContent({
+ title: _('Allow location access'),
+ /* Translators: %s is an application name */
+ description: _('The app %s wants to access your location').format(name),
+ });
+
+ let reasonLabel = new St.Label({
+ text: reason,
+ style_class: 'message-dialog-description',
+ });
+ content.add_child(reasonLabel);
+
+ let infoLabel = new St.Label({
+ text: _('Location access can be changed at any time from the privacy settings.'),
+ style_class: 'message-dialog-description',
+ });
+ content.add_child(infoLabel);
+
+ this.contentLayout.add_child(content);
+
+ let button = this.addButton({ label: _("Deny Access"),
+ action: this._onDenyClicked.bind(this),
+ key: Clutter.KEY_Escape });
+ this.addButton({ label: _("Grant Access"),
+ action: this._onGrantClicked.bind(this) });
+
+ this.setInitialKeyFocus(button);
+ }
+
+ _onGrantClicked() {
+ this.emit('response', this.reqAccuracyLevel);
+ this.close();
+ }
+
+ _onDenyClicked() {
+ this.emit('response', GeoclueAccuracyLevel.NONE);
+ this.close();
+ }
+});
diff --git a/js/ui/status/network.js b/js/ui/status/network.js
new file mode 100644
index 0000000..377f44e
--- /dev/null
+++ b/js/ui/status/network.js
@@ -0,0 +1,2101 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported NMApplet */
+const { Clutter, Gio, GLib, GObject, NM, St } = imports.gi;
+const Signals = imports.signals;
+
+const Animation = imports.ui.animation;
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+const MessageTray = imports.ui.messageTray;
+const ModalDialog = imports.ui.modalDialog;
+const ModemManager = imports.misc.modemManager;
+const Rfkill = imports.ui.status.rfkill;
+const Util = imports.misc.util;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+Gio._promisify(Gio.DBusConnection.prototype, 'call', 'call_finish');
+Gio._promisify(NM.Client, 'new_async', 'new_finish');
+Gio._promisify(NM.Client.prototype,
+ 'check_connectivity_async', 'check_connectivity_finish');
+
+const NMConnectionCategory = {
+ INVALID: 'invalid',
+ WIRED: 'wired',
+ WIRELESS: 'wireless',
+ WWAN: 'wwan',
+ VPN: 'vpn',
+};
+
+const NMAccessPointSecurity = {
+ NONE: 1,
+ WEP: 2,
+ WPA_PSK: 3,
+ WPA2_PSK: 4,
+ WPA_ENT: 5,
+ WPA2_ENT: 6,
+};
+
+var MAX_DEVICE_ITEMS = 4;
+
+// small optimization, to avoid using [] all the time
+const NM80211Mode = NM['80211Mode'];
+const NM80211ApFlags = NM['80211ApFlags'];
+const NM80211ApSecurityFlags = NM['80211ApSecurityFlags'];
+
+var PortalHelperResult = {
+ CANCELLED: 0,
+ COMPLETED: 1,
+ RECHECK: 2,
+};
+
+const PortalHelperIface = loadInterfaceXML('org.gnome.Shell.PortalHelper');
+const PortalHelperProxy = Gio.DBusProxy.makeProxyWrapper(PortalHelperIface);
+
+function signalToIcon(value) {
+ if (value > 80)
+ return 'excellent';
+ if (value > 55)
+ return 'good';
+ if (value > 30)
+ return 'ok';
+ if (value > 5)
+ return 'weak';
+ return 'none';
+}
+
+function ssidToLabel(ssid) {
+ let label = NM.utils_ssid_to_utf8(ssid.get_data());
+ if (!label)
+ label = _("<unknown>");
+ return label;
+}
+
+function ensureActiveConnectionProps(active) {
+ if (!active._primaryDevice) {
+ let devices = active.get_devices();
+ if (devices.length > 0) {
+ // This list is guaranteed to have at most one device in it.
+ let device = devices[0]._delegate;
+ active._primaryDevice = device;
+ }
+ }
+}
+
+function launchSettingsPanel(panel, ...args) {
+ const param = new GLib.Variant('(sav)',
+ [panel, args.map(s => new GLib.Variant('s', s))]);
+ const platformData = {
+ 'desktop-startup-id': new GLib.Variant('s',
+ '_TIME%s'.format(global.get_current_time())),
+ };
+ try {
+ Gio.DBus.session.call(
+ 'org.gnome.ControlCenter',
+ '/org/gnome/ControlCenter',
+ 'org.freedesktop.Application',
+ 'ActivateAction',
+ new GLib.Variant('(sava{sv})',
+ ['launch-panel', [param], platformData]),
+ null,
+ Gio.DBusCallFlags.NONE,
+ -1,
+ null);
+ } catch (e) {
+ log('Failed to launch Settings panel: %s'.format(e.message));
+ }
+}
+
+var NMConnectionItem = class {
+ constructor(section, connection) {
+ this._section = section;
+ this._connection = connection;
+ this._activeConnection = null;
+ this._activeConnectionChangedId = 0;
+
+ this._buildUI();
+ this._sync();
+ }
+
+ _buildUI() {
+ this.labelItem = new PopupMenu.PopupMenuItem('');
+ this.labelItem.connect('activate', this._toggle.bind(this));
+
+ this.radioItem = new PopupMenu.PopupMenuItem(this._connection.get_id(), false);
+ this.radioItem.connect('activate', this._activate.bind(this));
+ }
+
+ destroy() {
+ this.labelItem.destroy();
+ this.radioItem.destroy();
+ }
+
+ updateForConnection(connection) {
+ // connection should always be the same object
+ // (and object path) as this._connection, but
+ // this can be false if NetworkManager was restarted
+ // and picked up connections in a different order
+ // Just to be safe, we set it here again
+
+ this._connection = connection;
+ this.radioItem.label.text = connection.get_id();
+ this._sync();
+ this.emit('name-changed');
+ }
+
+ getName() {
+ return this._connection.get_id();
+ }
+
+ isActive() {
+ if (this._activeConnection == null)
+ return false;
+
+ return this._activeConnection.state <= NM.ActiveConnectionState.ACTIVATED;
+ }
+
+ _sync() {
+ let isActive = this.isActive();
+ this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel();
+ this.radioItem.setOrnament(isActive ? PopupMenu.Ornament.DOT : PopupMenu.Ornament.NONE);
+ this.emit('icon-changed');
+ }
+
+ _toggle() {
+ if (this._activeConnection == null)
+ this._section.activateConnection(this._connection);
+ else
+ this._section.deactivateConnection(this._activeConnection);
+
+ this._sync();
+ }
+
+ _activate() {
+ if (this._activeConnection == null)
+ this._section.activateConnection(this._connection);
+
+ this._sync();
+ }
+
+ _connectionStateChanged(_ac, _newstate, _reason) {
+ this._sync();
+ }
+
+ setActiveConnection(activeConnection) {
+ if (this._activeConnectionChangedId > 0) {
+ this._activeConnection.disconnect(this._activeConnectionChangedId);
+ this._activeConnectionChangedId = 0;
+ }
+
+ this._activeConnection = activeConnection;
+
+ if (this._activeConnection) {
+ this._activeConnectionChangedId = this._activeConnection.connect('notify::state',
+ this._connectionStateChanged.bind(this));
+ }
+
+ this._sync();
+ }
+};
+Signals.addSignalMethods(NMConnectionItem.prototype);
+
+var NMConnectionSection = class NMConnectionSection {
+ constructor(client) {
+ if (this.constructor === NMConnectionSection)
+ throw new TypeError('Cannot instantiate abstract type %s'.format(this.constructor.name));
+
+ this._client = client;
+
+ this._connectionItems = new Map();
+ this._connections = [];
+
+ this._labelSection = new PopupMenu.PopupMenuSection();
+ this._radioSection = new PopupMenu.PopupMenuSection();
+
+ this.item = new PopupMenu.PopupSubMenuMenuItem('', true);
+ this.item.menu.addMenuItem(this._labelSection);
+ this.item.menu.addMenuItem(this._radioSection);
+
+ this._notifyConnectivityId = this._client.connect('notify::connectivity', this._iconChanged.bind(this));
+ }
+
+ destroy() {
+ if (this._notifyConnectivityId != 0) {
+ this._client.disconnect(this._notifyConnectivityId);
+ this._notifyConnectivityId = 0;
+ }
+
+ this.item.destroy();
+ }
+
+ _iconChanged() {
+ this._sync();
+ this.emit('icon-changed');
+ }
+
+ _sync() {
+ let nItems = this._connectionItems.size;
+
+ this._radioSection.actor.visible = nItems > 1;
+ this._labelSection.actor.visible = nItems == 1;
+
+ this.item.label.text = this._getStatus();
+ this.item.icon.icon_name = this._getMenuIcon();
+ }
+
+ _getMenuIcon() {
+ return this.getIndicatorIcon();
+ }
+
+ getConnectLabel() {
+ return _("Connect");
+ }
+
+ _connectionValid(_connection) {
+ return true;
+ }
+
+ _connectionSortFunction(one, two) {
+ return GLib.utf8_collate(one.get_id(), two.get_id());
+ }
+
+ _makeConnectionItem(connection) {
+ return new NMConnectionItem(this, connection);
+ }
+
+ checkConnection(connection) {
+ if (!this._connectionValid(connection))
+ return;
+
+ // This function is called every time the connection is added or updated.
+ // In the usual case, we already added this connection and UUID
+ // didn't change. So we need to check if we already have an item,
+ // and update it for properties in the connection that changed
+ // (the only one we care about is the name)
+ // But it's also possible we didn't know about this connection
+ // (eg, during coldplug, or because it was updated and suddenly
+ // it's valid for this device), in which case we add a new item.
+
+ let item = this._connectionItems.get(connection.get_uuid());
+ if (item)
+ this._updateForConnection(item, connection);
+ else
+ this._addConnection(connection);
+ }
+
+ _updateForConnection(item, connection) {
+ let pos = this._connections.indexOf(connection);
+
+ this._connections.splice(pos, 1);
+ pos = Util.insertSorted(this._connections, connection, this._connectionSortFunction.bind(this));
+ this._labelSection.moveMenuItem(item.labelItem, pos);
+ this._radioSection.moveMenuItem(item.radioItem, pos);
+
+ item.updateForConnection(connection);
+ }
+
+ _addConnection(connection) {
+ let item = this._makeConnectionItem(connection);
+ if (!item)
+ return;
+
+ item.connect('icon-changed', () => this._iconChanged());
+ item.connect('activation-failed', (o, reason) => {
+ this.emit('activation-failed', reason);
+ });
+ item.connect('name-changed', this._sync.bind(this));
+
+ let pos = Util.insertSorted(this._connections, connection, this._connectionSortFunction.bind(this));
+ this._labelSection.addMenuItem(item.labelItem, pos);
+ this._radioSection.addMenuItem(item.radioItem, pos);
+ this._connectionItems.set(connection.get_uuid(), item);
+ this._sync();
+ }
+
+ removeConnection(connection) {
+ let uuid = connection.get_uuid();
+ let item = this._connectionItems.get(uuid);
+ if (item == undefined)
+ return;
+
+ item.destroy();
+ this._connectionItems.delete(uuid);
+
+ let pos = this._connections.indexOf(connection);
+ this._connections.splice(pos, 1);
+
+ this._sync();
+ }
+};
+Signals.addSignalMethods(NMConnectionSection.prototype);
+
+var NMConnectionDevice = class NMConnectionDevice extends NMConnectionSection {
+ constructor(client, device) {
+ super(client);
+
+ if (this.constructor === NMConnectionDevice)
+ throw new TypeError('Cannot instantiate abstract type %s'.format(this.constructor.name));
+
+ this._device = device;
+ this._description = '';
+
+ this._autoConnectItem = this.item.menu.addAction(_("Connect"), this._autoConnect.bind(this));
+ this._deactivateItem = this._radioSection.addAction(_("Turn Off"), this.deactivateConnection.bind(this));
+
+ this._stateChangedId = this._device.connect('state-changed', this._deviceStateChanged.bind(this));
+ this._activeConnectionChangedId = this._device.connect('notify::active-connection', this._activeConnectionChanged.bind(this));
+ }
+
+ _canReachInternet() {
+ if (this._client.primary_connection != this._device.active_connection)
+ return true;
+
+ return this._client.connectivity == NM.ConnectivityState.FULL;
+ }
+
+ _autoConnect() {
+ let connection = new NM.SimpleConnection();
+ this._client.add_and_activate_connection_async(connection, this._device, null, null, null);
+ }
+
+ destroy() {
+ if (this._stateChangedId) {
+ GObject.signal_handler_disconnect(this._device, this._stateChangedId);
+ this._stateChangedId = 0;
+ }
+ if (this._activeConnectionChangedId) {
+ GObject.signal_handler_disconnect(this._device, this._activeConnectionChangedId);
+ this._activeConnectionChangedId = 0;
+ }
+
+ super.destroy();
+ }
+
+ _activeConnectionChanged() {
+ if (this._activeConnection) {
+ let item = this._connectionItems.get(this._activeConnection.connection.get_uuid());
+ item.setActiveConnection(null);
+ this._activeConnection = null;
+ }
+
+ this._sync();
+ }
+
+ _deviceStateChanged(device, newstate, oldstate, reason) {
+ if (newstate == oldstate) {
+ log('device emitted state-changed without actually changing state');
+ return;
+ }
+
+ /* Emit a notification if activation fails, but don't do it
+ if the reason is no secrets, as that indicates the user
+ cancelled the agent dialog */
+ if (newstate == NM.DeviceState.FAILED &&
+ reason != NM.DeviceStateReason.NO_SECRETS)
+ this.emit('activation-failed', reason);
+
+ this._sync();
+ }
+
+ _connectionValid(connection) {
+ return this._device.connection_valid(connection);
+ }
+
+ activateConnection(connection) {
+ this._client.activate_connection_async(connection, this._device, null, null, null);
+ }
+
+ deactivateConnection(_activeConnection) {
+ this._device.disconnect(null);
+ }
+
+ setDeviceDescription(desc) {
+ this._description = desc;
+ this._sync();
+ }
+
+ _getDescription() {
+ return this._description;
+ }
+
+ _sync() {
+ let nItems = this._connectionItems.size;
+ this._autoConnectItem.visible = nItems == 0;
+ this._deactivateItem.visible = this._device.state > NM.DeviceState.DISCONNECTED;
+
+ if (this._activeConnection == null) {
+ let activeConnection = this._device.active_connection;
+ if (activeConnection && activeConnection.connection) {
+ let item = this._connectionItems.get(activeConnection.connection.get_uuid());
+ if (item) {
+ this._activeConnection = activeConnection;
+ ensureActiveConnectionProps(this._activeConnection);
+ item.setActiveConnection(this._activeConnection);
+ }
+ }
+ }
+
+ super._sync();
+ }
+
+ _getStatus() {
+ if (!this._device)
+ return '';
+
+ switch (this._device.state) {
+ case NM.DeviceState.DISCONNECTED:
+ /* Translators: %s is a network identifier */
+ return _("%s Off").format(this._getDescription());
+ case NM.DeviceState.ACTIVATED:
+ /* Translators: %s is a network identifier */
+ return _("%s Connected").format(this._getDescription());
+ case NM.DeviceState.UNMANAGED:
+ /* Translators: this is for network devices that are physically present but are not
+ under NetworkManager's control (and thus cannot be used in the menu);
+ %s is a network identifier */
+ return _("%s Unmanaged").format(this._getDescription());
+ case NM.DeviceState.DEACTIVATING:
+ /* Translators: %s is a network identifier */
+ return _("%s Disconnecting").format(this._getDescription());
+ case NM.DeviceState.PREPARE:
+ case NM.DeviceState.CONFIG:
+ case NM.DeviceState.IP_CONFIG:
+ case NM.DeviceState.IP_CHECK:
+ case NM.DeviceState.SECONDARIES:
+ /* Translators: %s is a network identifier */
+ return _("%s Connecting").format(this._getDescription());
+ case NM.DeviceState.NEED_AUTH:
+ /* Translators: this is for network connections that require some kind of key or password; %s is a network identifier */
+ return _("%s Requires Authentication").format(this._getDescription());
+ case NM.DeviceState.UNAVAILABLE:
+ // This state is actually a compound of various states (generically unavailable,
+ // firmware missing), that are exposed by different properties (whose state may
+ // or may not updated when we receive state-changed).
+ if (this._device.firmware_missing) {
+ /* Translators: this is for devices that require some kind of firmware or kernel
+ module, which is missing; %s is a network identifier */
+ return _("Firmware Missing For %s").format(this._getDescription());
+ }
+ /* Translators: this is for a network device that cannot be activated (for example it
+ is disabled by rfkill, or it has no coverage; %s is a network identifier */
+ return _("%s Unavailable").format(this._getDescription());
+ case NM.DeviceState.FAILED:
+ /* Translators: %s is a network identifier */
+ return _("%s Connection Failed").format(this._getDescription());
+ default:
+ log('Device state invalid, is %d'.format(this._device.state));
+ return 'invalid';
+ }
+ }
+};
+
+var NMDeviceWired = class extends NMConnectionDevice {
+ constructor(client, device) {
+ super(client, device);
+
+ this.item.menu.addSettingsAction(_("Wired Settings"), 'gnome-network-panel.desktop');
+ }
+
+ get category() {
+ return NMConnectionCategory.WIRED;
+ }
+
+ _hasCarrier() {
+ if (this._device instanceof NM.DeviceEthernet)
+ return this._device.carrier;
+ else
+ return true;
+ }
+
+ _sync() {
+ this.item.visible = this._hasCarrier();
+ super._sync();
+ }
+
+ getIndicatorIcon() {
+ if (this._device.active_connection) {
+ let state = this._device.active_connection.state;
+
+ if (state == NM.ActiveConnectionState.ACTIVATING) {
+ return 'network-wired-acquiring-symbolic';
+ } else if (state == NM.ActiveConnectionState.ACTIVATED) {
+ if (this._canReachInternet())
+ return 'network-wired-symbolic';
+ else
+ return 'network-wired-no-route-symbolic';
+ } else {
+ return 'network-wired-disconnected-symbolic';
+ }
+ } else {
+ return 'network-wired-disconnected-symbolic';
+ }
+ }
+};
+
+var NMDeviceModem = class extends NMConnectionDevice {
+ constructor(client, device) {
+ super(client, device);
+
+ this.item.menu.addSettingsAction(_("Mobile Broadband Settings"), 'gnome-network-panel.desktop');
+
+ this._mobileDevice = null;
+
+ let capabilities = device.current_capabilities;
+ if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0)
+ this._mobileDevice = new ModemManager.BroadbandModem(device.udi, capabilities);
+ else if (capabilities & NM.DeviceModemCapabilities.GSM_UMTS)
+ this._mobileDevice = new ModemManager.ModemGsm(device.udi);
+ else if (capabilities & NM.DeviceModemCapabilities.CDMA_EVDO)
+ this._mobileDevice = new ModemManager.ModemCdma(device.udi);
+ else if (capabilities & NM.DeviceModemCapabilities.LTE)
+ this._mobileDevice = new ModemManager.ModemGsm(device.udi);
+
+ if (this._mobileDevice) {
+ this._operatorNameId = this._mobileDevice.connect('notify::operator-name', this._sync.bind(this));
+ this._signalQualityId = this._mobileDevice.connect('notify::signal-quality', () => {
+ this._iconChanged();
+ });
+ }
+ }
+
+ get category() {
+ return NMConnectionCategory.WWAN;
+ }
+
+ _autoConnect() {
+ launchSettingsPanel('network', 'connect-3g', this._device.get_path());
+ }
+
+ destroy() {
+ if (this._operatorNameId) {
+ this._mobileDevice.disconnect(this._operatorNameId);
+ this._operatorNameId = 0;
+ }
+ if (this._signalQualityId) {
+ this._mobileDevice.disconnect(this._signalQualityId);
+ this._signalQualityId = 0;
+ }
+
+ super.destroy();
+ }
+
+ _getStatus() {
+ if (!this._client.wwan_hardware_enabled)
+ /* Translators: %s is a network identifier */
+ return _("%s Hardware Disabled").format(this._getDescription());
+ else if (!this._client.wwan_enabled)
+ /* Translators: this is for a network device that cannot be activated
+ because it's disabled by rfkill (airplane mode); %s is a network identifier */
+ return _("%s Disabled").format(this._getDescription());
+ else if (this._device.state == NM.DeviceState.ACTIVATED &&
+ this._mobileDevice && this._mobileDevice.operator_name)
+ return this._mobileDevice.operator_name;
+ else
+ return super._getStatus();
+ }
+
+ getIndicatorIcon() {
+ if (this._device.active_connection) {
+ if (this._device.active_connection.state == NM.ActiveConnectionState.ACTIVATING)
+ return 'network-cellular-acquiring-symbolic';
+
+ return this._getSignalIcon();
+ } else {
+ return 'network-cellular-signal-none-symbolic';
+ }
+ }
+
+ _getSignalIcon() {
+ return 'network-cellular-signal-%s-symbolic'.format(
+ signalToIcon(this._mobileDevice.signal_quality));
+ }
+};
+
+var NMDeviceBluetooth = class extends NMConnectionDevice {
+ constructor(client, device) {
+ super(client, device);
+
+ this.item.menu.addSettingsAction(_("Bluetooth Settings"), 'gnome-network-panel.desktop');
+ }
+
+ get category() {
+ return NMConnectionCategory.WWAN;
+ }
+
+ _getDescription() {
+ return this._device.name;
+ }
+
+ getConnectLabel() {
+ return _("Connect to Internet");
+ }
+
+ getIndicatorIcon() {
+ if (this._device.active_connection) {
+ let state = this._device.active_connection.state;
+ if (state == NM.ActiveConnectionState.ACTIVATING)
+ return 'network-cellular-acquiring-symbolic';
+ else if (state == NM.ActiveConnectionState.ACTIVATED)
+ return 'network-cellular-connected-symbolic';
+ else
+ return 'network-cellular-signal-none-symbolic';
+ } else {
+ return 'network-cellular-signal-none-symbolic';
+ }
+ }
+};
+
+var NMWirelessDialogItem = GObject.registerClass({
+ Signals: {
+ 'selected': {},
+ },
+}, class NMWirelessDialogItem extends St.BoxLayout {
+ _init(network) {
+ this._network = network;
+ this._ap = network.accessPoints[0];
+
+ super._init({ style_class: 'nm-dialog-item',
+ can_focus: true,
+ reactive: true });
+
+ let action = new Clutter.ClickAction();
+ action.connect('clicked', () => this.grab_key_focus());
+ this.add_action(action);
+
+ let title = ssidToLabel(this._ap.get_ssid());
+ this._label = new St.Label({
+ text: title,
+ x_expand: true,
+ });
+
+ this.label_actor = this._label;
+ this.add_child(this._label);
+
+ this._selectedIcon = new St.Icon({ style_class: 'nm-dialog-icon',
+ icon_name: 'object-select-symbolic' });
+ this.add(this._selectedIcon);
+
+ this._icons = new St.BoxLayout({
+ style_class: 'nm-dialog-icons',
+ x_align: Clutter.ActorAlign.END,
+ });
+ this.add_child(this._icons);
+
+ this._secureIcon = new St.Icon({ style_class: 'nm-dialog-icon' });
+ if (this._ap._secType != NMAccessPointSecurity.NONE)
+ this._secureIcon.icon_name = 'network-wireless-encrypted-symbolic';
+ this._icons.add_actor(this._secureIcon);
+
+ this._signalIcon = new St.Icon({ style_class: 'nm-dialog-icon' });
+ this._icons.add_actor(this._signalIcon);
+
+ this._sync();
+ }
+
+ vfunc_key_focus_in() {
+ this.emit('selected');
+ }
+
+ _sync() {
+ this._signalIcon.icon_name = this._getSignalIcon();
+ }
+
+ updateBestAP(ap) {
+ this._ap = ap;
+ this._sync();
+ }
+
+ setActive(isActive) {
+ this._selectedIcon.opacity = isActive ? 255 : 0;
+ }
+
+ _getSignalIcon() {
+ if (this._ap.mode == NM80211Mode.ADHOC) {
+ return 'network-workgroup-symbolic';
+ } else {
+ return 'network-wireless-signal-%s-symbolic'.format(
+ signalToIcon(this._ap.strength));
+ }
+ }
+});
+
+var NMWirelessDialog = GObject.registerClass(
+class NMWirelessDialog extends ModalDialog.ModalDialog {
+ _init(client, device) {
+ super._init({ styleClass: 'nm-dialog' });
+
+ this._client = client;
+ this._device = device;
+
+ this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled',
+ this._syncView.bind(this));
+
+ this._rfkill = Rfkill.getRfkillManager();
+ this._airplaneModeChangedId = this._rfkill.connect('airplane-mode-changed',
+ this._syncView.bind(this));
+
+ this._networks = [];
+ this._buildLayout();
+
+ let connections = client.get_connections();
+ this._connections = connections.filter(
+ connection => device.connection_valid(connection));
+
+ this._apAddedId = device.connect('access-point-added', this._accessPointAdded.bind(this));
+ this._apRemovedId = device.connect('access-point-removed', this._accessPointRemoved.bind(this));
+ this._activeApChangedId = device.connect('notify::active-access-point', this._activeApChanged.bind(this));
+
+ // accessPointAdded will also create dialog items
+ let accessPoints = device.get_access_points() || [];
+ accessPoints.forEach(ap => {
+ this._accessPointAdded(this._device, ap);
+ });
+
+ this._selectedNetwork = null;
+ this._activeApChanged();
+ this._updateSensitivity();
+ this._syncView();
+
+ this._scanTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 15, this._onScanTimeout.bind(this));
+ GLib.Source.set_name_by_id(this._scanTimeoutId, '[gnome-shell] this._onScanTimeout');
+ this._onScanTimeout();
+
+ let id = Main.sessionMode.connect('updated', () => {
+ if (Main.sessionMode.allowSettings)
+ return;
+
+ Main.sessionMode.disconnect(id);
+ this.close();
+ });
+
+ this.connect('destroy', this._onDestroy.bind(this));
+ }
+
+ _onDestroy() {
+ if (this._apAddedId) {
+ GObject.Object.prototype.disconnect.call(this._device, this._apAddedId);
+ this._apAddedId = 0;
+ }
+ if (this._apRemovedId) {
+ GObject.Object.prototype.disconnect.call(this._device, this._apRemovedId);
+ this._apRemovedId = 0;
+ }
+ if (this._activeApChangedId) {
+ GObject.Object.prototype.disconnect.call(this._device, this._activeApChangedId);
+ this._activeApChangedId = 0;
+ }
+ if (this._wirelessEnabledChangedId) {
+ this._client.disconnect(this._wirelessEnabledChangedId);
+ this._wirelessEnabledChangedId = 0;
+ }
+ if (this._airplaneModeChangedId) {
+ this._rfkill.disconnect(this._airplaneModeChangedId);
+ this._airplaneModeChangedId = 0;
+ }
+
+ if (this._scanTimeoutId) {
+ GLib.source_remove(this._scanTimeoutId);
+ this._scanTimeoutId = 0;
+ }
+ }
+
+ _onScanTimeout() {
+ this._device.request_scan_async(null, null);
+ return GLib.SOURCE_CONTINUE;
+ }
+
+ _activeApChanged() {
+ if (this._activeNetwork)
+ this._activeNetwork.item.setActive(false);
+
+ this._activeNetwork = null;
+ if (this._device.active_access_point) {
+ let idx = this._findNetwork(this._device.active_access_point);
+ if (idx >= 0)
+ this._activeNetwork = this._networks[idx];
+ }
+
+ if (this._activeNetwork)
+ this._activeNetwork.item.setActive(true);
+ this._updateSensitivity();
+ }
+
+ _updateSensitivity() {
+ let connectSensitive = this._client.wireless_enabled && this._selectedNetwork && (this._selectedNetwork != this._activeNetwork);
+ this._connectButton.reactive = connectSensitive;
+ this._connectButton.can_focus = connectSensitive;
+ }
+
+ _syncView() {
+ if (this._rfkill.airplaneMode) {
+ this._airplaneBox.show();
+
+ this._airplaneIcon.icon_name = 'airplane-mode-symbolic';
+ this._airplaneHeadline.text = _("Airplane Mode is On");
+ this._airplaneText.text = _("Wi-Fi is disabled when airplane mode is on.");
+ this._airplaneButton.label = _("Turn Off Airplane Mode");
+
+ this._airplaneButton.visible = !this._rfkill.hwAirplaneMode;
+ this._airplaneInactive.visible = this._rfkill.hwAirplaneMode;
+ this._noNetworksBox.hide();
+ } else if (!this._client.wireless_enabled) {
+ this._airplaneBox.show();
+
+ this._airplaneIcon.icon_name = 'dialog-information-symbolic';
+ this._airplaneHeadline.text = _("Wi-Fi is Off");
+ this._airplaneText.text = _("Wi-Fi needs to be turned on in order to connect to a network.");
+ this._airplaneButton.label = _("Turn On Wi-Fi");
+
+ this._airplaneButton.show();
+ this._airplaneInactive.hide();
+ this._noNetworksBox.hide();
+ } else {
+ this._airplaneBox.hide();
+
+ this._noNetworksBox.visible = this._networks.length == 0;
+ }
+
+ if (this._noNetworksBox.visible)
+ this._noNetworksSpinner.play();
+ else
+ this._noNetworksSpinner.stop();
+ }
+
+ _buildLayout() {
+ let headline = new St.BoxLayout({ style_class: 'nm-dialog-header-hbox' });
+
+ let icon = new St.Icon({ style_class: 'nm-dialog-header-icon',
+ icon_name: 'network-wireless-signal-excellent-symbolic' });
+
+ let titleBox = new St.BoxLayout({ vertical: true });
+ let title = new St.Label({ style_class: 'nm-dialog-header',
+ text: _("Wi-Fi Networks") });
+ let subtitle = new St.Label({ style_class: 'nm-dialog-subheader',
+ text: _("Select a network") });
+ titleBox.add(title);
+ titleBox.add(subtitle);
+
+ headline.add(icon);
+ headline.add(titleBox);
+
+ this.contentLayout.style_class = 'nm-dialog-content';
+ this.contentLayout.add(headline);
+
+ this._stack = new St.Widget({
+ layout_manager: new Clutter.BinLayout(),
+ y_expand: true,
+ });
+
+ this._itemBox = new St.BoxLayout({ vertical: true });
+ this._scrollView = new St.ScrollView({ style_class: 'nm-dialog-scroll-view' });
+ this._scrollView.set_x_expand(true);
+ this._scrollView.set_y_expand(true);
+ this._scrollView.set_policy(St.PolicyType.NEVER,
+ St.PolicyType.AUTOMATIC);
+ this._scrollView.add_actor(this._itemBox);
+ this._stack.add_child(this._scrollView);
+
+ this._noNetworksBox = new St.BoxLayout({ vertical: true,
+ style_class: 'no-networks-box',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER });
+
+ this._noNetworksSpinner = new Animation.Spinner(16);
+ this._noNetworksBox.add_actor(this._noNetworksSpinner);
+ this._noNetworksBox.add_actor(new St.Label({ style_class: 'no-networks-label',
+ text: _("No Networks") }));
+ this._stack.add_child(this._noNetworksBox);
+
+ this._airplaneBox = new St.BoxLayout({ vertical: true,
+ style_class: 'nm-dialog-airplane-box',
+ x_align: Clutter.ActorAlign.CENTER,
+ y_align: Clutter.ActorAlign.CENTER });
+ this._airplaneIcon = new St.Icon({ icon_size: 48 });
+ this._airplaneHeadline = new St.Label({ style_class: 'nm-dialog-airplane-headline headline' });
+ this._airplaneText = new St.Label({ style_class: 'nm-dialog-airplane-text' });
+
+ let airplaneSubStack = new St.Widget({ layout_manager: new Clutter.BinLayout() });
+ this._airplaneButton = new St.Button({ style_class: 'modal-dialog-button button' });
+ this._airplaneButton.connect('clicked', () => {
+ if (this._rfkill.airplaneMode)
+ this._rfkill.airplaneMode = false;
+ else
+ this._client.wireless_enabled = true;
+ });
+ airplaneSubStack.add_actor(this._airplaneButton);
+ this._airplaneInactive = new St.Label({ style_class: 'nm-dialog-airplane-text',
+ text: _("Use hardware switch to turn off") });
+ airplaneSubStack.add_actor(this._airplaneInactive);
+
+ this._airplaneBox.add_child(this._airplaneIcon);
+ this._airplaneBox.add_child(this._airplaneHeadline);
+ this._airplaneBox.add_child(this._airplaneText);
+ this._airplaneBox.add_child(airplaneSubStack);
+ this._stack.add_child(this._airplaneBox);
+
+ this.contentLayout.add_child(this._stack);
+
+ this._disconnectButton = this.addButton({ action: () => this.close(),
+ label: _("Cancel"),
+ key: Clutter.KEY_Escape });
+ this._connectButton = this.addButton({ action: this._connect.bind(this),
+ label: _("Connect"),
+ key: Clutter.KEY_Return });
+ }
+
+ _connect() {
+ let network = this._selectedNetwork;
+ if (network.connections.length > 0) {
+ let connection = network.connections[0];
+ this._client.activate_connection_async(connection, this._device, null, null, null);
+ } else {
+ let accessPoints = network.accessPoints;
+ if ((accessPoints[0]._secType == NMAccessPointSecurity.WPA2_ENT) ||
+ (accessPoints[0]._secType == NMAccessPointSecurity.WPA_ENT)) {
+ // 802.1x-enabled APs require further configuration, so they're
+ // handled in gnome-control-center
+ launchSettingsPanel('wifi', 'connect-8021x-wifi',
+ this._device.get_path(), accessPoints[0].get_path());
+ } else {
+ let connection = new NM.SimpleConnection();
+ this._client.add_and_activate_connection_async(connection, this._device, accessPoints[0].get_path(), null, null);
+ }
+ }
+
+ this.close();
+ }
+
+ _notifySsidCb(accessPoint) {
+ if (accessPoint.get_ssid() != null) {
+ accessPoint.disconnect(accessPoint._notifySsidId);
+ accessPoint._notifySsidId = 0;
+ this._accessPointAdded(this._device, accessPoint);
+ }
+ }
+
+ _getApSecurityType(accessPoint) {
+ if (accessPoint._secType)
+ return accessPoint._secType;
+
+ let flags = accessPoint.flags;
+ let wpaFlags = accessPoint.wpa_flags;
+ let rsnFlags = accessPoint.rsn_flags;
+ let type;
+ if (rsnFlags != NM80211ApSecurityFlags.NONE) {
+ /* RSN check first so that WPA+WPA2 APs are treated as RSN/WPA2 */
+ if (rsnFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
+ type = NMAccessPointSecurity.WPA2_ENT;
+ else if (rsnFlags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
+ type = NMAccessPointSecurity.WPA2_PSK;
+ } else if (wpaFlags != NM80211ApSecurityFlags.NONE) {
+ if (wpaFlags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
+ type = NMAccessPointSecurity.WPA_ENT;
+ else if (wpaFlags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
+ type = NMAccessPointSecurity.WPA_PSK;
+ } else {
+ // eslint-disable-next-line no-lonely-if
+ if (flags & NM80211ApFlags.PRIVACY)
+ type = NMAccessPointSecurity.WEP;
+ else
+ type = NMAccessPointSecurity.NONE;
+ }
+
+ // cache the found value to avoid checking flags all the time
+ accessPoint._secType = type;
+ return type;
+ }
+
+ _networkSortFunction(one, two) {
+ let oneHasConnection = one.connections.length != 0;
+ let twoHasConnection = two.connections.length != 0;
+
+ // place known connections first
+ // (-1 = good order, 1 = wrong order)
+ if (oneHasConnection && !twoHasConnection)
+ return -1;
+ else if (!oneHasConnection && twoHasConnection)
+ return 1;
+
+ let oneAp = one.accessPoints[0] || null;
+ let twoAp = two.accessPoints[0] || null;
+
+ if (oneAp != null && twoAp == null)
+ return -1;
+ else if (oneAp == null && twoAp != null)
+ return 1;
+
+ let oneStrength = oneAp.strength;
+ let twoStrength = twoAp.strength;
+
+ // place stronger connections first
+ if (oneStrength != twoStrength)
+ return oneStrength < twoStrength ? 1 : -1;
+
+ let oneHasSecurity = one.security != NMAccessPointSecurity.NONE;
+ let twoHasSecurity = two.security != NMAccessPointSecurity.NONE;
+
+ // place secure connections first
+ // (we treat WEP/WPA/WPA2 the same as there is no way to
+ // take them apart from the UI)
+ if (oneHasSecurity && !twoHasSecurity)
+ return -1;
+ else if (!oneHasSecurity && twoHasSecurity)
+ return 1;
+
+ // sort alphabetically
+ return GLib.utf8_collate(one.ssidText, two.ssidText);
+ }
+
+ _networkCompare(network, accessPoint) {
+ if (!network.ssid.equal(accessPoint.get_ssid()))
+ return false;
+ if (network.mode != accessPoint.mode)
+ return false;
+ if (network.security != this._getApSecurityType(accessPoint))
+ return false;
+
+ return true;
+ }
+
+ _findExistingNetwork(accessPoint) {
+ for (let i = 0; i < this._networks.length; i++) {
+ let network = this._networks[i];
+ for (let j = 0; j < network.accessPoints.length; j++) {
+ if (network.accessPoints[j] == accessPoint)
+ return { network: i, ap: j };
+ }
+ }
+
+ return null;
+ }
+
+ _findNetwork(accessPoint) {
+ if (accessPoint.get_ssid() == null)
+ return -1;
+
+ for (let i = 0; i < this._networks.length; i++) {
+ if (this._networkCompare(this._networks[i], accessPoint))
+ return i;
+ }
+ return -1;
+ }
+
+ _checkConnections(network, accessPoint) {
+ this._connections.forEach(connection => {
+ if (accessPoint.connection_valid(connection) &&
+ !network.connections.includes(connection))
+ network.connections.push(connection);
+ });
+ }
+
+ _accessPointAdded(device, accessPoint) {
+ if (accessPoint.get_ssid() == null) {
+ // This access point is not visible yet
+ // Wait for it to get a ssid
+ accessPoint._notifySsidId = accessPoint.connect('notify::ssid', this._notifySsidCb.bind(this));
+ return;
+ }
+
+ let pos = this._findNetwork(accessPoint);
+ let network;
+
+ if (pos != -1) {
+ network = this._networks[pos];
+ if (network.accessPoints.includes(accessPoint)) {
+ log('Access point was already seen, not adding again');
+ return;
+ }
+
+ Util.insertSorted(network.accessPoints, accessPoint, (one, two) => {
+ return two.strength - one.strength;
+ });
+ network.item.updateBestAP(network.accessPoints[0]);
+ this._checkConnections(network, accessPoint);
+
+ this._resortItems();
+ } else {
+ network = {
+ ssid: accessPoint.get_ssid(),
+ mode: accessPoint.mode,
+ security: this._getApSecurityType(accessPoint),
+ connections: [],
+ item: null,
+ accessPoints: [accessPoint],
+ };
+ network.ssidText = ssidToLabel(network.ssid);
+ this._checkConnections(network, accessPoint);
+
+ let newPos = Util.insertSorted(this._networks, network, this._networkSortFunction);
+ this._createNetworkItem(network);
+ this._itemBox.insert_child_at_index(network.item, newPos);
+ }
+
+ this._syncView();
+ }
+
+ _accessPointRemoved(device, accessPoint) {
+ let res = this._findExistingNetwork(accessPoint);
+
+ if (res == null) {
+ log('Removing an access point that was never added');
+ return;
+ }
+
+ let network = this._networks[res.network];
+ network.accessPoints.splice(res.ap, 1);
+
+ if (network.accessPoints.length == 0) {
+ network.item.destroy();
+ this._networks.splice(res.network, 1);
+ } else {
+ network.item.updateBestAP(network.accessPoints[0]);
+ this._resortItems();
+ }
+
+ this._syncView();
+ }
+
+ _resortItems() {
+ let adjustment = this._scrollView.vscroll.adjustment;
+ let scrollValue = adjustment.value;
+
+ this._itemBox.remove_all_children();
+ this._networks.forEach(network => {
+ this._itemBox.add_child(network.item);
+ });
+
+ adjustment.value = scrollValue;
+ }
+
+ _selectNetwork(network) {
+ if (this._selectedNetwork)
+ this._selectedNetwork.item.remove_style_pseudo_class('selected');
+
+ this._selectedNetwork = network;
+ this._updateSensitivity();
+
+ if (this._selectedNetwork)
+ this._selectedNetwork.item.add_style_pseudo_class('selected');
+ }
+
+ _createNetworkItem(network) {
+ network.item = new NMWirelessDialogItem(network);
+ network.item.setActive(network == this._selectedNetwork);
+ network.item.connect('selected', () => {
+ Util.ensureActorVisibleInScrollView(this._scrollView, network.item);
+ this._selectNetwork(network);
+ });
+ network.item.connect('destroy', () => {
+ let keyFocus = global.stage.key_focus;
+ if (keyFocus && keyFocus.contains(network.item))
+ this._itemBox.grab_key_focus();
+ });
+ }
+});
+
+var NMDeviceWireless = class {
+ constructor(client, device) {
+ this._client = client;
+ this._device = device;
+
+ this._description = '';
+
+ this.item = new PopupMenu.PopupSubMenuMenuItem('', true);
+ this.item.menu.addAction(_("Select Network"), this._showDialog.bind(this));
+
+ this._toggleItem = new PopupMenu.PopupMenuItem('');
+ this._toggleItem.connect('activate', this._toggleWifi.bind(this));
+ this.item.menu.addMenuItem(this._toggleItem);
+
+ this.item.menu.addSettingsAction(_("Wi-Fi Settings"), 'gnome-wifi-panel.desktop');
+
+ this._wirelessEnabledChangedId = this._client.connect('notify::wireless-enabled', this._sync.bind(this));
+ this._wirelessHwEnabledChangedId = this._client.connect('notify::wireless-hardware-enabled', this._sync.bind(this));
+ this._activeApChangedId = this._device.connect('notify::active-access-point', this._activeApChanged.bind(this));
+ this._stateChangedId = this._device.connect('state-changed', this._deviceStateChanged.bind(this));
+ this._notifyConnectivityId = this._client.connect('notify::connectivity', this._iconChanged.bind(this));
+
+ this._sync();
+ }
+
+ get category() {
+ return NMConnectionCategory.WIRELESS;
+ }
+
+ _iconChanged() {
+ this._sync();
+ this.emit('icon-changed');
+ }
+
+ destroy() {
+ if (this._activeApChangedId) {
+ GObject.signal_handler_disconnect(this._device, this._activeApChangedId);
+ this._activeApChangedId = 0;
+ }
+ if (this._stateChangedId) {
+ GObject.signal_handler_disconnect(this._device, this._stateChangedId);
+ this._stateChangedId = 0;
+ }
+ if (this._strengthChangedId > 0) {
+ this._activeAccessPoint.disconnect(this._strengthChangedId);
+ this._strengthChangedId = 0;
+ }
+ if (this._wirelessEnabledChangedId) {
+ this._client.disconnect(this._wirelessEnabledChangedId);
+ this._wirelessEnabledChangedId = 0;
+ }
+ if (this._wirelessHwEnabledChangedId) {
+ this._client.disconnect(this._wirelessHwEnabledChangedId);
+ this._wirelessHwEnabledChangedId = 0;
+ }
+ if (this._dialog) {
+ this._dialog.destroy();
+ this._dialog = null;
+ }
+ if (this._notifyConnectivityId) {
+ this._client.disconnect(this._notifyConnectivityId);
+ this._notifyConnectivityId = 0;
+ }
+
+ this.item.destroy();
+ }
+
+ _deviceStateChanged(device, newstate, oldstate, reason) {
+ if (newstate == oldstate) {
+ log('device emitted state-changed without actually changing state');
+ return;
+ }
+
+ /* Emit a notification if activation fails, but don't do it
+ if the reason is no secrets, as that indicates the user
+ cancelled the agent dialog */
+ if (newstate == NM.DeviceState.FAILED &&
+ reason != NM.DeviceStateReason.NO_SECRETS)
+ this.emit('activation-failed', reason);
+
+ this._sync();
+ }
+
+ _toggleWifi() {
+ this._client.wireless_enabled = !this._client.wireless_enabled;
+ }
+
+ _showDialog() {
+ this._dialog = new NMWirelessDialog(this._client, this._device);
+ this._dialog.connect('closed', this._dialogClosed.bind(this));
+ this._dialog.open();
+ }
+
+ _dialogClosed() {
+ this._dialog = null;
+ }
+
+ _strengthChanged() {
+ this._iconChanged();
+ }
+
+ _activeApChanged() {
+ if (this._activeAccessPoint) {
+ this._activeAccessPoint.disconnect(this._strengthChangedId);
+ this._strengthChangedId = 0;
+ }
+
+ this._activeAccessPoint = this._device.active_access_point;
+
+ if (this._activeAccessPoint) {
+ this._strengthChangedId = this._activeAccessPoint.connect('notify::strength',
+ this._strengthChanged.bind(this));
+ }
+
+ this._sync();
+ }
+
+ _sync() {
+ this._toggleItem.label.text = this._client.wireless_enabled ? _("Turn Off") : _("Turn On");
+ this._toggleItem.visible = this._client.wireless_hardware_enabled;
+
+ this.item.icon.icon_name = this._getMenuIcon();
+ this.item.label.text = this._getStatus();
+ }
+
+ setDeviceDescription(desc) {
+ this._description = desc;
+ this._sync();
+ }
+
+ _getStatus() {
+ let ap = this._device.active_access_point;
+
+ if (this._isHotSpotMaster())
+ /* Translators: %s is a network identifier */
+ return _("%s Hotspot Active").format(this._description);
+ else if (this._device.state >= NM.DeviceState.PREPARE &&
+ this._device.state < NM.DeviceState.ACTIVATED)
+ /* Translators: %s is a network identifier */
+ return _("%s Connecting").format(this._description);
+ else if (ap)
+ return ssidToLabel(ap.get_ssid());
+ else if (!this._client.wireless_hardware_enabled)
+ /* Translators: %s is a network identifier */
+ return _("%s Hardware Disabled").format(this._description);
+ else if (!this._client.wireless_enabled)
+ /* Translators: %s is a network identifier */
+ return _("%s Off").format(this._description);
+ else if (this._device.state == NM.DeviceState.DISCONNECTED)
+ /* Translators: %s is a network identifier */
+ return _("%s Not Connected").format(this._description);
+ else
+ return '';
+ }
+
+ _getMenuIcon() {
+ if (this._device.active_connection)
+ return this.getIndicatorIcon();
+ else
+ return 'network-wireless-signal-none-symbolic';
+ }
+
+ _canReachInternet() {
+ if (this._client.primary_connection != this._device.active_connection)
+ return true;
+
+ return this._client.connectivity == NM.ConnectivityState.FULL;
+ }
+
+ _isHotSpotMaster() {
+ if (!this._device.active_connection)
+ return false;
+
+ let connection = this._device.active_connection.connection;
+ if (!connection)
+ return false;
+
+ let ip4config = connection.get_setting_ip4_config();
+ if (!ip4config)
+ return false;
+
+ return ip4config.get_method() == NM.SETTING_IP4_CONFIG_METHOD_SHARED;
+ }
+
+ getIndicatorIcon() {
+ if (this._device.state < NM.DeviceState.PREPARE)
+ return 'network-wireless-disconnected-symbolic';
+ if (this._device.state < NM.DeviceState.ACTIVATED)
+ return 'network-wireless-acquiring-symbolic';
+
+ if (this._isHotSpotMaster())
+ return 'network-wireless-hotspot-symbolic';
+
+ let ap = this._device.active_access_point;
+ if (!ap) {
+ if (this._device.mode != NM80211Mode.ADHOC)
+ log('An active wireless connection, in infrastructure mode, involves no access point?');
+
+ if (this._canReachInternet())
+ return 'network-wireless-connected-symbolic';
+ else
+ return 'network-wireless-no-route-symbolic';
+ }
+
+ if (this._canReachInternet())
+ return 'network-wireless-signal-%s-symbolic'.format(signalToIcon(ap.strength));
+ else
+ return 'network-wireless-no-route-symbolic';
+ }
+};
+Signals.addSignalMethods(NMDeviceWireless.prototype);
+
+var NMVpnConnectionItem = class extends NMConnectionItem {
+ isActive() {
+ if (this._activeConnection == null)
+ return false;
+
+ return this._activeConnection.vpn_state != NM.VpnConnectionState.DISCONNECTED;
+ }
+
+ _buildUI() {
+ this.labelItem = new PopupMenu.PopupMenuItem('');
+ this.labelItem.connect('activate', this._toggle.bind(this));
+
+ this.radioItem = new PopupMenu.PopupSwitchMenuItem(this._connection.get_id(), false);
+ this.radioItem.connect('toggled', this._toggle.bind(this));
+ }
+
+ _sync() {
+ let isActive = this.isActive();
+ this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel();
+ this.radioItem.setToggleState(isActive);
+ this.radioItem.setStatus(this._getStatus());
+ this.emit('icon-changed');
+ }
+
+ _getStatus() {
+ if (this._activeConnection == null)
+ return null;
+
+ switch (this._activeConnection.vpn_state) {
+ case NM.VpnConnectionState.DISCONNECTED:
+ case NM.VpnConnectionState.ACTIVATED:
+ return null;
+ case NM.VpnConnectionState.PREPARE:
+ case NM.VpnConnectionState.CONNECT:
+ case NM.VpnConnectionState.IP_CONFIG_GET:
+ return _("connecting…");
+ case NM.VpnConnectionState.NEED_AUTH:
+ /* Translators: this is for network connections that require some kind of key or password */
+ return _("authentication required");
+ case NM.VpnConnectionState.FAILED:
+ return _("connection failed");
+ default:
+ return 'invalid';
+ }
+ }
+
+ _connectionStateChanged(ac, newstate, reason) {
+ if (newstate == NM.VpnConnectionState.FAILED &&
+ reason != NM.VpnConnectionStateReason.NO_SECRETS) {
+ // FIXME: if we ever want to show something based on reason,
+ // we need to convert from NM.VpnConnectionStateReason
+ // to NM.DeviceStateReason
+ this.emit('activation-failed', reason);
+ }
+
+ this.emit('icon-changed');
+ super._connectionStateChanged();
+ }
+
+ setActiveConnection(activeConnection) {
+ if (this._activeConnectionChangedId > 0) {
+ this._activeConnection.disconnect(this._activeConnectionChangedId);
+ this._activeConnectionChangedId = 0;
+ }
+
+ this._activeConnection = activeConnection;
+
+ if (this._activeConnection) {
+ this._activeConnectionChangedId = this._activeConnection.connect('vpn-state-changed',
+ this._connectionStateChanged.bind(this));
+ }
+
+ this._sync();
+ }
+
+ getIndicatorIcon() {
+ if (this._activeConnection) {
+ if (this._activeConnection.vpn_state < NM.VpnConnectionState.ACTIVATED)
+ return 'network-vpn-acquiring-symbolic';
+ else
+ return 'network-vpn-symbolic';
+ } else {
+ return '';
+ }
+ }
+};
+
+var NMVpnSection = class extends NMConnectionSection {
+ constructor(client) {
+ super(client);
+
+ this.item.menu.addSettingsAction(_("VPN Settings"), 'gnome-network-panel.desktop');
+
+ this._sync();
+ }
+
+ _sync() {
+ let nItems = this._connectionItems.size;
+ this.item.visible = nItems > 0;
+
+ super._sync();
+ }
+
+ get category() {
+ return NMConnectionCategory.VPN;
+ }
+
+ _getDescription() {
+ return _("VPN");
+ }
+
+ _getStatus() {
+ let values = this._connectionItems.values();
+ for (let item of values) {
+ if (item.isActive())
+ return item.getName();
+ }
+
+ return _("VPN Off");
+ }
+
+ _getMenuIcon() {
+ return this.getIndicatorIcon() || 'network-vpn-symbolic';
+ }
+
+ activateConnection(connection) {
+ this._client.activate_connection_async(connection, null, null, null, null);
+ }
+
+ deactivateConnection(activeConnection) {
+ this._client.deactivate_connection(activeConnection, null);
+ }
+
+ setActiveConnections(vpnConnections) {
+ let connections = this._connectionItems.values();
+ for (let item of connections)
+ item.setActiveConnection(null);
+
+ vpnConnections.forEach(a => {
+ if (a.connection) {
+ let item = this._connectionItems.get(a.connection.get_uuid());
+ item.setActiveConnection(a);
+ }
+ });
+ }
+
+ _makeConnectionItem(connection) {
+ return new NMVpnConnectionItem(this, connection);
+ }
+
+ getIndicatorIcon() {
+ let items = this._connectionItems.values();
+ for (let item of items) {
+ let icon = item.getIndicatorIcon();
+ if (icon)
+ return icon;
+ }
+ return '';
+ }
+};
+Signals.addSignalMethods(NMVpnSection.prototype);
+
+var DeviceCategory = class extends PopupMenu.PopupMenuSection {
+ constructor(category) {
+ super();
+
+ this._category = category;
+
+ this.devices = [];
+
+ this.section = new PopupMenu.PopupMenuSection();
+ this.section.box.connect('actor-added', this._sync.bind(this));
+ this.section.box.connect('actor-removed', this._sync.bind(this));
+ this.addMenuItem(this.section);
+
+ this._summaryItem = new PopupMenu.PopupSubMenuMenuItem('', true);
+ this._summaryItem.icon.icon_name = this._getSummaryIcon();
+ this.addMenuItem(this._summaryItem);
+
+ this._summaryItem.menu.addSettingsAction(_('Network Settings'),
+ 'gnome-network-panel.desktop');
+ this._summaryItem.hide();
+
+ }
+
+ _sync() {
+ let nDevices = this.section.box.get_children().reduce(
+ (prev, child) => prev + (child.visible ? 1 : 0), 0);
+ this._summaryItem.label.text = this._getSummaryLabel(nDevices);
+ let shouldSummarize = nDevices > MAX_DEVICE_ITEMS;
+ this._summaryItem.visible = shouldSummarize;
+ this.section.actor.visible = !shouldSummarize;
+ }
+
+ _getSummaryIcon() {
+ switch (this._category) {
+ case NMConnectionCategory.WIRED:
+ return 'network-wired-symbolic';
+ case NMConnectionCategory.WIRELESS:
+ case NMConnectionCategory.WWAN:
+ return 'network-wireless-symbolic';
+ }
+ return '';
+ }
+
+ _getSummaryLabel(nDevices) {
+ switch (this._category) {
+ case NMConnectionCategory.WIRED:
+ return ngettext("%s Wired Connection",
+ "%s Wired Connections",
+ nDevices).format(nDevices);
+ case NMConnectionCategory.WIRELESS:
+ return ngettext("%s Wi-Fi Connection",
+ "%s Wi-Fi Connections",
+ nDevices).format(nDevices);
+ case NMConnectionCategory.WWAN:
+ return ngettext("%s Modem Connection",
+ "%s Modem Connections",
+ nDevices).format(nDevices);
+ }
+ return '';
+ }
+};
+
+var NMApplet = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._primaryIndicator = this._addIndicator();
+ this._vpnIndicator = this._addIndicator();
+
+ // Device types
+ this._dtypes = { };
+ this._dtypes[NM.DeviceType.ETHERNET] = NMDeviceWired;
+ this._dtypes[NM.DeviceType.WIFI] = NMDeviceWireless;
+ this._dtypes[NM.DeviceType.MODEM] = NMDeviceModem;
+ this._dtypes[NM.DeviceType.BT] = NMDeviceBluetooth;
+
+ // Connection types
+ this._ctypes = { };
+ this._ctypes[NM.SETTING_WIRED_SETTING_NAME] = NMConnectionCategory.WIRED;
+ this._ctypes[NM.SETTING_WIRELESS_SETTING_NAME] = NMConnectionCategory.WIRELESS;
+ this._ctypes[NM.SETTING_BLUETOOTH_SETTING_NAME] = NMConnectionCategory.WWAN;
+ this._ctypes[NM.SETTING_CDMA_SETTING_NAME] = NMConnectionCategory.WWAN;
+ this._ctypes[NM.SETTING_GSM_SETTING_NAME] = NMConnectionCategory.WWAN;
+ this._ctypes[NM.SETTING_VPN_SETTING_NAME] = NMConnectionCategory.VPN;
+
+ this._getClient();
+ }
+
+ async _getClient() {
+ this._client = await NM.Client.new_async(null);
+
+ this._activeConnections = [];
+ this._connections = [];
+ this._connectivityQueue = [];
+
+ this._mainConnection = null;
+ this._mainConnectionIconChangedId = 0;
+ this._mainConnectionStateChangedId = 0;
+
+ this._notification = null;
+
+ this._nmDevices = [];
+ this._devices = { };
+
+ let categories = [NMConnectionCategory.WIRED,
+ NMConnectionCategory.WIRELESS,
+ NMConnectionCategory.WWAN];
+ for (let category of categories) {
+ this._devices[category] = new DeviceCategory(category);
+ this.menu.addMenuItem(this._devices[category]);
+ }
+
+ this._vpnSection = new NMVpnSection(this._client);
+ this._vpnSection.connect('activation-failed', this._onActivationFailed.bind(this));
+ this._vpnSection.connect('icon-changed', this._updateIcon.bind(this));
+ this.menu.addMenuItem(this._vpnSection.item);
+
+ this._readConnections();
+ this._readDevices();
+ this._syncNMState();
+ this._syncMainConnection();
+ this._syncVpnConnections();
+
+ this._client.connect('notify::nm-running', this._syncNMState.bind(this));
+ this._client.connect('notify::networking-enabled', this._syncNMState.bind(this));
+ this._client.connect('notify::state', this._syncNMState.bind(this));
+ this._client.connect('notify::primary-connection', this._syncMainConnection.bind(this));
+ this._client.connect('notify::activating-connection', this._syncMainConnection.bind(this));
+ this._client.connect('notify::active-connections', this._syncVpnConnections.bind(this));
+ this._client.connect('notify::connectivity', this._syncConnectivity.bind(this));
+ this._client.connect('device-added', this._deviceAdded.bind(this));
+ this._client.connect('device-removed', this._deviceRemoved.bind(this));
+ this._client.connect('connection-added', this._connectionAdded.bind(this));
+ this._client.connect('connection-removed', this._connectionRemoved.bind(this));
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+ }
+
+ _sessionUpdated() {
+ let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this.menu.setSensitive(sensitive);
+ }
+
+ _ensureSource() {
+ if (!this._source) {
+ this._source = new MessageTray.Source(_("Network Manager"),
+ 'network-transmit-receive');
+ this._source.policy = new MessageTray.NotificationApplicationPolicy('gnome-network-panel');
+
+ this._source.connect('destroy', () => (this._source = null));
+ Main.messageTray.add(this._source);
+ }
+ }
+
+ _readDevices() {
+ let devices = this._client.get_devices() || [];
+ for (let i = 0; i < devices.length; ++i) {
+ try {
+ this._deviceAdded(this._client, devices[i], true);
+ } catch (e) {
+ log('Failed to add device %s: %s'.format(devices[i], e.toString()));
+ }
+ }
+ this._syncDeviceNames();
+ }
+
+ _notify(iconName, title, text, urgency) {
+ if (this._notification)
+ this._notification.destroy();
+
+ this._ensureSource();
+
+ let gicon = new Gio.ThemedIcon({ name: iconName });
+ this._notification = new MessageTray.Notification(this._source, title, text, { gicon });
+ this._notification.setUrgency(urgency);
+ this._notification.setTransient(true);
+ this._notification.connect('destroy', () => {
+ this._notification = null;
+ });
+ this._source.showNotification(this._notification);
+ }
+
+ _onActivationFailed(_device, _reason) {
+ // XXX: nm-applet has no special text depending on reason
+ // but I'm not sure of this generic message
+ this._notify('network-error-symbolic',
+ _("Connection failed"),
+ _("Activation of network connection failed"),
+ MessageTray.Urgency.HIGH);
+ }
+
+ _syncDeviceNames() {
+ let names = NM.Device.disambiguate_names(this._nmDevices);
+ for (let i = 0; i < this._nmDevices.length; i++) {
+ let device = this._nmDevices[i];
+ let description = names[i];
+ if (device._delegate)
+ device._delegate.setDeviceDescription(description);
+ }
+ }
+
+ _deviceAdded(client, device, skipSyncDeviceNames) {
+ if (device._delegate) {
+ // already seen, not adding again
+ return;
+ }
+
+ let wrapperClass = this._dtypes[device.get_device_type()];
+ if (wrapperClass) {
+ let wrapper = new wrapperClass(this._client, device);
+ device._delegate = wrapper;
+ this._addDeviceWrapper(wrapper);
+
+ this._nmDevices.push(device);
+ this._deviceChanged(device, skipSyncDeviceNames);
+
+ device.connect('notify::interface', () => {
+ this._deviceChanged(device, false);
+ });
+ }
+ }
+
+ _deviceChanged(device, skipSyncDeviceNames) {
+ let wrapper = device._delegate;
+
+ if (!skipSyncDeviceNames)
+ this._syncDeviceNames();
+
+ if (wrapper instanceof NMConnectionSection) {
+ this._connections.forEach(connection => {
+ wrapper.checkConnection(connection);
+ });
+ }
+ }
+
+ _addDeviceWrapper(wrapper) {
+ wrapper._activationFailedId = wrapper.connect('activation-failed',
+ this._onActivationFailed.bind(this));
+
+ let section = this._devices[wrapper.category].section;
+ section.addMenuItem(wrapper.item);
+
+ let devices = this._devices[wrapper.category].devices;
+ devices.push(wrapper);
+ }
+
+ _deviceRemoved(client, device) {
+ let pos = this._nmDevices.indexOf(device);
+ if (pos != -1) {
+ this._nmDevices.splice(pos, 1);
+ this._syncDeviceNames();
+ }
+
+ let wrapper = device._delegate;
+ if (!wrapper) {
+ log('Removing a network device that was not added');
+ return;
+ }
+
+ this._removeDeviceWrapper(wrapper);
+ }
+
+ _removeDeviceWrapper(wrapper) {
+ wrapper.disconnect(wrapper._activationFailedId);
+ wrapper.destroy();
+
+ let devices = this._devices[wrapper.category].devices;
+ let pos = devices.indexOf(wrapper);
+ devices.splice(pos, 1);
+ }
+
+ _getMainConnection() {
+ let connection;
+
+ connection = this._client.get_primary_connection();
+ if (connection) {
+ ensureActiveConnectionProps(connection);
+ return connection;
+ }
+
+ connection = this._client.get_activating_connection();
+ if (connection) {
+ ensureActiveConnectionProps(connection);
+ return connection;
+ }
+
+ return null;
+ }
+
+ _syncMainConnection() {
+ if (this._mainConnectionIconChangedId > 0) {
+ this._mainConnection._primaryDevice.disconnect(this._mainConnectionIconChangedId);
+ this._mainConnectionIconChangedId = 0;
+ }
+
+ if (this._mainConnectionStateChangedId > 0) {
+ this._mainConnection.disconnect(this._mainConnectionStateChangedId);
+ this._mainConnectionStateChangedId = 0;
+ }
+
+ this._mainConnection = this._getMainConnection();
+
+ if (this._mainConnection) {
+ if (this._mainConnection._primaryDevice)
+ this._mainConnectionIconChangedId = this._mainConnection._primaryDevice.connect('icon-changed', this._updateIcon.bind(this));
+ this._mainConnectionStateChangedId = this._mainConnection.connect('notify::state', this._mainConnectionStateChanged.bind(this));
+ this._mainConnectionStateChanged();
+ }
+
+ this._updateIcon();
+ this._syncConnectivity();
+ }
+
+ _syncVpnConnections() {
+ let activeConnections = this._client.get_active_connections() || [];
+ let vpnConnections = activeConnections.filter(
+ a => a instanceof NM.VpnConnection);
+ vpnConnections.forEach(a => {
+ ensureActiveConnectionProps(a);
+ });
+ this._vpnSection.setActiveConnections(vpnConnections);
+
+ this._updateIcon();
+ }
+
+ _mainConnectionStateChanged() {
+ if (this._mainConnection.state == NM.ActiveConnectionState.ACTIVATED && this._notification)
+ this._notification.destroy();
+ }
+
+ _ignoreConnection(connection) {
+ let setting = connection.get_setting_connection();
+ if (!setting)
+ return true;
+
+ // Ignore slave connections
+ if (setting.get_master())
+ return true;
+
+ return false;
+ }
+
+ _addConnection(connection) {
+ if (this._ignoreConnection(connection))
+ return;
+ if (connection._updatedId) {
+ // connection was already seen
+ return;
+ }
+
+ connection._updatedId = connection.connect('changed', this._updateConnection.bind(this));
+
+ this._updateConnection(connection);
+ this._connections.push(connection);
+ }
+
+ _readConnections() {
+ let connections = this._client.get_connections();
+ connections.forEach(this._addConnection.bind(this));
+ }
+
+ _connectionAdded(client, connection) {
+ this._addConnection(connection);
+ }
+
+ _connectionRemoved(client, connection) {
+ let pos = this._connections.indexOf(connection);
+ if (pos != -1)
+ this._connections.splice(pos, 1);
+
+ let section = connection._section;
+
+ if (section == NMConnectionCategory.INVALID)
+ return;
+
+ if (section == NMConnectionCategory.VPN) {
+ this._vpnSection.removeConnection(connection);
+ } else {
+ let devices = this._devices[section].devices;
+ for (let i = 0; i < devices.length; i++) {
+ if (devices[i] instanceof NMConnectionSection)
+ devices[i].removeConnection(connection);
+ }
+ }
+
+ connection.disconnect(connection._updatedId);
+ connection._updatedId = 0;
+ }
+
+ _updateConnection(connection) {
+ let connectionSettings = connection.get_setting_by_name(NM.SETTING_CONNECTION_SETTING_NAME);
+ connection._type = connectionSettings.type;
+ connection._section = this._ctypes[connection._type] || NMConnectionCategory.INVALID;
+
+ let section = connection._section;
+
+ if (section == NMConnectionCategory.INVALID)
+ return;
+
+ if (section == NMConnectionCategory.VPN) {
+ this._vpnSection.checkConnection(connection);
+ } else {
+ let devices = this._devices[section].devices;
+ devices.forEach(wrapper => {
+ if (wrapper instanceof NMConnectionSection)
+ wrapper.checkConnection(connection);
+ });
+ }
+ }
+
+ _syncNMState() {
+ this.visible = this._client.nm_running;
+ this.menu.actor.visible = this._client.networking_enabled;
+
+ this._updateIcon();
+ this._syncConnectivity();
+ }
+
+ _flushConnectivityQueue() {
+ if (this._portalHelperProxy) {
+ for (let item of this._connectivityQueue)
+ this._portalHelperProxy.CloseRemote(item);
+ }
+
+ this._connectivityQueue = [];
+ }
+
+ _closeConnectivityCheck(path) {
+ let index = this._connectivityQueue.indexOf(path);
+
+ if (index >= 0) {
+ if (this._portalHelperProxy)
+ this._portalHelperProxy.CloseRemote(path);
+
+ this._connectivityQueue.splice(index, 1);
+ }
+ }
+
+ async _portalHelperDone(proxy, emitter, parameters) {
+ let [path, result] = parameters;
+
+ if (result == PortalHelperResult.CANCELLED) {
+ // Keep the connection in the queue, so the user is not
+ // spammed with more logins until we next flush the queue,
+ // which will happen once he chooses a better connection
+ // or we get to full connectivity through other means
+ } else if (result == PortalHelperResult.COMPLETED) {
+ this._closeConnectivityCheck(path);
+ } else if (result == PortalHelperResult.RECHECK) {
+ try {
+ const state = await this._client.check_connectivity_async(null);
+ if (state >= NM.ConnectivityState.FULL)
+ this._closeConnectivityCheck(path);
+ } catch (e) { }
+ } else {
+ log('Invalid result from portal helper: %s'.format(result));
+ }
+ }
+
+ _syncConnectivity() {
+ if (this._mainConnection == null ||
+ this._mainConnection.state != NM.ActiveConnectionState.ACTIVATED) {
+ this._flushConnectivityQueue();
+ return;
+ }
+
+ let isPortal = this._client.connectivity == NM.ConnectivityState.PORTAL;
+ // For testing, allow interpreting any value != FULL as PORTAL, because
+ // LIMITED (no upstream route after the default gateway) is easy to obtain
+ // with a tethered phone
+ // NONE is also possible, with a connection configured to force no default route
+ // (but in general we should only prompt a portal if we know there is a portal)
+ if (GLib.getenv('GNOME_SHELL_CONNECTIVITY_TEST') != null)
+ isPortal = isPortal || this._client.connectivity < NM.ConnectivityState.FULL;
+ if (!isPortal || Main.sessionMode.isGreeter)
+ return;
+
+ let path = this._mainConnection.get_path();
+ for (let item of this._connectivityQueue) {
+ if (item == path)
+ return;
+ }
+
+ let timestamp = global.get_current_time();
+ if (this._portalHelperProxy) {
+ this._portalHelperProxy.AuthenticateRemote(path, '', timestamp);
+ } else {
+ new PortalHelperProxy(Gio.DBus.session, 'org.gnome.Shell.PortalHelper',
+ '/org/gnome/Shell/PortalHelper', (proxy, error) => {
+ if (error) {
+ log('Error launching the portal helper: %s'.format(error));
+ return;
+ }
+
+ this._portalHelperProxy = proxy;
+ proxy.connectSignal('Done', this._portalHelperDone.bind(this));
+
+ proxy.AuthenticateRemote(path, '', timestamp);
+ });
+ }
+
+ this._connectivityQueue.push(path);
+ }
+
+ _updateIcon() {
+ if (!this._client.networking_enabled) {
+ this._primaryIndicator.visible = false;
+ } else {
+ let dev = null;
+ if (this._mainConnection)
+ dev = this._mainConnection._primaryDevice;
+
+ let state = this._client.get_state();
+ let connected = state == NM.State.CONNECTED_GLOBAL;
+ this._primaryIndicator.visible = (dev != null) || connected;
+ if (dev) {
+ this._primaryIndicator.icon_name = dev.getIndicatorIcon();
+ } else if (connected) {
+ if (this._client.connectivity == NM.ConnectivityState.FULL)
+ this._primaryIndicator.icon_name = 'network-wired-symbolic';
+ else
+ this._primaryIndicator.icon_name = 'network-wired-no-route-symbolic';
+ }
+ }
+
+ this._vpnIndicator.icon_name = this._vpnSection.getIndicatorIcon();
+ this._vpnIndicator.visible = this._vpnIndicator.icon_name !== null;
+ }
+});
diff --git a/js/ui/status/nightLight.js b/js/ui/status/nightLight.js
new file mode 100644
index 0000000..c595c3d
--- /dev/null
+++ b/js/ui/status/nightLight.js
@@ -0,0 +1,70 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Gio, GObject } = imports.gi;
+
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const BUS_NAME = 'org.gnome.SettingsDaemon.Color';
+const OBJECT_PATH = '/org/gnome/SettingsDaemon/Color';
+
+const ColorInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Color');
+const ColorProxy = Gio.DBusProxy.makeProxyWrapper(ColorInterface);
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._indicator = this._addIndicator();
+ this._indicator.icon_name = 'night-light-symbolic';
+ this._proxy = new ColorProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH,
+ (proxy, error) => {
+ if (error) {
+ log(error.message);
+ return;
+ }
+ this._proxy.connect('g-properties-changed',
+ this._sync.bind(this));
+ this._sync();
+ });
+
+ this._item = new PopupMenu.PopupSubMenuMenuItem("", true);
+ this._item.icon.icon_name = 'night-light-symbolic';
+ this._disableItem = this._item.menu.addAction('', () => {
+ this._proxy.DisabledUntilTomorrow = !this._proxy.DisabledUntilTomorrow;
+ });
+ this._item.menu.addAction(_("Turn Off"), () => {
+ let settings = new Gio.Settings({ schema_id: 'org.gnome.settings-daemon.plugins.color' });
+ settings.set_boolean('night-light-enabled', false);
+ });
+ this._item.menu.addSettingsAction(_("Display Settings"), 'gnome-display-panel.desktop');
+ this.menu.addMenuItem(this._item);
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+ this._sync();
+ }
+
+ _sessionUpdated() {
+ let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this.menu.setSensitive(sensitive);
+ }
+
+ _sync() {
+ let visible = this._proxy.NightLightActive;
+ let disabled = this._proxy.DisabledUntilTomorrow;
+
+ this._item.label.text = disabled
+ ? _("Night Light Disabled")
+ : _("Night Light On");
+ this._disableItem.label.text = disabled
+ ? _("Resume")
+ : _("Disable Until Tomorrow");
+ this._item.visible = this._indicator.visible = visible;
+ }
+});
diff --git a/js/ui/status/power.js b/js/ui/status/power.js
new file mode 100644
index 0000000..ca85a98
--- /dev/null
+++ b/js/ui/status/power.js
@@ -0,0 +1,155 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Clutter, Gio, GObject, St, UPowerGlib: UPower } = imports.gi;
+
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const BUS_NAME = 'org.freedesktop.UPower';
+const OBJECT_PATH = '/org/freedesktop/UPower/devices/DisplayDevice';
+
+const DisplayDeviceInterface = loadInterfaceXML('org.freedesktop.UPower.Device');
+const PowerManagerProxy = Gio.DBusProxy.makeProxyWrapper(DisplayDeviceInterface);
+
+const SHOW_BATTERY_PERCENTAGE = 'show-battery-percentage';
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._desktopSettings = new Gio.Settings({
+ schema_id: 'org.gnome.desktop.interface',
+ });
+ this._desktopSettings.connect(
+ 'changed::%s'.format(SHOW_BATTERY_PERCENTAGE), this._sync.bind(this));
+
+ this._indicator = this._addIndicator();
+ this._percentageLabel = new St.Label({
+ y_expand: true,
+ y_align: Clutter.ActorAlign.CENTER,
+ });
+ this.add_child(this._percentageLabel);
+ this.add_style_class_name('power-status');
+
+ this._proxy = new PowerManagerProxy(Gio.DBus.system, BUS_NAME, OBJECT_PATH,
+ (proxy, error) => {
+ if (error) {
+ log(error.message);
+ } else {
+ this._proxy.connect('g-properties-changed',
+ this._sync.bind(this));
+ }
+ this._sync();
+ });
+
+ this._item = new PopupMenu.PopupSubMenuMenuItem('', true);
+ this._item.menu.addSettingsAction(_('Power Settings'),
+ 'gnome-power-panel.desktop');
+ this.menu.addMenuItem(this._item);
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+ }
+
+ _sessionUpdated() {
+ let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this.menu.setSensitive(sensitive);
+ }
+
+ _getStatus() {
+ let seconds = 0;
+
+ if (this._proxy.State === UPower.DeviceState.FULLY_CHARGED)
+ return _('Fully Charged');
+ else if (this._proxy.State === UPower.DeviceState.CHARGING)
+ seconds = this._proxy.TimeToFull;
+ else if (this._proxy.State === UPower.DeviceState.DISCHARGING)
+ seconds = this._proxy.TimeToEmpty;
+ else if (this._proxy.State === UPower.DeviceState.PENDING_CHARGE)
+ return _('Not Charging');
+ // state is PENDING_DISCHARGE
+ else
+ return _('Estimating…');
+
+ let time = Math.round(seconds / 60);
+ if (time === 0) {
+ // 0 is reported when UPower does not have enough data
+ // to estimate battery life
+ return _('Estimating…');
+ }
+
+ let minutes = time % 60;
+ let hours = Math.floor(time / 60);
+
+ if (this._proxy.State === UPower.DeviceState.DISCHARGING) {
+ // Translators: this is <hours>:<minutes> Remaining (<percentage>)
+ return _('%d\u2236%02d Remaining (%d\u2009%%)').format(
+ hours, minutes, this._proxy.Percentage);
+ }
+
+ if (this._proxy.State === UPower.DeviceState.CHARGING) {
+ // Translators: this is <hours>:<minutes> Until Full (<percentage>)
+ return _('%d\u2236%02d Until Full (%d\u2009%%)').format(
+ hours, minutes, this._proxy.Percentage);
+ }
+
+ return null;
+ }
+
+ _sync() {
+ // Do we have batteries or a UPS?
+ let visible = this._proxy.IsPresent;
+ if (visible) {
+ this._item.show();
+ this._percentageLabel.visible =
+ this._desktopSettings.get_boolean(SHOW_BATTERY_PERCENTAGE);
+ } else {
+ // If there's no battery, then we use the power icon.
+ this._item.hide();
+ this._indicator.icon_name = 'system-shutdown-symbolic';
+ this._percentageLabel.hide();
+ return;
+ }
+
+ // The icons
+ let chargingState = this._proxy.State === UPower.DeviceState.CHARGING
+ ? '-charging' : '';
+ let fillLevel = 10 * Math.floor(this._proxy.Percentage / 10);
+ const charged =
+ this._proxy.State === UPower.DeviceState.FULLY_CHARGED ||
+ (this._proxy.State === UPower.DeviceState.CHARGING && fillLevel === 100);
+ const icon = charged
+ ? 'battery-level-100-charged-symbolic'
+ : 'battery-level-%d%s-symbolic'.format(fillLevel, chargingState);
+
+ // Make sure we fall back to fallback-icon-name and not GThemedIcon's
+ // default fallbacks
+ let gicon = new Gio.ThemedIcon({
+ name: icon,
+ use_default_fallbacks: false,
+ });
+
+ this._indicator.gicon = gicon;
+ this._item.icon.gicon = gicon;
+
+ let fallbackIcon = this._proxy.IconName;
+ this._indicator.fallback_icon_name = fallbackIcon;
+ this._item.icon.fallback_icon_name = fallbackIcon;
+
+ // The icon label
+ let label;
+ if (this._proxy.State == UPower.DeviceState.FULLY_CHARGED)
+ label = _("%d\u2009%%").format(100);
+ else
+ label = _("%d\u2009%%").format(this._proxy.Percentage);
+ this._percentageLabel.text = label;
+
+ // The status label
+ this._item.label.text = this._getStatus();
+ }
+});
diff --git a/js/ui/status/remoteAccess.js b/js/ui/status/remoteAccess.js
new file mode 100644
index 0000000..21f6581
--- /dev/null
+++ b/js/ui/status/remoteAccess.js
@@ -0,0 +1,97 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported RemoteAccessApplet */
+
+const { GObject, Meta } = imports.gi;
+
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+var RemoteAccessApplet = GObject.registerClass(
+class RemoteAccessApplet extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ let controller = global.backend.get_remote_access_controller();
+
+ if (!controller)
+ return;
+
+ this._handles = new Set();
+ this._sharedIndicator = null;
+ this._recordingIndicator = null;
+ this._menuSection = null;
+
+ controller.connect('new-handle', (o, handle) => {
+ this._onNewHandle(handle);
+ });
+ }
+
+ _ensureControls() {
+ if (this._sharedIndicator && this._recordingIndicator)
+ return;
+
+ this._sharedIndicator = this._addIndicator();
+ this._sharedIndicator.icon_name = 'screen-shared-symbolic';
+ this._sharedIndicator.add_style_class_name('remote-access-indicator');
+
+ this._sharedItem =
+ new PopupMenu.PopupSubMenuMenuItem(_("Screen is Being Shared"),
+ true);
+ this._sharedItem.menu.addAction(_("Turn off"),
+ () => {
+ for (let handle of this._handles) {
+ if (!handle.is_recording)
+ handle.stop();
+ }
+ });
+ this._sharedItem.icon.icon_name = 'screen-shared-symbolic';
+ this.menu.addMenuItem(this._sharedItem);
+
+ this._recordingIndicator = this._addIndicator();
+ this._recordingIndicator.icon_name = 'media-record-symbolic';
+ this._recordingIndicator.add_style_class_name('screencast-indicator');
+ }
+
+ _isScreenShared() {
+ return [...this._handles].some(handle => !handle.is_recording);
+ }
+
+ _isRecording() {
+ return [...this._handles].some(handle => handle.is_recording);
+ }
+
+ _sync() {
+ if (this._isScreenShared()) {
+ this._sharedIndicator.visible = true;
+ this._sharedItem.visible = true;
+ } else {
+ this._sharedIndicator.visible = false;
+ this._sharedItem.visible = false;
+ }
+
+ this._recordingIndicator.visible = this._isRecording();
+ }
+
+ _onStopped(handle) {
+ this._handles.delete(handle);
+ this._sync();
+ }
+
+ _onNewHandle(handle) {
+ // We can't possibly know about all types of screen sharing on X11, so
+ // showing these controls on X11 might give a false sense of security.
+ // Thus, only enable these controls when using Wayland, where we are
+ // in control of sharing.
+ //
+ // We still want to show screen recordings though, to indicate when
+ // the built in screen recorder is active, no matter the session type.
+ if (!Meta.is_wayland_compositor() && !handle.is_recording)
+ return;
+
+ this._handles.add(handle);
+ handle.connect('stopped', this._onStopped.bind(this));
+
+ this._ensureControls();
+ this._sync();
+ }
+});
diff --git a/js/ui/status/rfkill.js b/js/ui/status/rfkill.js
new file mode 100644
index 0000000..9f8b09d
--- /dev/null
+++ b/js/ui/status/rfkill.js
@@ -0,0 +1,112 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Gio, GObject } = imports.gi;
+const Signals = imports.signals;
+
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
+const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';
+
+const RfkillManagerInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Rfkill');
+const RfkillManagerProxy = Gio.DBusProxy.makeProxyWrapper(RfkillManagerInterface);
+
+var RfkillManager = class {
+ constructor() {
+ this._proxy = new RfkillManagerProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH,
+ (proxy, error) => {
+ if (error) {
+ log(error.message);
+ return;
+ }
+ this._proxy.connect('g-properties-changed',
+ this._changed.bind(this));
+ this._changed();
+ });
+ }
+
+ get airplaneMode() {
+ return this._proxy.AirplaneMode;
+ }
+
+ set airplaneMode(v) {
+ this._proxy.AirplaneMode = v;
+ }
+
+ get hwAirplaneMode() {
+ return this._proxy.HardwareAirplaneMode;
+ }
+
+ get shouldShowAirplaneMode() {
+ return this._proxy.ShouldShowAirplaneMode;
+ }
+
+ _changed() {
+ this.emit('airplane-mode-changed');
+ }
+};
+Signals.addSignalMethods(RfkillManager.prototype);
+
+var _manager;
+function getRfkillManager() {
+ if (_manager != null)
+ return _manager;
+
+ _manager = new RfkillManager();
+ return _manager;
+}
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._manager = getRfkillManager();
+ this._manager.connect('airplane-mode-changed', this._sync.bind(this));
+
+ this._indicator = this._addIndicator();
+ this._indicator.icon_name = 'airplane-mode-symbolic';
+ this._indicator.hide();
+
+ // The menu only appears when airplane mode is on, so just
+ // statically build it as if it was on, rather than dynamically
+ // changing the menu contents.
+ this._item = new PopupMenu.PopupSubMenuMenuItem(_("Airplane Mode On"), true);
+ this._item.icon.icon_name = 'airplane-mode-symbolic';
+ this._offItem = this._item.menu.addAction(_("Turn Off"), () => {
+ this._manager.airplaneMode = false;
+ });
+ this._item.menu.addSettingsAction(_("Network Settings"), 'gnome-network-panel.desktop');
+ this.menu.addMenuItem(this._item);
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+
+ this._sync();
+ }
+
+ _sessionUpdated() {
+ let sensitive = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this.menu.setSensitive(sensitive);
+ }
+
+ _sync() {
+ let airplaneMode = this._manager.airplaneMode;
+ let hwAirplaneMode = this._manager.hwAirplaneMode;
+ let showAirplaneMode = this._manager.shouldShowAirplaneMode;
+
+ this._indicator.visible = airplaneMode && showAirplaneMode;
+ this._item.visible = airplaneMode && showAirplaneMode;
+ this._offItem.setSensitive(!hwAirplaneMode);
+
+ if (hwAirplaneMode)
+ this._offItem.label.text = _("Use hardware switch to turn off");
+ else
+ this._offItem.label.text = _("Turn Off");
+ }
+});
diff --git a/js/ui/status/system.js b/js/ui/status/system.js
new file mode 100644
index 0000000..6f71109
--- /dev/null
+++ b/js/ui/status/system.js
@@ -0,0 +1,178 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { GObject, Shell, St } = imports.gi;
+
+const BoxPointer = imports.ui.boxpointer;
+const SystemActions = imports.misc.systemActions;
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._systemActions = new SystemActions.getDefault();
+
+ this._createSubMenu();
+
+ this._loginScreenItem.connect('notify::visible',
+ () => this._updateSessionSubMenu());
+ this._logoutItem.connect('notify::visible',
+ () => this._updateSessionSubMenu());
+ this._suspendItem.connect('notify::visible',
+ () => this._updateSessionSubMenu());
+ this._powerOffItem.connect('notify::visible',
+ () => this._updateSessionSubMenu());
+ this._restartItem.connect('notify::visible',
+ () => this._updateSessionSubMenu());
+ // Whether shutdown is available or not depends on both lockdown
+ // settings (disable-log-out) and Polkit policy - the latter doesn't
+ // notify, so we update the menu item each time the menu opens or
+ // the lockdown setting changes, which should be close enough.
+ this.menu.connect('open-state-changed', (menu, open) => {
+ if (!open)
+ return;
+
+ this._systemActions.forceUpdate();
+ });
+ this._updateSessionSubMenu();
+
+ Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
+ this._sessionUpdated();
+ }
+
+ _sessionUpdated() {
+ this._settingsItem.visible = Main.sessionMode.allowSettings;
+ }
+
+ _updateSessionSubMenu() {
+ this._sessionSubMenu.visible =
+ this._loginScreenItem.visible ||
+ this._logoutItem.visible ||
+ this._suspendItem.visible ||
+ this._restartItem.visible ||
+ this._powerOffItem.visible;
+ }
+
+ _createSubMenu() {
+ let bindFlags = GObject.BindingFlags.DEFAULT | GObject.BindingFlags.SYNC_CREATE;
+ let item;
+
+ item = new PopupMenu.PopupImageMenuItem(
+ this._systemActions.getName('lock-orientation'),
+ this._systemActions.orientation_lock_icon);
+
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activateLockOrientation();
+ });
+ this.menu.addMenuItem(item);
+ this._orientationLockItem = item;
+ this._systemActions.bind_property('can-lock-orientation',
+ this._orientationLockItem, 'visible',
+ bindFlags);
+ this._systemActions.connect('notify::orientation-lock-icon', () => {
+ let iconName = this._systemActions.orientation_lock_icon;
+ let labelText = this._systemActions.getName("lock-orientation");
+
+ this._orientationLockItem.setIcon(iconName);
+ this._orientationLockItem.label.text = labelText;
+ });
+
+ let app = this._settingsApp = Shell.AppSystem.get_default().lookup_app(
+ 'gnome-control-center.desktop');
+ if (app) {
+ const [icon] = app.app_info.get_icon().names;
+ const name = app.app_info.get_name();
+ item = new PopupMenu.PopupImageMenuItem(name, icon);
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ Main.overview.hide();
+ this._settingsApp.activate();
+ });
+ this.menu.addMenuItem(item);
+ this._settingsItem = item;
+ } else {
+ log('Missing required core component Settings, expect trouble…');
+ this._settingsItem = new St.Widget();
+ }
+
+ item = new PopupMenu.PopupImageMenuItem(_('Lock'), 'changes-prevent-symbolic');
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activateLockScreen();
+ });
+ this.menu.addMenuItem(item);
+ this._lockScreenItem = item;
+ this._systemActions.bind_property('can-lock-screen',
+ this._lockScreenItem, 'visible',
+ bindFlags);
+
+ this._sessionSubMenu = new PopupMenu.PopupSubMenuMenuItem(
+ _('Power Off / Log Out'), true);
+ this._sessionSubMenu.icon.icon_name = 'system-shutdown-symbolic';
+
+ item = new PopupMenu.PopupMenuItem(_('Suspend'));
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activateSuspend();
+ });
+ this._sessionSubMenu.menu.addMenuItem(item);
+ this._suspendItem = item;
+ this._systemActions.bind_property('can-suspend',
+ this._suspendItem, 'visible',
+ bindFlags);
+
+ item = new PopupMenu.PopupMenuItem(_('Restart…'));
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activateRestart();
+ });
+ this._sessionSubMenu.menu.addMenuItem(item);
+ this._restartItem = item;
+ this._systemActions.bind_property('can-restart',
+ this._restartItem, 'visible',
+ bindFlags);
+
+ item = new PopupMenu.PopupMenuItem(_('Power Off…'));
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activatePowerOff();
+ });
+ this._sessionSubMenu.menu.addMenuItem(item);
+ this._powerOffItem = item;
+ this._systemActions.bind_property('can-power-off',
+ this._powerOffItem, 'visible',
+ bindFlags);
+
+ this._sessionSubMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ item = new PopupMenu.PopupMenuItem(_('Log Out'));
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activateLogout();
+ });
+ this._sessionSubMenu.menu.addMenuItem(item);
+ this._logoutItem = item;
+ this._systemActions.bind_property('can-logout',
+ this._logoutItem, 'visible',
+ bindFlags);
+
+ item = new PopupMenu.PopupMenuItem(_('Switch User…'));
+ item.connect('activate', () => {
+ this.menu.itemActivated(BoxPointer.PopupAnimation.NONE);
+ this._systemActions.activateSwitchUser();
+ });
+ this._sessionSubMenu.menu.addMenuItem(item);
+ this._loginScreenItem = item;
+ this._systemActions.bind_property('can-switch-user',
+ this._loginScreenItem, 'visible',
+ bindFlags);
+
+ this.menu.addMenuItem(this._sessionSubMenu);
+ }
+});
diff --git a/js/ui/status/thunderbolt.js b/js/ui/status/thunderbolt.js
new file mode 100644
index 0000000..d98355d
--- /dev/null
+++ b/js/ui/status/thunderbolt.js
@@ -0,0 +1,340 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+// the following is a modified version of bolt/contrib/js/client.js
+
+const { Gio, GLib, GObject, Polkit, Shell } = imports.gi;
+const Signals = imports.signals;
+
+const Main = imports.ui.main;
+const MessageTray = imports.ui.messageTray;
+const PanelMenu = imports.ui.panelMenu;
+
+const { loadInterfaceXML } = imports.misc.fileUtils;
+
+/* Keep in sync with data/org.freedesktop.bolt.xml */
+
+const BoltClientInterface = loadInterfaceXML('org.freedesktop.bolt1.Manager');
+const BoltDeviceInterface = loadInterfaceXML('org.freedesktop.bolt1.Device');
+
+const BoltDeviceProxy = Gio.DBusProxy.makeProxyWrapper(BoltDeviceInterface);
+
+/* */
+
+var Status = {
+ DISCONNECTED: 'disconnected',
+ CONNECTING: 'connecting',
+ CONNECTED: 'connected',
+ AUTHORIZING: 'authorizing',
+ AUTH_ERROR: 'auth-error',
+ AUTHORIZED: 'authorized',
+};
+
+var Policy = {
+ DEFAULT: 'default',
+ MANUAL: 'manual',
+ AUTO: 'auto',
+};
+
+var AuthCtrl = {
+ NONE: 'none',
+};
+
+var AuthMode = {
+ DISABLED: 'disabled',
+ ENABLED: 'enabled',
+};
+
+const BOLT_DBUS_CLIENT_IFACE = 'org.freedesktop.bolt1.Manager';
+const BOLT_DBUS_NAME = 'org.freedesktop.bolt';
+const BOLT_DBUS_PATH = '/org/freedesktop/bolt';
+
+var Client = class {
+ constructor() {
+ this._proxy = null;
+ this.probing = false;
+ this._getProxy();
+ }
+
+ async _getProxy() {
+ let nodeInfo = Gio.DBusNodeInfo.new_for_xml(BoltClientInterface);
+ try {
+ this._proxy = await Gio.DBusProxy.new(
+ Gio.DBus.system,
+ Gio.DBusProxyFlags.DO_NOT_AUTO_START,
+ nodeInfo.lookup_interface(BOLT_DBUS_CLIENT_IFACE),
+ BOLT_DBUS_NAME,
+ BOLT_DBUS_PATH,
+ BOLT_DBUS_CLIENT_IFACE,
+ null);
+ } catch (e) {
+ log('error creating bolt proxy: %s'.format(e.message));
+ return;
+ }
+ this._propsChangedId = this._proxy.connect('g-properties-changed', this._onPropertiesChanged.bind(this));
+ this._deviceAddedId = this._proxy.connectSignal('DeviceAdded', this._onDeviceAdded.bind(this));
+
+ this.probing = this._proxy.Probing;
+ if (this.probing)
+ this.emit('probing-changed', this.probing);
+
+ }
+
+ _onPropertiesChanged(proxy, properties) {
+ let unpacked = properties.deep_unpack();
+ if (!('Probing' in unpacked))
+ return;
+
+ this.probing = this._proxy.Probing;
+ this.emit('probing-changed', this.probing);
+ }
+
+ _onDeviceAdded(proxy, emitter, params) {
+ let [path] = params;
+ let device = new BoltDeviceProxy(Gio.DBus.system,
+ BOLT_DBUS_NAME,
+ path);
+ this.emit('device-added', device);
+ }
+
+ /* public methods */
+ close() {
+ if (!this._proxy)
+ return;
+
+ this._proxy.disconnectSignal(this._deviceAddedId);
+ this._proxy.disconnect(this._propsChangedId);
+ this._proxy = null;
+ }
+
+ enrollDevice(id, policy, callback) {
+ this._proxy.EnrollDeviceRemote(id, policy, AuthCtrl.NONE, (res, error) => {
+ if (error) {
+ Gio.DBusError.strip_remote_error(error);
+ callback(null, error);
+ return;
+ }
+
+ let [path] = res;
+ let device = new BoltDeviceProxy(Gio.DBus.system,
+ BOLT_DBUS_NAME,
+ path);
+ callback(device, null);
+ });
+ }
+
+ get authMode() {
+ return this._proxy.AuthMode;
+ }
+};
+Signals.addSignalMethods(Client.prototype);
+
+/* helper class to automatically authorize new devices */
+var AuthRobot = class {
+ constructor(client) {
+ this._client = client;
+
+ this._devicesToEnroll = [];
+ this._enrolling = false;
+
+ this._client.connect('device-added', this._onDeviceAdded.bind(this));
+ }
+
+ close() {
+ this.disconnectAll();
+ this._client = null;
+ }
+
+ /* the "device-added" signal will be emitted by boltd for every
+ * device that is not currently stored in the database. We are
+ * only interested in those devices, because all known devices
+ * will be handled by the user himself */
+ _onDeviceAdded(cli, dev) {
+ if (dev.Status !== Status.CONNECTED)
+ return;
+
+ /* check if authorization is enabled in the daemon. if not
+ * we won't even bother authorizing, because we will only
+ * get an error back. The exact contents of AuthMode might
+ * change in the future, but must contain AuthMode.ENABLED
+ * if it is enabled. */
+ if (!cli.authMode.split('|').includes(AuthMode.ENABLED))
+ return;
+
+ /* check if we should enroll the device */
+ let res = [false];
+ this.emit('enroll-device', dev, res);
+ if (res[0] !== true)
+ return;
+
+ /* ok, we should authorize the device, add it to the back
+ * of the list */
+ this._devicesToEnroll.push(dev);
+ this._enrollDevices();
+ }
+
+ /* The enrollment queue:
+ * - new devices will be added to the end of the array.
+ * - an idle callback will be scheduled that will keep
+ * calling itself as long as there a devices to be
+ * enrolled.
+ */
+ _enrollDevices() {
+ if (this._enrolling)
+ return;
+
+ this._enrolling = true;
+ GLib.idle_add(GLib.PRIORITY_DEFAULT,
+ this._enrollDevicesIdle.bind(this));
+ }
+
+ _onEnrollDone(device, error) {
+ if (error)
+ this.emit('enroll-failed', device, error);
+
+ /* TODO: scan the list of devices to be authorized for children
+ * of this device and remove them (and their children and
+ * their children and ....) from the device queue
+ */
+ this._enrolling = this._devicesToEnroll.length > 0;
+
+ if (this._enrolling) {
+ GLib.idle_add(GLib.PRIORITY_DEFAULT,
+ this._enrollDevicesIdle.bind(this));
+ }
+ }
+
+ _enrollDevicesIdle() {
+ let devices = this._devicesToEnroll;
+
+ let dev = devices.shift();
+ if (dev === undefined)
+ return GLib.SOURCE_REMOVE;
+
+ this._client.enrollDevice(dev.Uid,
+ Policy.DEFAULT,
+ this._onEnrollDone.bind(this));
+ return GLib.SOURCE_REMOVE;
+ }
+};
+Signals.addSignalMethods(AuthRobot.prototype);
+
+/* eof client.js */
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._indicator = this._addIndicator();
+ this._indicator.icon_name = 'thunderbolt-symbolic';
+
+ this._client = new Client();
+ this._client.connect('probing-changed', this._onProbing.bind(this));
+
+ this._robot = new AuthRobot(this._client);
+
+ this._robot.connect('enroll-device', this._onEnrollDevice.bind(this));
+ this._robot.connect('enroll-failed', this._onEnrollFailed.bind(this));
+
+ Main.sessionMode.connect('updated', this._sync.bind(this));
+ this._sync();
+
+ this._source = null;
+ this._perm = null;
+ this._createPermission();
+ }
+
+ async _createPermission() {
+ try {
+ this._perm = await Polkit.Permission.new('org.freedesktop.bolt.enroll', null, null);
+ } catch (e) {
+ log('Failed to get PolKit permission: %s'.format(e.toString()));
+ }
+ }
+
+ _onDestroy() {
+ this._robot.close();
+ this._client.close();
+ }
+
+ _ensureSource() {
+ if (!this._source) {
+ this._source = new MessageTray.Source(_("Thunderbolt"),
+ 'thunderbolt-symbolic');
+ this._source.connect('destroy', () => (this._source = null));
+
+ Main.messageTray.add(this._source);
+ }
+
+ return this._source;
+ }
+
+ _notify(title, body) {
+ if (this._notification)
+ this._notification.destroy();
+
+ let source = this._ensureSource();
+
+ this._notification = new MessageTray.Notification(source, title, body);
+ this._notification.setUrgency(MessageTray.Urgency.HIGH);
+ this._notification.connect('destroy', () => {
+ this._notification = null;
+ });
+ this._notification.connect('activated', () => {
+ let app = Shell.AppSystem.get_default().lookup_app('gnome-thunderbolt-panel.desktop');
+ if (app)
+ app.activate();
+ });
+ this._source.showNotification(this._notification);
+ }
+
+ /* Session callbacks */
+ _sync() {
+ let active = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ this._indicator.visible = active && this._client.probing;
+ }
+
+ /* Bolt.Client callbacks */
+ _onProbing(cli, probing) {
+ if (probing)
+ this._indicator.icon_name = 'thunderbolt-acquiring-symbolic';
+ else
+ this._indicator.icon_name = 'thunderbolt-symbolic';
+
+ this._sync();
+ }
+
+ /* AuthRobot callbacks */
+ _onEnrollDevice(obj, device, policy) {
+ /* only authorize new devices when in an unlocked user session */
+ let unlocked = !Main.sessionMode.isLocked && !Main.sessionMode.isGreeter;
+ /* and if we have the permission to do so, otherwise we trigger a PolKit dialog */
+ let allowed = this._perm && this._perm.allowed;
+
+ let auth = unlocked && allowed;
+ policy[0] = auth;
+
+ log('thunderbolt: [%s] auto enrollment: %s (allowed: %s)'.format(
+ device.Name, auth ? 'yes' : 'no', allowed ? 'yes' : 'no'));
+
+ if (auth)
+ return; /* we are done */
+
+ if (!unlocked) {
+ const title = _("Unknown Thunderbolt device");
+ const body = _("New device has been detected while you were away. Please disconnect and reconnect the device to start using it.");
+ this._notify(title, body);
+ } else {
+ const title = _("Unauthorized Thunderbolt device");
+ const body = _("New device has been detected and needs to be authorized by an administrator.");
+ this._notify(title, body);
+ }
+ }
+
+ _onEnrollFailed(obj, device, error) {
+ const title = _("Thunderbolt authorization error");
+ const body = _("Could not authorize the Thunderbolt device: %s").format(error.message);
+ this._notify(title, body);
+ }
+});
diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js
new file mode 100644
index 0000000..7b50658
--- /dev/null
+++ b/js/ui/status/volume.js
@@ -0,0 +1,430 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported Indicator */
+
+const { Clutter, Gio, GLib, GObject, Gvc, St } = imports.gi;
+const Signals = imports.signals;
+
+const Main = imports.ui.main;
+const PanelMenu = imports.ui.panelMenu;
+const PopupMenu = imports.ui.popupMenu;
+const Slider = imports.ui.slider;
+
+const ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent';
+
+// Each Gvc.MixerControl is a connection to PulseAudio,
+// so it's better to make it a singleton
+let _mixerControl;
+function getMixerControl() {
+ if (_mixerControl)
+ return _mixerControl;
+
+ _mixerControl = new Gvc.MixerControl({ name: 'GNOME Shell Volume Control' });
+ _mixerControl.open();
+
+ return _mixerControl;
+}
+
+var StreamSlider = class {
+ constructor(control) {
+ this._control = control;
+
+ this.item = new PopupMenu.PopupBaseMenuItem({ activate: false });
+
+ this._inDrag = false;
+ this._notifyVolumeChangeId = 0;
+
+ this._slider = new Slider.Slider(0);
+
+ this._soundSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.sound' });
+ this._soundSettings.connect('changed::%s'.format(ALLOW_AMPLIFIED_VOLUME_KEY), this._amplifySettingsChanged.bind(this));
+ this._amplifySettingsChanged();
+
+ this._sliderChangedId = this._slider.connect('notify::value',
+ this._sliderChanged.bind(this));
+ this._slider.connect('drag-begin', () => (this._inDrag = true));
+ this._slider.connect('drag-end', () => {
+ this._inDrag = false;
+ this._notifyVolumeChange();
+ });
+
+ this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
+ this.item.add(this._icon);
+ this.item.add_child(this._slider);
+ this.item.connect('button-press-event', (actor, event) => {
+ return this._slider.startDragging(event);
+ });
+ this.item.connect('key-press-event', (actor, event) => {
+ return this._slider.emit('key-press-event', event);
+ });
+ this.item.connect('scroll-event', (actor, event) => {
+ return this._slider.emit('scroll-event', event);
+ });
+
+ this._stream = null;
+ this._volumeCancellable = null;
+ this._icons = [];
+ }
+
+ get stream() {
+ return this._stream;
+ }
+
+ set stream(stream) {
+ if (this._stream)
+ this._disconnectStream(this._stream);
+
+ this._stream = stream;
+
+ if (this._stream) {
+ this._connectStream(this._stream);
+ this._updateVolume();
+ } else {
+ this.emit('stream-updated');
+ }
+
+ this._updateVisibility();
+ }
+
+ _disconnectStream(stream) {
+ stream.disconnect(this._mutedChangedId);
+ this._mutedChangedId = 0;
+ stream.disconnect(this._volumeChangedId);
+ this._volumeChangedId = 0;
+ }
+
+ _connectStream(stream) {
+ this._mutedChangedId = stream.connect('notify::is-muted', this._updateVolume.bind(this));
+ this._volumeChangedId = stream.connect('notify::volume', this._updateVolume.bind(this));
+ }
+
+ _shouldBeVisible() {
+ return this._stream != null;
+ }
+
+ _updateVisibility() {
+ let visible = this._shouldBeVisible();
+ this.item.visible = visible;
+ }
+
+ scroll(event) {
+ return this._slider.scroll(event);
+ }
+
+ _sliderChanged() {
+ if (!this._stream)
+ return;
+
+ let value = this._slider.value;
+ let volume = value * this._control.get_vol_max_norm();
+ let prevMuted = this._stream.is_muted;
+ let prevVolume = this._stream.volume;
+ if (volume < 1) {
+ this._stream.volume = 0;
+ if (!prevMuted)
+ this._stream.change_is_muted(true);
+ } else {
+ this._stream.volume = volume;
+ if (prevMuted)
+ this._stream.change_is_muted(false);
+ }
+ this._stream.push_volume();
+
+ let volumeChanged = this._stream.volume !== prevVolume;
+ if (volumeChanged && !this._notifyVolumeChangeId && !this._inDrag) {
+ this._notifyVolumeChangeId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 30, () => {
+ this._notifyVolumeChange();
+ this._notifyVolumeChangeId = 0;
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._notifyVolumeChangeId,
+ '[gnome-shell] this._notifyVolumeChangeId');
+ }
+ }
+
+ _notifyVolumeChange() {
+ if (this._volumeCancellable)
+ this._volumeCancellable.cancel();
+ this._volumeCancellable = null;
+
+ if (this._stream.state === Gvc.MixerStreamState.RUNNING)
+ return; // feedback not necessary while playing
+
+ this._volumeCancellable = new Gio.Cancellable();
+ let player = global.display.get_sound_player();
+ player.play_from_theme('audio-volume-change',
+ _("Volume changed"),
+ this._volumeCancellable);
+ }
+
+ _changeSlider(value) {
+ this._slider.block_signal_handler(this._sliderChangedId);
+ this._slider.value = value;
+ this._slider.unblock_signal_handler(this._sliderChangedId);
+ }
+
+ _updateVolume() {
+ let muted = this._stream.is_muted;
+ this._changeSlider(muted
+ ? 0 : this._stream.volume / this._control.get_vol_max_norm());
+ this.emit('stream-updated');
+ }
+
+ _amplifySettingsChanged() {
+ this._allowAmplified = this._soundSettings.get_boolean(ALLOW_AMPLIFIED_VOLUME_KEY);
+
+ this._slider.maximum_value = this._allowAmplified
+ ? this.getMaxLevel() : 1;
+
+ if (this._stream)
+ this._updateVolume();
+ }
+
+ getIcon() {
+ if (!this._stream)
+ return null;
+
+ let volume = this._stream.volume;
+ let n;
+ if (this._stream.is_muted || volume <= 0) {
+ n = 0;
+ } else {
+ n = Math.ceil(3 * volume / this._control.get_vol_max_norm());
+ n = Math.clamp(n, 1, this._icons.length - 1);
+ }
+ return this._icons[n];
+ }
+
+ getLevel() {
+ if (!this._stream)
+ return null;
+
+ return this._stream.volume / this._control.get_vol_max_norm();
+ }
+
+ getMaxLevel() {
+ let maxVolume = this._control.get_vol_max_norm();
+ if (this._allowAmplified)
+ maxVolume = this._control.get_vol_max_amplified();
+
+ return maxVolume / this._control.get_vol_max_norm();
+ }
+};
+Signals.addSignalMethods(StreamSlider.prototype);
+
+var OutputStreamSlider = class extends StreamSlider {
+ constructor(control) {
+ super(control);
+ this._slider.accessible_name = _("Volume");
+ this._icons = [
+ 'audio-volume-muted-symbolic',
+ 'audio-volume-low-symbolic',
+ 'audio-volume-medium-symbolic',
+ 'audio-volume-high-symbolic',
+ 'audio-volume-overamplified-symbolic',
+ ];
+ }
+
+ _connectStream(stream) {
+ super._connectStream(stream);
+ this._portChangedId = stream.connect('notify::port', this._portChanged.bind(this));
+ this._portChanged();
+ }
+
+ _findHeadphones(sink) {
+ // This only works for external headphones (e.g. bluetooth)
+ if (sink.get_form_factor() == 'headset' ||
+ sink.get_form_factor() == 'headphone')
+ return true;
+
+ // a bit hackish, but ALSA/PulseAudio have a number
+ // of different identifiers for headphones, and I could
+ // not find the complete list
+ if (sink.get_ports().length > 0)
+ return sink.get_port().port.includes('headphone');
+
+ return false;
+ }
+
+ _disconnectStream(stream) {
+ super._disconnectStream(stream);
+ stream.disconnect(this._portChangedId);
+ this._portChangedId = 0;
+ }
+
+ _updateSliderIcon() {
+ this._icon.icon_name = this._hasHeadphones
+ ? 'audio-headphones-symbolic'
+ : 'audio-speakers-symbolic';
+ }
+
+ _portChanged() {
+ let hasHeadphones = this._findHeadphones(this._stream);
+ if (hasHeadphones != this._hasHeadphones) {
+ this._hasHeadphones = hasHeadphones;
+ this._updateSliderIcon();
+ }
+ }
+};
+
+var InputStreamSlider = class extends StreamSlider {
+ constructor(control) {
+ super(control);
+ this._slider.accessible_name = _("Microphone");
+ this._control.connect('stream-added', this._maybeShowInput.bind(this));
+ this._control.connect('stream-removed', this._maybeShowInput.bind(this));
+ this._icon.icon_name = 'audio-input-microphone-symbolic';
+ this._icons = [
+ 'microphone-sensitivity-muted-symbolic',
+ 'microphone-sensitivity-low-symbolic',
+ 'microphone-sensitivity-medium-symbolic',
+ 'microphone-sensitivity-high-symbolic',
+ ];
+ }
+
+ _connectStream(stream) {
+ super._connectStream(stream);
+ this._maybeShowInput();
+ }
+
+ _maybeShowInput() {
+ // only show input widgets if any application is recording audio
+ let showInput = false;
+ if (this._stream) {
+ // skip gnome-volume-control and pavucontrol which appear
+ // as recording because they show the input level
+ let skippedApps = [
+ 'org.gnome.VolumeControl',
+ 'org.PulseAudio.pavucontrol',
+ ];
+
+ showInput = this._control.get_source_outputs().some(output => {
+ return !skippedApps.includes(output.get_application_id());
+ });
+ }
+
+ this._showInput = showInput;
+ this._updateVisibility();
+ }
+
+ _shouldBeVisible() {
+ return super._shouldBeVisible() && this._showInput;
+ }
+};
+
+var VolumeMenu = class extends PopupMenu.PopupMenuSection {
+ constructor(control) {
+ super();
+
+ this.hasHeadphones = false;
+
+ this._control = control;
+ this._control.connect('state-changed', this._onControlStateChanged.bind(this));
+ this._control.connect('default-sink-changed', this._readOutput.bind(this));
+ this._control.connect('default-source-changed', this._readInput.bind(this));
+
+ this._output = new OutputStreamSlider(this._control);
+ this._output.connect('stream-updated', () => {
+ this.emit('output-icon-changed');
+ });
+ this.addMenuItem(this._output.item);
+
+ this._input = new InputStreamSlider(this._control);
+ this._input.item.connect('notify::visible', () => {
+ this.emit('input-visible-changed');
+ });
+ this._input.connect('stream-updated', () => {
+ this.emit('input-icon-changed');
+ });
+ this.addMenuItem(this._input.item);
+
+ this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._onControlStateChanged();
+ }
+
+ scroll(event) {
+ return this._output.scroll(event);
+ }
+
+ _onControlStateChanged() {
+ if (this._control.get_state() == Gvc.MixerControlState.READY) {
+ this._readInput();
+ this._readOutput();
+ } else {
+ this.emit('output-icon-changed');
+ }
+ }
+
+ _readOutput() {
+ this._output.stream = this._control.get_default_sink();
+ }
+
+ _readInput() {
+ this._input.stream = this._control.get_default_source();
+ }
+
+ getOutputIcon() {
+ return this._output.getIcon();
+ }
+
+ getInputIcon() {
+ return this._input.getIcon();
+ }
+
+ getLevel() {
+ return this._output.getLevel();
+ }
+
+ getMaxLevel() {
+ return this._output.getMaxLevel();
+ }
+
+ getInputVisible() {
+ return this._input.item.visible;
+ }
+};
+
+var Indicator = GObject.registerClass(
+class Indicator extends PanelMenu.SystemIndicator {
+ _init() {
+ super._init();
+
+ this._primaryIndicator = this._addIndicator();
+ this._inputIndicator = this._addIndicator();
+
+ this._control = getMixerControl();
+ this._volumeMenu = new VolumeMenu(this._control);
+ this._volumeMenu.connect('output-icon-changed', () => {
+ let icon = this._volumeMenu.getOutputIcon();
+
+ if (icon != null)
+ this._primaryIndicator.icon_name = icon;
+ this._primaryIndicator.visible = icon !== null;
+ });
+
+ this._inputIndicator.visible = this._volumeMenu.getInputVisible();
+ this._volumeMenu.connect('input-visible-changed', () => {
+ this._inputIndicator.visible = this._volumeMenu.getInputVisible();
+ });
+ this._volumeMenu.connect('input-icon-changed', () => {
+ let icon = this._volumeMenu.getInputIcon();
+
+ if (icon !== null)
+ this._inputIndicator.icon_name = icon;
+ });
+
+ this.menu.addMenuItem(this._volumeMenu);
+ }
+
+ vfunc_scroll_event() {
+ let result = this._volumeMenu.scroll(Clutter.get_current_event());
+ if (result == Clutter.EVENT_PROPAGATE || this.menu.actor.mapped)
+ return result;
+
+ let gicon = new Gio.ThemedIcon({ name: this._volumeMenu.getOutputIcon() });
+ let level = this._volumeMenu.getLevel();
+ let maxLevel = this._volumeMenu.getMaxLevel();
+ Main.osdWindowManager.show(-1, gicon, null, level, maxLevel);
+ return result;
+ }
+});