gnome-shell/js/ui/status/network.js

2196 lines
71 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported NMApplet */
const { Clutter, Gio, GLib, GObject, Meta, NM, Polkit, St } = imports.gi;
const Signals = imports.misc.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;
const {registerDestroyableType} = imports.misc.signalTracker;
Gio._promisify(Gio.DBusConnection.prototype, 'call');
Gio._promisify(NM.Client, 'new_async');
Gio._promisify(NM.Client.prototype, 'check_connectivity_async');
const NMConnectionCategory = {
INVALID: 'invalid',
WIRED: 'wired',
WIRELESS: 'wireless',
WWAN: 'wwan',
VPN: 'vpn',
};
var MAX_DEVICE_ITEMS = 4;
// small optimization, to avoid using [] all the time
const NM80211Mode = NM['80211Mode'];
var PortalHelperResult = {
CANCELLED: 0,
COMPLETED: 1,
RECHECK: 2,
};
const PortalHelperIface = loadInterfaceXML('org.gnome.Shell.PortalHelper');
const PortalHelperInfo = Gio.DBusInterfaceInfo.new_for_xml(PortalHelperIface);
function signalToIcon(value) {
if (value < 20)
return 'none';
else if (value < 40)
return 'weak';
else if (value < 50)
return 'ok';
else if (value < 80)
return 'good';
else
return 'excellent';
}
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${global.get_current_time()}`),
};
try {
Gio.DBus.session.call(
'org.gnome.Settings',
'/org/gnome/Settings',
'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: ${e.message}`);
}
}
var NMConnectionItem = class extends Signals.EventEmitter {
constructor(section, connection) {
super();
this._section = section;
this._connection = connection;
this._activeConnection = null;
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._activeConnection?.disconnectObject(this);
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) {
this._activeConnection?.disconnectObject(this);
this._activeConnection = activeConnection;
this._activeConnection?.connectObject('notify::state',
this._connectionStateChanged.bind(this), this);
this._sync();
}
};
var NMConnectionSection = class NMConnectionSection extends Signals.EventEmitter {
constructor(client) {
super();
if (this.constructor === NMConnectionSection)
throw new TypeError(`Cannot instantiate abstract type ${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._client.connectObject('notify::connectivity',
this._iconChanged.bind(this), this);
}
destroy() {
this._client.disconnectObject(this);
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', () => this.emit('activation-failed'));
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();
}
};
var NMDeviceItem = class NMDeviceItem extends NMConnectionSection {
constructor(client, device) {
super(client);
if (this.constructor === NMDeviceItem)
throw new TypeError(`Cannot instantiate abstract type ${this.constructor.name}`);
this._device = device;
this._deviceName = '';
this._autoConnectItem = this.item.menu.addAction(_("Connect"), this._autoConnect.bind(this));
this._deactivateItem = this._radioSection.addAction(_("Turn Off"), this.deactivateConnection.bind(this));
this._client.connectObject(
'notify::primary-connection', () => this._iconChanged(),
this);
this._device.connectObject(
'state-changed', this._deviceStateChanged.bind(this),
'notify::active-connection', this._activeConnectionChanged.bind(this),
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() {
this._device.disconnectObject(this);
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');
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);
}
setDeviceName(name) {
this._deviceName = name;
this._sync();
}
_getDescription() {
return this._deviceName;
}
_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 ${this._device.state}`);
return 'invalid';
}
}
};
var NMWiredDeviceItem = class extends NMDeviceItem {
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 NMModemDeviceItem = class extends NMDeviceItem {
constructor(client, device) {
super(client, device);
const settingsPanel = this._useWwanPanel()
? 'gnome-wwan-panel.desktop'
: 'gnome-network-panel.desktop';
this.item.menu.addSettingsAction(_('Mobile Broadband Settings'), settingsPanel);
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);
this._mobileDevice?.connectObject(
'notify::operator-name', this._sync.bind(this),
'notify::signal-quality', () => this._iconChanged(), this);
Main.sessionMode.connectObject('updated',
this._sessionUpdated.bind(this), this);
this._sessionUpdated();
}
get category() {
return NMConnectionCategory.WWAN;
}
_useWwanPanel() {
// Currently, wwan panel doesn't support CDMA_EVDO modems
const supportedCaps =
NM.DeviceModemCapabilities.GSM_UMTS |
NM.DeviceModemCapabilities.LTE;
return this._device.current_capabilities & supportedCaps;
}
_autoConnect() {
if (this._useWwanPanel())
launchSettingsPanel('wwan', 'show-device', this._device.udi);
else
launchSettingsPanel('network', 'connect-3g', this._device.get_path());
}
_sessionUpdated() {
this._autoConnectItem.sensitive = Main.sessionMode.hasWindows;
}
destroy() {
this._mobileDevice?.disconnectObject(this);
Main.sessionMode.disconnectObject(this);
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();
}
_getMenuIcon() {
if (!this._device.active_connection)
return 'network-cellular-disabled-symbolic';
return this.getIndicatorIcon();
}
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-${signalToIcon(this._mobileDevice.signal_quality)}-symbolic`;
}
};
var NMBluetoothDeviceItem = class extends NMDeviceItem {
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");
}
_getMenuIcon() {
if (!this._device.active_connection)
return 'network-cellular-disabled-symbolic';
return this.getIndicatorIcon();
}
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';
}
}
};
const WirelessNetwork = GObject.registerClass({
Properties: {
'name': GObject.ParamSpec.string(
'name', '', '',
GObject.ParamFlags.READABLE,
''),
'icon-name': GObject.ParamSpec.string(
'icon-name', '', '',
GObject.ParamFlags.READABLE,
''),
'secure': GObject.ParamSpec.boolean(
'secure', '', '',
GObject.ParamFlags.READABLE,
false),
'is-active': GObject.ParamSpec.boolean(
'is-active', '', '',
GObject.ParamFlags.READABLE,
false),
},
Signals: {
'destroy': {},
},
}, class WirelessNetwork extends GObject.Object {
static _securityTypes =
Object.values(NM.UtilsSecurityType).sort((a, b) => b - a);
_init(device) {
super._init();
this._device = device;
this._device.connectObject(
'notify::active-access-point', () => this.notify('is-active'),
this);
this._accessPoints = new Set();
this._connections = [];
this._name = '';
this._ssid = null;
this._bestAp = null;
this._mode = 0;
this._securityType = NM.UtilsSecurityType.NONE;
}
get _strength() {
return this._bestAp?.strength ?? 0;
}
get name() {
return this._name;
}
get icon_name() {
if (this._mode === NM80211Mode.ADHOC)
return 'network-workgroup-symbolic';
if (!this._bestAp)
return '';
return `network-wireless-signal-${signalToIcon(this._bestAp.strength)}-symbolic`;
}
get secure() {
return this._securityType !== NM.UtilsSecurityType.NONE;
}
get is_active() {
return this._accessPoints.has(this._device.activeAccessPoint);
}
hasAccessPoint(ap) {
return this._accessPoints.has(ap);
}
hasAccessPoints() {
return this._accessPoints.size > 0;
}
checkAccessPoint(ap) {
if (!ap.get_ssid())
return false;
const secType = this._getApSecurityType(ap);
if (secType === NM.UtilsSecurityType.INVALID)
return false;
if (this._accessPoints.size === 0)
return true;
return this._ssid.equal(ap.ssid) &&
this._mode === ap.mode &&
this._securityType === secType;
}
/**
* @param {NM.AccessPoint} ap - an access point
* @returns {bool} - whether the access point was added
*/
addAccessPoint(ap) {
if (!this.checkAccessPoint(ap))
return false;
if (this._accessPoints.size === 0) {
this._ssid = ap.get_ssid();
this._mode = ap.mode;
this._securityType = this._getApSecurityType(ap);
this._name = NM.utils_ssid_to_utf8(this._ssid.get_data()) || '<unknown>';
this.notify('name');
this.notify('secure');
}
const wasActive = this.is_active;
this._accessPoints.add(ap);
ap.connectObject(
'notify::strength', () => this._updateBestAp(),
this);
this._updateBestAp();
if (wasActive !== this.is_active)
this.notify('is-active');
return true;
}
/**
* @param {NM.AccessPoint} ap - an access point
* @returns {bool} - whether the access point was removed
*/
removeAccessPoint(ap) {
const wasActive = this.is_active;
if (!this._accessPoints.delete(ap))
return false;
this._updateBestAp();
if (wasActive !== this.is_active)
this.notify('is-active');
return true;
}
/**
* @param {WirelessNetwork} other - network to compare with
* @returns {number} - the sort order
*/
compare(other) {
// place known connections first
const cmpConnections = other.hasConnections() - this.hasConnections();
if (cmpConnections !== 0)
return cmpConnections;
const cmpAps = other.hasAccessPoints() - this.hasAccessPoints();
if (cmpAps !== 0)
return cmpAps;
// place stronger connections first
const cmpStrength = other._strength - this._strength;
if (cmpStrength !== 0)
return cmpStrength;
// place secure connections first
const cmpSec = other.secure - this.secure;
if (cmpSec !== 0)
return cmpSec;
// sort alphabetically
return GLib.utf8_collate(this._name, other._name);
}
hasConnections() {
return this._connections.length > 0;
}
checkConnections(connections) {
const aps = [...this._accessPoints];
this._connections = connections.filter(
c => aps.some(ap => ap.connection_valid(c)));
}
canAutoconnect() {
const canAutoconnect =
this._securityTypes !== NM.UtilsSecurityType.WPA_ENTERPRISE &&
this._securityTypes !== NM.UtilsSecurityType.WPA2_ENTERPRISE;
return canAutoconnect;
}
activate() {
const [ap] = this._accessPoints;
let [conn] = this._connections;
if (conn) {
this._device.client.activate_connection_async(conn, this._device, null, null, null);
} else if (!this.canAutoconnect()) {
launchSettingsPanel('wifi', 'connect-8021x-wifi',
this._getDeviceDBusPath(), ap.get_path());
} else {
conn = new NM.SimpleConnection();
this._device.client.add_and_activate_connection_async(
conn, this._device, ap.get_path(), null, null);
}
}
destroy() {
this.emit('destroy');
}
_getDeviceDBusPath() {
// nm_object_get_path() is shadowed by nm_device_get_path()
return NM.Object.prototype.get_path.call(this._device);
}
_getApSecurityType(ap) {
const {wirelessCapabilities: caps} = this._device;
const {flags, wpaFlags, rsnFlags} = ap;
const haveAp = true;
const adHoc = ap.mode === NM80211Mode.ADHOC;
const bestType = WirelessNetwork._securityTypes
.find(t => NM.utils_security_valid(t, caps, haveAp, adHoc, flags, wpaFlags, rsnFlags));
return bestType ?? NM.UtilsSecurityType.INVALID;
}
_updateBestAp() {
const [bestAp] =
[...this._accessPoints].sort((a, b) => b.strength - a.strength);
if (this._bestAp === bestAp)
return;
this._bestAp = bestAp;
this.notify('icon-name');
}
});
registerDestroyableType(WirelessNetwork);
var NMWirelessDialogItem = GObject.registerClass({
Signals: {
'selected': {},
},
}, class NMWirelessDialogItem extends St.BoxLayout {
_init(network) {
this._network = network;
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);
this._label = new St.Label({
x_expand: true,
});
this.label_actor = this._label;
this.add_child(this._label);
this._selectedIcon = new St.Icon({
style_class: 'nm-dialog-icon nm-dialog-network-selected',
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',
});
this._icons.add_actor(this._secureIcon);
this._signalIcon = new St.Icon({
style_class: 'nm-dialog-icon',
});
this._icons.add_actor(this._signalIcon);
this._network.bind_property('icon-name',
this._signalIcon, 'icon-name',
GObject.BindingFlags.SYNC_CREATE);
this._network.bind_property('name',
this._label, 'text',
GObject.BindingFlags.SYNC_CREATE);
this._network.bind_property('is-active',
this._selectedIcon, 'visible',
GObject.BindingFlags.SYNC_CREATE);
this._network.bind_property_full('secure',
this._secureIcon, 'icon-name',
GObject.BindingFlags.SYNC_CREATE,
(bind, source) => [true, source ? 'network-wireless-encrypted-symbolic' : ''],
null);
}
get network() {
return this._network;
}
vfunc_key_focus_in() {
this.emit('selected');
}
});
var NMWirelessDialog = GObject.registerClass(
class NMWirelessDialog extends ModalDialog.ModalDialog {
_init(client, device) {
super._init({ styleClass: 'nm-dialog' });
this._client = client;
this._device = device;
this._client.connectObject('notify::wireless-enabled',
this._syncView.bind(this), this);
this._rfkill = Rfkill.getRfkillManager();
this._rfkill.connectObject(
'notify::airplane-mode', () => this._syncView(),
'notify::hw-airplane-mode', () => this._syncView(),
this);
this._networkItems = new Map();
this._buildLayout();
let connections = client.get_connections();
this._connections = connections.filter(
connection => device.connection_valid(connection));
device.connectObject(
'notify::active-access-point', () => this._updateSensitivity(),
'notify::available-connections', () => this._availableConnectionsChanged(),
'access-point-added', (d, ap) => {
this._addAccessPoint(ap);
this._syncNetworksList();
this._syncView();
},
'access-point-removed', (d, ap) => {
this._removeAccessPoint(ap);
this._syncNetworksList();
this._syncView();
},
this);
for (const ap of this._device.get_access_points())
this._addAccessPoint(ap);
this._selectedNetwork = null;
this._availableConnectionsChanged();
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._scanTimeoutId) {
GLib.source_remove(this._scanTimeoutId);
this._scanTimeoutId = 0;
}
if (this._syncVisibilityId) {
Meta.later_remove(this._syncVisibilityId);
this._syncVisibilityId = 0;
}
}
_onScanTimeout() {
this._device.request_scan_async(null, null);
return GLib.SOURCE_CONTINUE;
}
_availableConnectionsChanged() {
const connections = this._device.get_available_connections();
for (const net of this._networkItems.keys())
net.checkConnections(connections);
this._syncNetworksList();
}
_updateSensitivity() {
const connectSensitive =
this._client.wireless_enabled && this._selectedNetwork && !this._selectedNetwork.isActive;
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._networkItems.size === 0;
}
if (this._noNetworksBox.visible)
this._noNetworksSpinner.play();
else
this._noNetworksSpinner.stop();
}
_buildLayout() {
let headline = new St.BoxLayout({ style_class: 'nm-dialog-header-hbox' });
const icon = new St.Icon({
style_class: 'nm-dialog-header-icon',
icon_name: 'network-wireless-symbolic',
});
let titleBox = new St.BoxLayout({ vertical: true });
const title = new St.Label({
style_class: 'nm-dialog-header',
text: _('Wi-Fi Networks'),
});
const 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 });
2015-01-15 19:39:52 -05:00
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() {
this._selectedNetwork?.activate();
this.close();
}
_addAccessPoint(ap) {
if (ap.get_ssid() == null) {
// This access point is not visible yet
// Wait for it to get a ssid
ap.connectObject('notify::ssid', () => {
if (!ap.ssid)
return;
ap.disconnectObject(this);
this._addAccessPoint(ap);
}, this);
return;
}
let network = [...this._networkItems.keys()]
.find(n => n.checkAccessPoint(ap));
if (!network) {
network = new WirelessNetwork(this._device);
network.connectObject(
'notify::icon-name', () => this._syncNetworksList(),
'notify::is-active', () => this._syncNetworksList(),
this);
const item = this._createNetworkItem(network);
this._itemBox.add_child(item);
this._networkItems.set(network, item);
}
network.addAccessPoint(ap);
}
_removeAccessPoint(ap) {
const network = [...this._networkItems.keys()]
.find(n => n.removeAccessPoint(ap));
if (!network || network.hasAccessPoints())
return;
this._networkItems.get(network)?.destroy();
this._networkItems.delete(network);
network.destroy();
}
_syncNetworksList() {
const {hasWindows} = Main.sessionMode;
const sortedItems = [...this._networkItems.values()]
.sort((one, two) => one.network.compare(two.network));
for (const [index, item] of sortedItems.entries())
this._itemBox.set_child_at_index(item, index);
for (const [net, item] of this._networkItems) {
item.visible =
hasWindows || net.hasConnections() || net.canAutoconnect();
}
}
_selectNetwork(network) {
this._networkItems.get(this._selectedNetwork)?.remove_style_pseudo_class('selected');
this._selectedNetwork = network;
this._updateSensitivity();
this._networkItems.get(this._selectedNetwork)?.add_style_pseudo_class('selected');
}
_createNetworkItem(network) {
const item = new NMWirelessDialogItem(network);
item.connect('selected', () => {
Util.ensureActorVisibleInScrollView(this._scrollView, item);
this._selectNetwork(network);
});
item.connect('destroy', () => {
let keyFocus = global.stage.key_focus;
if (keyFocus && keyFocus.contains(item))
this._itemBox.grab_key_focus();
});
return item;
}
});
var NMWirelessDeviceItem = class extends Signals.EventEmitter {
constructor(client, device) {
super();
this._client = client;
this._device = device;
this._deviceName = '';
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._client.connectObject(
'notify::wireless-enabled', this._sync.bind(this),
'notify::wireless-hardware-enabled', this._sync.bind(this),
'notify::connectivity', () => this._iconChanged(),
'notify::primary-connection', () => this._iconChanged(),
this);
this._device.connectObject(
'notify::active-access-point', this._activeApChanged.bind(this),
'state-changed', this._deviceStateChanged.bind(this), this);
this._activeApChanged();
this._sync();
}
get category() {
return NMConnectionCategory.WIRELESS;
}
_iconChanged() {
this._sync();
this.emit('icon-changed');
}
destroy() {
this._device.disconnectObject(this);
this._activeAccessPoint?.disconnectObject(this);
this._client.disconnectObject(this);
if (this._dialog) {
this._dialog.destroy();
this._dialog = null;
}
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');
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;
}
_activeApChanged() {
this._activeAccessPoint?.disconnectObject(this);
this._activeAccessPoint = this._device.active_access_point;
this._activeAccessPoint?.connectObject(
'notify::strength', () => this._iconChanged(),
this);
this._iconChanged();
}
_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();
}
setDeviceName(name) {
this._deviceName = name;
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._deviceName);
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._deviceName);
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._deviceName);
else if (!this._client.wireless_enabled)
/* Translators: %s is a network identifier */
return _('%s Off').format(this._deviceName);
else if (this._device.state == NM.DeviceState.DISCONNECTED)
/* Translators: %s is a network identifier */
return _('%s Not Connected').format(this._deviceName);
else
return '';
}
_getMenuIcon() {
if (!this._client.wireless_enabled)
return 'network-wireless-disabled-symbolic';
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-${signalToIcon(ap.strength)}-symbolic`;
else
return 'network-wireless-no-route-symbolic';
}
};
var NMWireguardItem = class extends NMConnectionItem {
_buildUI() {
this.labelItem = new PopupMenu.PopupMenuItem('');
this.labelItem.connect('activate', this._activate.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.state) {
case NM.ActiveConnectionState.UNKNOWN:
return _('unknown');
case NM.ActiveConnectionState.ACTIVATING:
return _('activating…');
case NM.ActiveConnectionState.ACTIVATED:
return null;
case NM.ActiveConnectionState.DEACTIVATING:
return _('deactivating…');
case NM.ActiveConnectionState.DEACTIVATED:
return _('deactivated');
default:
return 'invalid';
}
}
_connectionStateChanged() {
const state = this._activeConnection?.get_state();
const reason = this._activeConnection?.get_state_reason();
if (state === NM.ActiveConnectionState.DEACTIVATED &&
reason !== NM.ActiveConnectionStateReason.NO_SECRETS &&
reason !== NM.ActiveConnectionStateReason.USER_DISCONNECTED)
this.emit('activation-failed');
this.emit('icon-changed');
super._connectionStateChanged();
}
getIndicatorIcon() {
if (this._activeConnection) {
if (this._activeConnection.state === NM.ActiveConnectionState.UNKNOWN)
return '';
else if (this._activeConnection.state < NM.ActiveConnectionState.ACTIVATED)
return 'network-vpn-acquiring-symbolic';
else
return 'network-vpn-symbolic';
} else {
return '';
}
}
};
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)
this.emit('activation-failed');
this.emit('icon-changed');
super._connectionStateChanged();
}
setActiveConnection(activeConnection) {
this._activeConnection?.disconnectObject(this);
this._activeConnection = activeConnection;
this._activeConnection?.connectObject('vpn-state-changed',
this._connectionStateChanged.bind(this), 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-disabled-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) {
if (connection.get_connection_type() === 'wireguard')
return new NMWireguardItem(this, 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 '';
}
};
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] = NMWiredDeviceItem;
this._dtypes[NM.DeviceType.WIFI] = NMWirelessDeviceItem;
this._dtypes[NM.DeviceType.MODEM] = NMModemDeviceItem;
this._dtypes[NM.DeviceType.BT] = NMBluetoothDeviceItem;
// 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._ctypes[NM.SETTING_WIREGUARD_SETTING_NAME] = NMConnectionCategory.VPN;
this._getClient().catch(logError);
}
async _getClient() {
this._client = await NM.Client.new_async(null);
this._activeConnections = [];
this._connections = [];
this._connectivityQueue = new Set();
this._mainConnection = null;
this._notification = null;
this._nmDevices = [];
this._deviceSections = new Map();
const categories = [
NMConnectionCategory.WIRED,
NMConnectionCategory.WIRELESS,
NMConnectionCategory.WWAN,
];
for (let category of categories) {
this._deviceSections.set(category, new DeviceCategory(category));
this.menu.addMenuItem(this._deviceSections.get(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._syncMainConnection();
this._syncVpnConnections();
this._client.bind_property('nm-running',
this, 'visible',
GObject.BindingFlags.SYNC_CREATE);
this._client.bind_property('networking-enabled',
this.menu.actor, 'visible',
GObject.BindingFlags.SYNC_CREATE);
this._client.connectObject(
'notify::state', () => this._updateIcon(),
'notify::primary-connection', () => this._syncMainConnection(),
'notify::activating-connection', () => this._syncMainConnection(),
'notify::active-connections', () => this._syncVpnConnections(),
'notify::connectivity', () => this._syncConnectivity(),
'device-added', this._deviceAdded.bind(this),
'device-removed', this._deviceRemoved.bind(this),
'connection-added', this._connectionAdded.bind(this),
'connection-removed', this._connectionRemoved.bind(this),
this);
try {
this._configPermission = await Polkit.Permission.new(
'org.freedesktop.NetworkManager.network-control', null, null);
} catch (e) {
log(`No permission to control network connections: ${e}`);
this._configPermission = null;
}
Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
this._sessionUpdated();
}
_sessionUpdated() {
const sensitive =
!Main.sessionMode.isLocked &&
this._configPermission && this._configPermission.allowed;
this.menu.setSensitive(sensitive);
}
_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 ${devices[i]}: ${e}`);
}
}
this._syncDeviceNames();
}
_onActivationFailed() {
this._notification?.destroy();
const source = new MessageTray.Source(
_('Network Manager'), 'network-error-symbolic');
source.policy =
new MessageTray.NotificationApplicationPolicy('gnome-network-panel');
this._notification = new MessageTray.Notification(source,
_('Connection failed'),
_('Activation of network connection failed'));
this._notification.setUrgency(MessageTray.Urgency.HIGH);
this._notification.setTransient(true);
this._notification.connect('destroy',
() => (this._notification = null));
Main.messageTray.add(source);
source.showNotification(this._notification);
}
_syncDeviceNames() {
let names = NM.Device.disambiguate_names(this._nmDevices);
for (let i = 0; i < this._nmDevices.length; i++) {
let device = this._nmDevices[i];
let name = names[i];
if (device._delegate)
device._delegate.setDeviceName(name);
}
}
_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.connectObject('activation-failed',
this._onActivationFailed.bind(this), this);
const {section} = this._deviceSections.get(wrapper.category);
section.addMenuItem(wrapper.item);
const {devices} = this._deviceSections.get(wrapper.category);
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.disconnectObject(this);
wrapper.destroy();
const {devices} = this._deviceSections.get(wrapper.category);
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() {
this._mainConnection?._primaryDevice?.disconnectObject(this);
this._mainConnection?.disconnectObject(this);
this._mainConnection = this._getMainConnection();
if (this._mainConnection) {
this._mainConnection._primaryDevice?.connectObject('icon-changed',
this._updateIcon.bind(this), this);
this._mainConnection.connectObject('notify::state',
this._mainConnectionStateChanged.bind(this), this);
this._mainConnectionStateChanged();
}
this._updateIcon();
this._syncConnectivity();
}
_syncVpnConnections() {
let activeConnections = this._client.get_active_connections() || [];
let vpnConnections = activeConnections.filter(
a => a instanceof NM.VpnConnection || a.get_connection_type() === 'wireguard');
vpnConnections.forEach(a => {
ensureActiveConnectionProps(a);
});
this._vpnSection.setActiveConnections(vpnConnections);
this._updateIcon();
}
_mainConnectionStateChanged() {
if (this._mainConnection.state === NM.ActiveConnectionState.ACTIVATED)
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 (this._connections.includes(connection)) {
// connection was already seen
return;
}
connection.connectObject('changed',
this._updateConnection.bind(this), 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 {
const {devices} = this._deviceSections.get(section);
for (let i = 0; i < devices.length; i++) {
if (devices[i] instanceof NMConnectionSection)
devices[i].removeConnection(connection);
}
}
connection.disconnectObject(this);
}
_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 {
const {devices} = this._deviceSections.get(section);
devices.forEach(wrapper => {
if (wrapper instanceof NMConnectionSection)
wrapper.checkConnection(connection);
});
}
}
_flushConnectivityQueue() {
for (let item of this._connectivityQueue)
this._portalHelperProxy?.CloseAsync(item);
this._connectivityQueue.clear();
}
_closeConnectivityCheck(path) {
if (this._connectivityQueue.delete(path))
this._portalHelperProxy?.CloseAsync(path);
}
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 they choose 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: ${result}`);
}
}
async _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 ||= this._client.connectivity < NM.ConnectivityState.FULL;
if (!isPortal || Main.sessionMode.isGreeter)
return;
let path = this._mainConnection.get_path();
if (this._connectivityQueue.has(path))
return;
let timestamp = global.get_current_time();
if (!this._portalHelperProxy) {
this._portalHelperProxy = new Gio.DBusProxy({
g_connection: Gio.DBus.session,
g_name: 'org.gnome.Shell.PortalHelper',
g_object_path: '/org/gnome/Shell/PortalHelper',
g_interface_name: PortalHelperInfo.name,
g_interface_info: PortalHelperInfo,
});
this._portalHelperProxy.connectSignal('Done',
() => this._portalHelperDone().catch(logError));
try {
await this._portalHelperProxy.init_async(
GLib.PRIORITY_DEFAULT, null);
} catch (e) {
console.error(`Error launching the portal helper: ${e.message}`);
}
}
this._portalHelperProxy?.AuthenticateAsync(path, '', timestamp).catch(logError);
this._connectivityQueue.add(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;
}
});