diff --git a/files/usr/share/cinnamon/applets/network@cinnamon.org/applet.js b/files/usr/share/cinnamon/applets/network@cinnamon.org/applet.js
new file mode 100644
index 0000000..45a5259
--- /dev/null
+++ b/files/usr/share/cinnamon/applets/network@cinnamon.org/applet.js
@@ -0,0 +1,2734 @@
+const Applet = imports.ui.applet;
+const GLib = imports.gi.GLib;
+const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
+const Lang = imports.lang;
+const NM = imports.gi.NM;
+const Signals = imports.signals;
+const St = imports.gi.St;
+const Mainloop = imports.mainloop;
+const Main = imports.ui.main;
+const PopupMenu = imports.ui.popupMenu;
+const MessageTray = imports.ui.messageTray;
+const ModemManager = imports.misc.modemManager;
+const Util = imports.misc.util;
+const Settings = imports.ui.settings;
+
+const DEFAULT_PERIODIC_UPDATE_FREQUENCY_SECONDS = 10;
+const FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS = 2;
+
+const NMConnectionCategory = {
+ INVALID: 'invalid',
+ WIRED: 'wired',
+ WIRELESS: 'wireless',
+ WWAN: 'wwan',
+ VPN: 'vpn',
+ WIREGUARD: 'wireguard'
+};
+
+const NMAccessPointSecurity = {
+ UNKNOWN: 0,
+ NONE: 1,
+ WEP: 2,
+ WPA_PSK: 3,
+ WPA2_PSK: 4,
+ WPA_ENT: 5,
+ WPA2_ENT: 6
+};
+
+// small optimization, to avoid using [] all the time
+const NM80211Mode = NM['80211Mode'];
+const NM80211ApFlags = NM['80211ApFlags'];
+const NM80211ApSecurityFlags = NM['80211ApSecurityFlags'];
+
+// number of wireless networks that should be visible
+// (the remaining are placed into More...)
+const NUM_VISIBLE_NETWORKS = 5;
+
+var NMIface = '\
+ \
+ \
+ \
+ \
+ \
+ \
+';
+
+var NMProxy = Gio.DBusProxy.makeProxyWrapper(NMIface);
+function NMDBus(initCallback, cancellable) {
+ return new NMProxy(Gio.DBus.system,
+ 'org.freedesktop.NetworkManager',
+ '/org/freedesktop/NetworkManager',
+ initCallback, cancellable);
+}
+
+// shared between NMNetworkMenuItem and NMDeviceWWAN
+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';
+}
+
+// shared between NMNetworkMenuItem and NMDeviceWireless
+function sortAccessPoints(accessPoints) {
+ return accessPoints.sort(function (one, two) {
+ return two.strength - one.strength;
+ });
+}
+
+function ssidToLabel(ssid) {
+ let label = NM.utils_ssid_to_utf8(ssid.get_data());
+ if (!label)
+ label = _("");
+ return label;
+}
+
+function NMNetworkMenuItem() {
+ this._init.apply(this, arguments);
+}
+
+function default_to_private_connections(client) {
+ let perms = client.get_permission_result (NM.ClientPermission.SETTINGS_MODIFY_SYSTEM);
+ return (perms != NM.ClientPermissionResult.YES);
+}
+
+NMNetworkMenuItem.prototype = {
+ __proto__: PopupMenu.PopupBaseMenuItem.prototype,
+
+ _init: function(accessPoints, title, params) {
+ PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);
+
+ accessPoints = sortAccessPoints(accessPoints);
+ this.bestAP = accessPoints[0];
+
+ if (!title) {
+ let ssid = this.bestAP.get_ssid();
+ title = ssidToLabel(ssid);
+ }
+
+ this._label = new St.Label({ text: title });
+ this.addActor(this._label);
+ let strStrengh = String(this.bestAP.strength);
+ strStrengh = strStrengh + '%';
+ this._labelStrength = new St.Label({ text: strStrengh });
+ this.addActor(this._labelStrength, { align: St.Align.END });
+ this._icons = new St.BoxLayout({ style_class: 'nm-menu-item-icons' });
+ this.addActor(this._icons, { align: St.Align.END });
+
+ this._signalIcon = new St.Icon({ icon_name: this._getIcon(),
+ style_class: 'popup-menu-icon' });
+ this._icons.add_actor(this._signalIcon);
+
+ this._accessPoints = [ ];
+ for (let i = 0; i < accessPoints.length; i++) {
+ let ap = accessPoints[i];
+ // need a wrapper object here, because the access points can be shared
+ // between many NMNetworkMenuItems
+ let apObj = {
+ ap: ap,
+ updateId: ap.connect('notify::strength', Lang.bind(this, this._updated))
+ };
+ this._accessPoints.push(apObj);
+ }
+ },
+
+ _updated: function(ap) {
+ if (ap.strength > this.bestAP.strength)
+ this.bestAP = ap;
+
+ this._signalIcon.icon_name = this._getIcon();
+ let strStrengh = String(this.bestAP.strength);
+ strStrengh = strStrengh + '%';
+ this._labelStrength.set_text(strStrengh);
+ },
+
+ _getIcon: function() {
+ if (this.bestAP.mode == NM80211Mode.ADHOC)
+ return 'xsi-network-workgroup';
+ else if (this.bestAP._secType != NMAccessPointSecurity.UNKNOWN &&
+ this.bestAP._secType != NMAccessPointSecurity.NONE)
+ return 'xsi-network-wireless-signal-' + signalToIcon(this.bestAP.strength) + '-secure';
+ else
+ return 'xsi-network-wireless-signal-' + signalToIcon(this.bestAP.strength);
+ },
+
+ updateAccessPoints: function(accessPoints) {
+ for (let i = 0; i < this._accessPoints.length; i++) {
+ let apObj = this._accessPoints[i];
+ apObj.ap.disconnect(apObj.updateId);
+ apObj.updateId = 0;
+ }
+
+ accessPoints = sortAccessPoints(accessPoints);
+ this.bestAP = accessPoints[0];
+ this._accessPoints = [ ];
+ for (let i = 0; i < accessPoints.length; i++) {
+ let ap = accessPoints[i];
+ let apObj = {
+ ap: ap,
+ updateId: ap.connect('notify::strength', Lang.bind(this, this._updated))
+ };
+ this._accessPoints.push(apObj);
+ }
+ },
+
+ destroy: function() {
+ for (let i = 0; i < this._accessPoints.length; i++) {
+ let apObj = this._accessPoints[i];
+ apObj.ap.disconnect(apObj.updateId);
+ apObj.updateId = 0;
+ }
+
+ PopupMenu.PopupBaseMenuItem.prototype.destroy.call(this);
+ }
+};
+
+function NMWiredSectionTitleMenuItem() {
+ this._init.apply(this, arguments);
+}
+
+NMWiredSectionTitleMenuItem.prototype = {
+ __proto__: PopupMenu.PopupSwitchMenuItem.prototype,
+
+ _init: function(label, params) {
+ params = params || { };
+ params.style_class = 'popup-subtitle-menu-item';
+ PopupMenu.PopupSwitchMenuItem.prototype._init.call(this, label, false, params);
+ },
+
+ updateForDevice: function(device) {
+ if (device) {
+ this._device = device;
+ this.setStatus(device.statusLabel);
+ this.setToggleState(device.connected);
+ // if this device is not controllable, hide the switch
+ this._switch.actor.visible = device.controllable;
+ } else {
+ this.setStatus('');
+ this._switch.actor.visible = false;
+ }
+ },
+
+ activate: function(event) {
+ PopupMenu.PopupSwitchMenuItem.prototype.activate.call(this, event);
+
+ if (!this._device) {
+ log('Section title activated when there is more than one device, should be non reactive');
+ return;
+ }
+
+ let newState = this._switch.state;
+
+ // Immediately reset the switch to false, it will be updated appropriately
+ // by state-changed signals in devices (but fixes the VPN not being in sync
+ // if the ActiveConnection object is never seen by libnm-glib)
+ this._switch.setToggleState(false);
+
+ if (newState)
+ this._device.activate();
+ else
+ this._device.deactivate();
+ }
+};
+
+function NMWirelessSectionTitleMenuItem() {
+ this._init.apply(this, arguments);
+}
+
+NMWirelessSectionTitleMenuItem.prototype = {
+ __proto__: PopupMenu.PopupSwitchMenuItem.prototype,
+
+ _init: function(client, property, title, params) {
+ params = params || { };
+ params.style_class = 'popup-subtitle-menu-item';
+ PopupMenu.PopupSwitchMenuItem.prototype._init.call(this, title, false, params);
+
+ this._client = client;
+ this._property = property + '_enabled';
+ this._propertyHardware = property + '_hardware_enabled';
+ this._setEnabledFunc = property + '_set_enabled';
+
+ this._client.connect('notify::' + property + '-enabled', Lang.bind(this, this._propertyChanged));
+ this._client.connect('notify::' + property + '-hardware-enabled', Lang.bind(this, this._propertyChanged));
+
+ this._propertyChanged();
+ },
+
+ updateForDevice: function(device) {
+ this._device = device;
+ // we show the switch
+ // - if there not just one device
+ // - if the switch is off
+ // - if the device is activated or disconnected
+ if (device && this._softwareEnabled && this._hardwareEnabled) {
+ this.setStatus(device.statusLabel);
+ this._switch.actor.visible = device.controllable;
+ } else {
+ this.setStatus(null);
+ this._switch.actor.show();
+ }
+ },
+
+ activate: function(event) {
+ PopupMenu.PopupSwitchMenuItem.prototype.activate.call(this, event);
+
+ this._client[this._setEnabledFunc](this._switch.state);
+
+ if (!this._device) {
+ log('Section title activated when there is more than one device, should be non reactive');
+ return;
+ }
+
+ let newState = this._switch.state;
+
+ if (newState)
+ this._device.activate();
+ else
+ this._device.deactivate();
+
+ this.emit('enabled-changed', this._switch.state);
+ },
+
+ _propertyChanged: function() {
+ this._softwareEnabled = this._client[this._property];
+ this._hardwareEnabled = this._client[this._propertyHardware];
+
+ let enabled = this._softwareEnabled && this._hardwareEnabled;
+ this.setToggleState(enabled);
+ if (!this._hardwareEnabled)
+ /* Translators: this indicates that wireless or wwan is disabled by hardware killswitch */
+ this.setStatus(_("disabled"));
+
+ this.emit('enabled-changed', enabled);
+ }
+};
+
+function NMDevice() {
+ throw new TypeError('Instantanting abstract class NMDevice');
+}
+
+NMDevice.prototype = {
+ _init: function(client, device, connections) {
+ this.device = device;
+ if (device) {
+ this.device._delegate = this;
+ this._stateChangedId = this.device.connect('state-changed', Lang.bind(this, this._deviceStateChanged));
+ } else
+ this._stateChangedId = 0;
+
+ // protected
+ this._client = client;
+ this._connections = [ ];
+ for (let i = 0; i < connections.length; i++) {
+ if (!connections[i]._uuid)
+ continue;
+ if (!this.connectionValid(connections[i]))
+ continue;
+ // record the connection
+ let obj = {
+ connection: connections[i],
+ name: connections[i]._name,
+ uuid: connections[i]._uuid,
+ timestamp: connections[i]._timestamp,
+ };
+ this._connections.push(obj);
+ }
+ this._connections.sort(this._connectionSortFunction);
+ this._activeConnection = null;
+ this._activeConnectionItem = null;
+ this._autoConnectionItem = null;
+ this._overflowItem = null;
+
+ this._carrierChangedId = 0;
+ this._firmwareChangedId = 0;
+ this._firmwareMissingId = 0;
+
+ if (this.device) {
+ this.statusItem = new PopupMenu.PopupSwitchMenuItem(this._getDescription(), this.connected, { style_class: 'popup-subtitle-menu-item' });
+ this._statusChanged = this.statusItem.connect('toggled', Lang.bind(this, function(item, state) {
+ if (state)
+ this.activate();
+ else
+ this.deactivate();
+ this.emit('enabled-changed');
+ }));
+
+ this._updateStatusItem();
+ }
+ this.section = new PopupMenu.PopupMenuSection();
+
+ this._createSection();
+ },
+
+ destroy: function() {
+ if (this.device)
+ this.device._delegate = null;
+
+ if (this._stateChangedId) {
+ // Need to go through GObject.Object.prototype because
+ // nm_device_disconnect conflicts with g_signal_disconnect
+ GObject.Object.prototype.disconnect.call(this.device, this._stateChangedId);
+ this._stateChangedId = 0;
+ }
+ if (this._carrierChangedId) {
+ // see above for why this is needed
+ GObject.Object.prototype.disconnect.call(this.device, this._carrierChangedId);
+ this._carrierChangedId = 0;
+ }
+ if (this._firmwareChangedId) {
+ GObject.Object.prototype.disconnect.call(this.device, this._firmwareChangedId);
+ this._firmwareChangedId = 0;
+ }
+ if (this._firmwareMissingId) {
+ GObject.Object.prototype.disconnect.call(this.device, this._firmwareMissingId);
+ this._firmwareMissingId = 0;
+ }
+
+ this._clearSection();
+ if (this.statusItem)
+ this.statusItem.destroy();
+ this.section.destroy();
+ },
+
+ deactivate: function() {
+ this.device.disconnect_async(null, null);
+ },
+
+ activate: function() {
+ if (this._activeConnection)
+ // nothing to do
+ return;
+
+ // pick the most recently used connection and connect to that
+ // or if no connections ever set, create an automatic one
+ if (this._connections.length > 0) {
+ this._client.activate_connection_async(this._connections[0].connection, this.device, null, null, null);
+ } else if (this._autoConnectionName) {
+ let connection = this._createAutomaticConnection();
+ if (connection)
+ this._client.add_and_activate_connection_async(connection, this.device, null, null, null);
+ }
+ },
+
+ get connected() {
+ return this.device.state == NM.DeviceState.ACTIVATED;
+ },
+
+ setActiveConnection: function(activeConnection) {
+ if (activeConnection == this._activeConnection)
+ // nothing to do
+ return;
+
+ // remove any UI
+ if (this._activeConnectionItem) {
+ this._activeConnectionItem.destroy();
+ this._activeConnectionItem = null;
+ }
+
+ this._activeConnection = activeConnection;
+
+ this._clearSection();
+ this._createSection();
+ },
+
+ checkConnection: function(connection) {
+ let exists = this._findConnection(connection._uuid) != -1;
+ let valid = this.connectionValid(connection);
+ if (exists && !valid)
+ this.removeConnection(connection);
+ else if (!exists && valid)
+ this.addConnection(connection);
+ },
+
+ addConnection: function(connection) {
+ // record the connection
+ let obj = {
+ connection: connection,
+ name: connection._name,
+ uuid: connection._uuid,
+ timestamp: connection._timestamp,
+ };
+ this._connections.push(obj);
+ this._connections.sort(this._connectionSortFunction);
+
+ this._clearSection();
+ this._createSection();
+ },
+
+ removeConnection: function(connection) {
+ if (!connection._uuid) {
+ log('Cannot remove a connection without an UUID');
+ return;
+ }
+ let pos = this._findConnection(connection._uuid);
+ if (pos == -1) {
+ // this connection was never added, nothing to do here
+ return;
+ }
+
+ let obj = this._connections[pos];
+ if (obj.item)
+ obj.item.destroy();
+ this._connections.splice(pos, 1);
+
+ if (this._connections.length <= 1) {
+ // We need to show the automatic connection again
+ // (or in the case of NMDeviceWired, we want to hide
+ // the only explicit connection)
+ this._clearSection();
+ this._createSection();
+ }
+ },
+
+ connectionValid: function(connection) {
+ return this.device.connection_valid(connection);
+ },
+
+ _connectionSortFunction: function(one, two) {
+ if (one.timestamp == two.timestamp)
+ return GLib.utf8_collate(one.name, two.name);
+
+ return two.timestamp - one.timestamp;
+ },
+
+ setEnabled: function(enabled) {
+ // do nothing by default, we want to keep the connection list visible
+ // in the majority of cases (wired, wwan, vpn)
+ },
+
+ get statusLabel(){
+ switch(this.device.state) {
+ case NM.DeviceState.DISCONNECTED:
+ case NM.DeviceState.ACTIVATED:
+ return null;
+ 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) */
+ return _("unmanaged");
+ case NM.DeviceState.DEACTIVATING:
+ return _("disconnecting...");
+ case NM.DeviceState.PREPARE:
+ case NM.DeviceState.CONFIG:
+ case NM.DeviceState.IP_CONFIG:
+ case NM.DeviceState.IP_CHECK:
+ case NM.DeviceState.SECONDARIES:
+ return _("connecting...");
+ case NM.DeviceState.NEED_AUTH:
+ /* Translators: this is for network connections that require some kind of key or password */
+ return _("authentication required");
+ case NM.DeviceState.UNAVAILABLE:
+ // This state is actually a compound of various states (generically unavailable,
+ // firmware missing, carrier not available), that are exposed by different properties
+ // (whose state may or may not updated when we receive state-changed).
+ if (!this._firmwareMissingId)
+ this._firmwareMissingId = this.device.connect('notify::firmware-missing', Lang.bind(this, this._substateChanged));
+ if (this.device.firmware_missing) {
+ /* Translators: this is for devices that require some kind of firmware or kernel
+ module, which is missing */
+ return _("firmware missing");
+ }
+ if (this.device.capabilities & NM.DeviceCapabilities.CARRIER_DETECT) {
+ if (!this._carrierChangedId)
+ this._carrierChangedId = this.device.connect('notify::carrier', Lang.bind(this, this._substateChanged));
+ if (!this.carrier) {
+ /* Translators: this is for wired network devices that are physically disconnected */
+ return _("cable unplugged");
+ }
+ }
+ /* Translators: this is for a network device that cannot be activated (for example it
+ is disabled by rfkill, or it has no coverage */
+ return _("unavailable");
+ case NM.DeviceState.FAILED:
+ return _("connection failed");
+ default:
+ log('Device state invalid, is %d'.format(this.device.state));
+ return 'invalid';
+ }
+ },
+
+ get controllable(){
+ // controllable for every state except unavailable or unmanaged
+ if(this.device.state === NM.DeviceState.UNAVAILABLE || this.device.state === NM.DeviceState.UNMANAGED)
+ return false;
+
+ return true;
+ },
+
+ // protected
+ _createAutomaticConnection: function() {
+ throw new TypeError('Invoking pure virtual function NMDevice.createAutomaticConnection');
+ },
+
+ _findConnection: function(uuid) {
+ for (let i = 0; i < this._connections.length; i++) {
+ let obj = this._connections[i];
+ if (obj.uuid == uuid)
+ return i;
+ }
+ return -1;
+ },
+
+ _clearSection: function() {
+ // Clear everything
+ this.section.removeAll();
+ this._autoConnectionItem = null;
+ this._activeConnectionItem = null;
+ this._overflowItem = null;
+ for (let i = 0; i < this._connections.length; i++) {
+ this._connections[i].item = null;
+ }
+ },
+
+ _shouldShowConnectionList: function() {
+ return (this.device.state >= NM.DeviceState.DISCONNECTED);
+ },
+
+ _createSection: function() {
+ if (!this._shouldShowConnectionList())
+ return;
+
+ if (this._activeConnection) {
+ this._createActiveConnectionItem();
+ this.section.addMenuItem(this._activeConnectionItem);
+ }
+ if (this._connections.length > 0) {
+ let activeOffset = this._activeConnectionItem ? 1 : 0;
+
+ for(let j = 0; j < this._connections.length; ++j) {
+ let obj = this._connections[j];
+ if (this._activeConnection &&
+ obj.connection == this._activeConnection.connection) {
+ activeOffset = 0; // we no longer need to account for this
+ continue;
+ }
+ obj.item = this._createConnectionItem(obj);
+
+ if (j + activeOffset >= NUM_VISIBLE_NETWORKS) {
+ if (!this._overflowItem) {
+ this._overflowItem = new PopupMenu.PopupSubMenuMenuItem(_("More"));
+ this.section.addMenuItem(this._overflowItem);
+ }
+ this._overflowItem.menu.addMenuItem(obj.item);
+ } else
+ this.section.addMenuItem(obj.item);
+ }
+ } else if (this._autoConnectionName) {
+ this._autoConnectionItem = new PopupMenu.PopupMenuItem(this._autoConnectionName);
+ this._autoConnectionItem.connect('activate', Lang.bind(this, function() {
+ let connection = this._createAutomaticConnection();
+ if (connection)
+ this._client.add_and_activate_connection_async(connection, this.device, null, null, null);
+ }));
+ this.section.addMenuItem(this._autoConnectionItem);
+ }
+ },
+
+ _createConnectionItem: function(obj) {
+ let connection = obj.connection;
+ let item = new PopupMenu.PopupMenuItem(obj.name);
+
+ item.connect('activate', Lang.bind(this, function() {
+ this._client.activate_connection_async(connection, this.device, null, null, null);
+ }));
+ return item;
+ },
+
+ _createActiveConnectionItem: function() {
+ let title;
+ let active = this._activeConnection.connection;
+ if (active) {
+ title = active._name;
+ } else {
+ /* Translators: this is the indication that a connection for another logged in user is active,
+ and we cannot access its settings (including the name) */
+ title = _("Connected (private)");
+ }
+ this._activeConnectionItem = new PopupMenu.PopupMenuItem(title, { reactive: false });
+ this._activeConnectionItem.setShowDot(true);
+ this._activeConnectionItem.actor.add_style_class_name('popup-device-menu-item');
+ },
+
+ _deviceStateChanged: function(device, newstate, oldstate, reason) {
+ if (newstate == oldstate) {
+ log('device emitted state-changed without actually changing state');
+ return;
+ }
+
+ if (oldstate == NM.DeviceState.ACTIVATED) {
+ this.emit('network-lost');
+ }
+
+ if (newstate == NM.DeviceState.FAILED) {
+ this.emit('activation-failed', reason);
+ }
+
+ this._updateStatusItem();
+
+ this._clearSection();
+ this._createSection();
+ this.emit('state-changed');
+ },
+
+ _updateStatusItem: function() {
+ if (this._carrierChangedId) {
+ // see above for why this is needed
+ GObject.Object.prototype.disconnect.call(this.device, this._carrierChangedId);
+ this._carrierChangedId = 0;
+ }
+ if (this._firmwareChangedId) {
+ GObject.Object.prototype.disconnect.call(this.device, this._firmwareChangedId);
+ this._firmwareChangedId = 0;
+ }
+
+ this.statusItem.setStatus(this.statusLabel);
+ this.statusItem.setToggleState(this.connected);
+ // if this device is not controllable, hide the switch
+ this.statusItem._switch.actor.visible = this.controllable;
+ },
+
+ _substateChanged: function() {
+ this.statusItem.setStatus(this.statusLabel);
+ this.statusItem._switch.actor.visible = this.controllable;
+
+ this.emit('state-changed');
+ },
+
+ _getDescription: function() {
+ let dev_product = this.device.get_product();
+ let dev_vendor = this.device.get_vendor();
+ if (!dev_product || !dev_vendor) {
+ if (this.device.name) {
+ return this.device.name;
+ }
+ else {
+ switch (this.device.get_device_type()) {
+ case NM.DeviceType.ETHERNET: return _("Ethernet");
+ case NM.DeviceType.WIFI: return _("Wifi");
+ case NM.DeviceType.MODEM: return _("Modem");
+ case NM.DeviceType.BT: return _("Bluetooth");
+ case NM.DeviceType.WIREGUARD: return _("WireGuard");
+ default: return "";
+ }
+ }
+ }
+
+ let product = Util.fixupPCIDescription(dev_product);
+ let vendor = Util.fixupPCIDescription(dev_vendor);
+ let out = '';
+
+ // Another quick hack; if all of the fixed up vendor string
+ // is found in product, ignore the vendor.
+ if (product.indexOf(vendor) == -1)
+ out += vendor + ' ';
+ out += product;
+
+ return out;
+ }
+};
+Signals.addSignalMethods(NMDevice.prototype);
+
+
+function NMDeviceWired() {
+ this._init.apply(this, arguments);
+}
+
+NMDeviceWired.prototype = {
+ __proto__: NMDevice.prototype,
+
+ _init: function(client, device, connections) {
+ this._autoConnectionName = _("Auto Ethernet");
+ this.category = NMConnectionCategory.WIRED;
+
+ NMDevice.prototype._init.call(this, client, device, connections);
+ },
+
+ _createSection: function() {
+ NMDevice.prototype._createSection.call(this);
+
+ // if we have only one connection (normal or automatic)
+ // we hide the connection list, and use the switch to control
+ // the device
+ // we can do it here because addConnection and removeConnection
+ // both call _createSection at some point
+ if (this._connections.length <= 1)
+ this.section.actor.hide();
+ else
+ this.section.actor.show();
+ },
+
+ _createAutomaticConnection: function() {
+ let connection = new NM.SimpleConnection();
+ connection._uuid = NM.utils_uuid_generate();
+ connection.add_setting(new NM.SettingWired());
+ let setting_conn = new NM.SettingConnection({
+ uuid: connection._uuid,
+ id: this._autoConnectionName,
+ type: NM.SETTING_WIRED_SETTING_NAME,
+ autoconnect: true
+ });
+ setting_conn.add_permission('user', GLib.get_user_name(), null);
+ connection.add_setting(setting_conn);
+ return connection;
+ }
+};
+
+function NMDeviceModem() {
+ this._init.apply(this, arguments);
+}
+
+NMDeviceModem.prototype = {
+ __proto__: NMDevice.prototype,
+
+ _init: function(client, device, connections) {
+ let is_wwan = false;
+
+ this._enabled = true;
+ this.mobileDevice = null;
+ this._connectionType = 'ppp';
+
+ this._capabilities = device.current_capabilities;
+ // Support new ModemManager1 devices
+ if (device.udi.indexOf('/org/freedesktop/ModemManager1/Modem') == 0) {
+ try {
+ is_wwan = true;
+ this.mobileDevice = new ModemManager.BroadbandModem(device.udi, device.current_capabilities);
+ if (this._capabilities & NM.DeviceModemCapabilities.GSM_UMTS) {
+ this._connectionType = NM.SETTING_GSM_SETTING_NAME;
+ } else if (this._capabilities & NM.DeviceModemCapabilities.LTE) {
+ this._connectionType = NM.SETTING_GSM_SETTING_NAME;
+ } else if (this._capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) {
+ this._connectionType = NM.SETTING_CDMA_SETTING_NAME;
+ }
+ }
+ catch (e){
+ global.logError(e);
+ }
+ } else if (this._capabilities & NM.DeviceModemCapabilities.GSM_UMTS) {
+ is_wwan = true;
+ this.mobileDevice = new ModemManager.ModemGsm(device.udi);
+ this._connectionType = NM.SETTING_GSM_SETTING_NAME;
+ } else if (this._capabilities & NM.DeviceModemCapabilities.CDMA_EVDO) {
+ is_wwan = true;
+ this.mobileDevice = new ModemManager.ModemCdma(device.udi);
+ this._connectionType = NM.SETTING_CDMA_SETTING_NAME;
+ } else if (this._capabilities & NM.DeviceModemCapabilities.LTE) {
+ is_wwan = true;
+ // FIXME: support signal quality
+ }
+
+ if (is_wwan) {
+ this.category = NMConnectionCategory.WWAN;
+ this._autoConnectionName = _("Auto broadband");
+ } else {
+ this.category = NMConnectionCategory.WIRED;
+ this._autoConnectionName = _("Auto dial-up");
+ }
+
+ if (this.mobileDevice) {
+ this._operatorNameId = this.mobileDevice.connect('notify::operator-name', Lang.bind(this, function() {
+ if (this._operatorItem) {
+ let name = this.mobileDevice.operator_name;
+ if (name) {
+ this._operatorItem.label.text = name;
+ this._operatorItem.actor.show();
+ } else
+ this._operatorItem.actor.hide();
+ }
+ }));
+ this._signalQualityId = this.mobileDevice.connect('notify::signal-quality', Lang.bind(this, function() {
+ if (this._operatorItem) {
+ this._operatorItem.setIcon(this._getSignalIcon());
+ }
+ }));
+ }
+
+ NMDevice.prototype._init.call(this, client, device, connections);
+ },
+
+ setEnabled: function(enabled) {
+ this._enabled = enabled;
+ if (this.category == NMConnectionCategory.WWAN) {
+ if (enabled) {
+ // prevent "network unavailable" statuses
+ this.statusItem.setStatus(null);
+ } else
+ this.statusItem.setStatus(this.statusLabel);
+ }
+
+ NMDevice.prototype.setEnabled.call(this, enabled);
+ },
+
+ get connected() {
+ return this._enabled && this.device.state == NM.DeviceState.ACTIVATED;
+ },
+
+ destroy: function() {
+ if (this._operatorNameId) {
+ this.mobileDevice.disconnect(this._operatorNameId);
+ this._operatorNameId = 0;
+ }
+ if (this._signalQualityId) {
+ this.mobileDevice.disconnect(this._signalQualityId);
+ this._signalQualityId = 0;
+ }
+
+ NMDevice.prototype.destroy.call(this);
+ },
+
+ _getSignalIcon: function() {
+ return 'xsi-network-cellular-signal-' + signalToIcon(this.mobileDevice.signal_quality);
+ },
+
+ _createSection: function() {
+ if (!this._shouldShowConnectionList())
+ return;
+
+ if (this.mobileDevice) {
+ // If operator_name is null, just pass the empty string, as the item is hidden anyway
+ this._operatorItem = new PopupMenu.PopupImageMenuItem(this.mobileDevice.operator_name || '',
+ this._getSignalIcon(),
+ { reactive: false });
+ if (!this.mobileDevice.operator_name)
+ this._operatorItem.actor.hide();
+ this.section.addMenuItem(this._operatorItem);
+ this._operatorItem.actor.add_style_class_name('popup-device-menu-item');
+ }
+
+ NMDevice.prototype._createSection.call(this);
+ },
+
+ _clearSection: function() {
+ this._operatorItem = null;
+
+ NMDevice.prototype._clearSection.call(this);
+ },
+
+ _createAutomaticConnection: function() {
+ // Mobile wizard is too complex for Cinnamon UI and
+ // is handled by the network panel
+ Util.spawn(['cinnamon-settings', 'network',
+ 'connect-3g', this.device.get_path()]);
+ return null;
+ }
+};
+
+function NMDeviceBluetooth() {
+ this._init.apply(this, arguments);
+}
+
+NMDeviceBluetooth.prototype = {
+ __proto__: NMDevice.prototype,
+
+ _init: function(client, device, connections) {
+ this._autoConnectionName = this._makeConnectionName(device);
+ device.connect('notify::name', Lang.bind(this, this._updateAutoConnectionName));
+
+ this.category = NMConnectionCategory.WWAN;
+
+ NMDevice.prototype._init.call(this, client, device, connections);
+ },
+
+ _createAutomaticConnection: function() {
+ let connection = new NM.SimpleConnection();
+ connection._uuid = NM.utils_uuid_generate();
+ connection.add_setting(new NM.SettingBluetooth());
+ connection.add_setting(new NM.SettingConnection({
+ uuid: connection._uuid,
+ id: this._autoConnectionName,
+ type: NM.SETTING_BLUETOOTH_SETTING_NAME,
+ autoconnect: false
+ }));
+ return connection;
+ },
+
+ _makeConnectionName: function(device) {
+ let name = device.name;
+ if (name)
+ return _("Auto %s").format(name);
+ else
+ return _("Auto bluetooth");
+ },
+
+ _updateAutoConnectionName: function() {
+ this._autoConnectionName = this._makeConnectionName(this.device);
+
+ this._clearSection();
+ this._createSection();
+ }
+};
+
+
+// Not a real device, but I save a lot code this way
+function NMDeviceVPN() {
+ this._init.apply(this, arguments);
+}
+
+NMDeviceVPN.prototype = {
+ __proto__: NMDevice.prototype,
+
+ _init: function(client) {
+ this._client = client
+ this._autoConnectionName = null;
+ this.category = NMConnectionCategory.VPN;
+
+ this._activeConnections = [];
+
+ NMDevice.prototype._init.call(this, client, null, []);
+ },
+
+ connectionValid: function(connection) {
+ return connection._type == NM.SETTING_VPN_SETTING_NAME;
+ },
+
+ get empty() {
+ return this._connections.length == 0;
+ },
+
+ get connected() {
+ return this._activeConnections.length > 0;
+ },
+
+ setActiveConnections: function(activeConnections) {
+ this._activeConnections = activeConnections || [];
+
+ this._createSection();
+ this.emit('active-connections-changed');
+ },
+
+ deactivate: function() {
+ for (let ac of this._activeConnections)
+ this._client.deactivate_connection(ac, null);
+
+ this._activeConnections = []
+ },
+
+ statusLabel: null,
+ controllable: true,
+
+ _clearSection: function() {
+ if (this.section && this.section.removeAll)
+ this.section.removeAll();
+
+ this._autoConnectionItem = null;
+ this._overflowItem = null;
+
+ for (let i = 0; i < this._connections.length; i++) {
+ if (this._connections[i].item && this._connections[i].item.destroy)
+ this._connections[i].item.destroy();
+
+ this._connections[i].item = null;
+ }
+ },
+
+ _updateConnectionItemView: function(item, connection, active) {
+ item.label.text = connection._name || _("Connected (private)");
+ if (active) {
+ item.setShowDot(true);
+ item.actor.add_style_class_name('popup-device-menu-item');
+ } else {
+ item.setShowDot(false);
+ item.actor.remove_style_class_name('popup-device-menu-item');
+ }
+ },
+
+ _createSection: function() {
+ for (let obj of this._connections) {
+ if (!obj.item) {
+ obj.item = new PopupMenu.PopupMenuItem(obj.name);
+ this._updateConnectionItemView(obj.item, obj.connection,
+ this._activeConnections.some(ac => ac.connection == obj.connection));
+ obj.item.connect('activate', Lang.bind(this, function() {
+ let activeConnection = this._activeConnections.find(ac => ac.connection === obj.connection);
+ if (activeConnection) {
+ this._client.deactivate_connection(activeConnection, null);
+ } else {
+ this._client.activate_connection_async(obj.connection, this.device, null, null, null);
+ }
+ }));
+ this.section.addMenuItem(obj.item);
+ } else {
+ this._updateConnectionItemView(obj.item, obj.connection, this._activeConnections.some(ac => ac.connection == obj.connection));
+ }
+ }
+ },
+};
+
+function NMDeviceWIREGUARD() {
+ this._init.apply(this, arguments);
+}
+
+NMDeviceWIREGUARD.prototype = {
+ __proto__: NMDevice.prototype,
+
+ _init: function(client, device, connections) {
+ // Disable autoconnections
+ this._autoConnectionName = null;
+ this._client = client;
+ this.category = NMConnectionCategory.WIREGUARD;
+ this._type = NM.SETTING_WIREGUARD_SETTING_NAME;
+
+ this._activeConnections = [];
+
+ NMDevice.prototype._init.call(this, client, null, [ ]);
+
+ // Tests:
+ this.category = NMConnectionCategory.WIREGUARD;
+ this._type = NM.SETTING_WIREGUARD_SETTING_NAME;
+ },
+
+ connectionValid: function(connection) {
+ return connection._type == NM.SETTING_WIREGUARD_SETTING_NAME;
+ },
+
+ get empty() {
+ return this._connections.length == 0;
+ },
+
+ get connected() {
+ return this._activeConnections.length > 0;
+ },
+
+ setActiveConnections: function(activeConnections) {
+ this._activeConnections = activeConnections || [];
+
+ this._createSection();
+ this.emit('active-connections-changed');
+ },
+
+ _shouldShowConnectionList: function() {
+ return true;
+ },
+
+ deactivate: function() {
+ for (let ac of this._activeConnections)
+ this._client.deactivate_connection(ac, null);
+
+ this._activeConnections = [];
+ },
+
+ _clearSection: function() {
+ if (this.section && this.section.removeAll)
+ this.section.removeAll();
+
+ this._autoConnectionItem = null;
+ this._overflowItem = null;
+
+ for (let i = 0; i < this._connections.length; i++) {
+ if (this._connections[i].item && this._connections[i].item.destroy)
+ this._connections[i].item.destroy();
+
+ this._connections[i].item = null;
+ }
+ },
+
+ /* A switch per tunnel: WireGuard connections are independent of one another,
+ so they are not a pick-one list. */
+ _createSection: function() {
+ for (let obj of this._connections) {
+ let active = this._activeConnections.some(ac => ac.connection == obj.connection);
+
+ if (!obj.item) {
+ obj.item = new PopupMenu.PopupSwitchMenuItem(obj.name, active);
+ obj.item.connect('toggled', (item, state) => {
+ let activeConnection = this._activeConnections.find(ac => ac.connection === obj.connection);
+
+ if (!state && activeConnection)
+ this._client.deactivate_connection(activeConnection, null);
+ else if (state && !activeConnection)
+ this._client.activate_connection_async(obj.connection, this.device, null, null, null);
+ });
+ this.section.addMenuItem(obj.item);
+ } else {
+ obj.item.setToggleState(active);
+ }
+ }
+ },
+
+ statusLabel: null,
+ controllable: true
+};
+
+
+function NMDeviceWireless() {
+ this._init.apply(this, arguments);
+}
+
+NMDeviceWireless.prototype = {
+ __proto__: NMDevice.prototype,
+
+ _init: function(client, device, connections) {
+ this.category = NMConnectionCategory.WIRELESS;
+
+ this._overflowItem = null;
+ this._networks = [ ];
+
+ this._client = client;
+
+ // breaking the layers with this, but cannot call
+ // this.connectionValid until I have a device
+ this.device = device;
+
+ let validConnections = connections.filter(Lang.bind(this, function(connection) {
+ return this.connectionValid(connection);
+ }));
+ let accessPoints = device.get_access_points() || [ ];
+ for (let i = 0; i < accessPoints.length; i++) {
+ // Access points are grouped by network
+ let ap = accessPoints[i];
+
+ if (ap.get_ssid() == null) {
+ // hidden access point cannot be added, we need to know
+ // the SSID and security details to connect
+ // nevertheless, the access point can acquire a SSID when
+ // NetworkManager connects to it (via nmcli or cinnamon-settings)
+ ap._notifySsidId = ap.connect('notify::ssid', Lang.bind(this, this._notifySsidCb));
+ continue;
+ }
+
+ let pos = this._findNetwork(ap);
+ let obj;
+ if (pos != -1) {
+ obj = this._networks[pos];
+ obj.accessPoints.push(ap);
+ } else {
+ obj = { ssid: ap.get_ssid(),
+ mode: ap.mode,
+ security: this._getApSecurityType(ap),
+ connections: [ ],
+ item: null,
+ accessPoints: [ ap ]
+ };
+ obj.ssidText = ssidToLabel(obj.ssid);
+ this._networks.push(obj);
+ }
+
+ // Check if some connection is valid for this AP
+ for (let j = 0; j < validConnections.length; j++) {
+ let connection = validConnections[j];
+ if (ap.connection_valid(connection) &&
+ obj.connections.indexOf(connection) == -1) {
+ obj.connections.push(connection);
+ }
+ }
+ }
+
+ if (this.device.active_access_point) {
+ let networkPos = this._findNetwork(this.device.active_access_point);
+
+ if (networkPos == -1) // the connected access point is invisible
+ this._activeNetwork = null;
+ else
+ this._activeNetwork = this._networks[networkPos];
+ } else {
+ this._activeNetwork = null;
+ }
+ this._networks.sort(this._networkSortFunction);
+
+ this._apChangedId = device.connect('notify::active-access-point', Lang.bind(this, this._activeApChanged));
+ this._apAddedId = device.connect('access-point-added', Lang.bind(this, this._accessPointAdded));
+ this._apRemovedId = device.connect('access-point-removed', Lang.bind(this, this._accessPointRemoved));
+
+ NMDevice.prototype._init.call(this, client, device, validConnections);
+ },
+
+ destroy: function() {
+ if (this._apChangedId) {
+ // see above for this HACK
+ GObject.Object.prototype.disconnect.call(this.device, this._apChangedId);
+ this._apChangedId = 0;
+ }
+
+ 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;
+ }
+
+ NMDevice.prototype.destroy.call(this);
+ },
+
+ setEnabled: function(enabled) {
+ if (enabled) {
+ this.statusItem.actor.show();
+ this.section.actor.show();
+ } else {
+ this.statusItem.actor.hide();
+ this.section.actor.hide();
+ }
+ },
+
+ activate: function() {
+ if (this._activeConnection)
+ // nothing to do
+ return;
+
+ // among all visible networks, pick the last recently used connection
+ let best = null;
+ let bestApObj = null;
+ let bestTime = 0;
+ for (let i = 0; i < this._networks.length; i++) {
+ let apObj = this._networks[i];
+ for (let j = 0; j < apObj.connections.length; j++) {
+ let connection = apObj.connections[j];
+ if (connection._timestamp > bestTime) {
+ best = connection;
+ bestTime = connection._timestamp;
+ bestApObj = apObj;
+ }
+ }
+ }
+
+ if (best) {
+ for (let i = 0; i < bestApObj.accessPoints.length; i++) {
+ let ap = bestApObj.accessPoints[i];
+ if (ap.connection_valid(best)) {
+ this._client.activate_connection_async(best, this.device, ap.path, null, null);
+ break;
+ }
+ }
+ return;
+ }
+
+ // XXX: what else to do?
+ // for now, just pick a random network
+ // (this function is called in a corner case anyway, that is, only when
+ // the user toggles the switch and has more than one wireless device)
+ if (this._networks.length > 0) {
+ let connection = this._createAutomaticConnection(this._networks[0]);
+ let accessPoints = sortAccessPoints(this._networks[0].accessPoints);
+ this._client.add_and_activate_connection_async(connection, this.device, accessPoints[0].path, null, null);
+ }
+ },
+
+ _notifySsidCb: function(accessPoint) {
+ if (accessPoint.get_ssid() != null) {
+ accessPoint.disconnect(accessPoint._notifySsidId);
+ accessPoint._notifySsidId = 0;
+ this._accessPointAdded(this.device, accessPoint);
+ }
+ },
+
+ _activeApChanged: function() {
+ this._activeNetwork = null;
+
+ let activeAp = this.device.active_access_point;
+
+ if (activeAp) {
+ let res = this._findExistingNetwork(activeAp);
+
+ if (res != null)
+ this._activeNetwork = this._networks[res.network];
+ }
+
+ // we don't refresh the view here, setActiveConnection will
+ },
+
+ _getApSecurityType: function(accessPoint) {
+ if (accessPoint._secType)
+ return accessPoint._secType;
+
+ let flags = accessPoint.flags;
+ let wpa_flags = accessPoint.wpa_flags;
+ let rsn_flags = accessPoint.rsn_flags;
+ let type;
+ if (rsn_flags != NM80211ApSecurityFlags.NONE) {
+ /* RSN check first so that WPA+WPA2 APs are treated as RSN/WPA2 */
+ if (rsn_flags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
+ type = NMAccessPointSecurity.WPA2_ENT;
+ else if (rsn_flags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
+ type = NMAccessPointSecurity.WPA2_PSK;
+ } else if (wpa_flags != NM80211ApSecurityFlags.NONE) {
+ if (wpa_flags & NM80211ApSecurityFlags.KEY_MGMT_802_1X)
+ type = NMAccessPointSecurity.WPA_ENT;
+ else if (wpa_flags & NM80211ApSecurityFlags.KEY_MGMT_PSK)
+ type = NMAccessPointSecurity.WPA_PSK;
+ } else {
+ 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: function(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;
+
+ return two.accessPoints[0].strength - one.accessPoints[0].strength;
+/**
+ 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: function(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: function(accessPoint) {
+ for (let i = 0; i < this._networks.length; i++) {
+ let apObj = this._networks[i];
+ for (let j = 0; j < apObj.accessPoints.length; j++) {
+ if (apObj.accessPoints[j] == accessPoint)
+ return { network: i, ap: j };
+ }
+ }
+
+ return null;
+ },
+
+ _findNetwork: function(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;
+ },
+
+ _accessPointAdded: function(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', Lang.bind(this, this._notifySsidCb));
+ return;
+ }
+
+ let pos = this._findNetwork(accessPoint);
+ let apObj;
+ let needsupdate = false;
+
+ if (pos != -1) {
+ apObj = this._networks[pos];
+ if (apObj.accessPoints.indexOf(accessPoint) != -1) {
+ log('Access point was already seen, not adding again');
+ return;
+ }
+
+ apObj.accessPoints.push(accessPoint);
+ if (apObj.item)
+ apObj.item.updateAccessPoints(apObj.accessPoints);
+ } else {
+ apObj = { ssid: accessPoint.get_ssid(),
+ mode: accessPoint.mode,
+ security: this._getApSecurityType(accessPoint),
+ connections: [ ],
+ item: null,
+ accessPoints: [ accessPoint ]
+ };
+ apObj.ssidText = ssidToLabel(apObj.ssid);
+ needsupdate = true;
+ }
+
+ // check if this enables new connections for this group
+ for (let i = 0; i < this._connections.length; i++) {
+ let connection = this._connections[i].connection;
+ if (accessPoint.connection_valid(connection) &&
+ apObj.connections.indexOf(connection) == -1) {
+ apObj.connections.push(connection);
+
+ // this potentially changes the order
+ needsupdate = true;
+ }
+ }
+
+ if (needsupdate) {
+ this._accessPointRemoved(device, accessPoint);
+
+ if (pos != -1)
+ this._networks.splice(pos, 1);
+
+ if (this._networks.length == 0) {
+ // only network in the list
+ this._networks.push(apObj);
+ this._clearSection();
+ this._createSection();
+ return;
+ }
+
+ // skip networks that should appear earlier
+ let menuPos = this._activeConnectionItem ? 1 : 0;
+ for (pos = 0;
+ pos < this._networks.length &&
+ this._networkSortFunction(this._networks[pos], apObj) < 0; ++pos) {
+ if (this._networks[pos] != this._activeNetwork)
+ menuPos++;
+ }
+
+ // (re-)add the network
+ this._networks.splice(pos, 0, apObj);
+
+ if (this._shouldShowConnectionList()) {
+ this._createNetworkItem(apObj, menuPos);
+ if (menuPos < NUM_VISIBLE_NETWORKS && this._networks.length > NUM_VISIBLE_NETWORKS) {
+ for (; menuPos < NUM_VISIBLE_NETWORKS; ++pos) {
+ if (this._networks[pos] != this._activeNetwork)
+ menuPos++;
+ }
+ let item = this._networks[pos].item;
+ if (item && item._apObj) {
+ item.destroy();
+ item._apObj.item = null;
+
+ this._createNetworkItem(item._apObj, NUM_VISIBLE_NETWORKS);
+ }
+ }
+ }
+ }
+ },
+
+ _accessPointRemoved: function(device, accessPoint) {
+ let res = this._findExistingNetwork(accessPoint);
+
+ if (res == null) {
+ return;
+ }
+
+ let apObj = this._networks[res.network];
+ apObj.accessPoints.splice(res.ap, 1);
+
+ if (apObj.accessPoints.length == 0) {
+ if (this._activeNetwork == apObj)
+ this._activeNetwork = null;
+
+ if (apObj.item)
+ apObj.item.destroy();
+
+ if (this._overflowItem) {
+ if (!apObj.isMore) {
+ // we removed an item in the main menu, and we have a more submenu
+ // we need to extract the first item in more and move it to the submenu
+
+ let item = this._overflowItem.menu.firstMenuItem;
+ if (item && item._apObj) {
+ item.destroy();
+ // clear the cycle, and allow the construction of the new item
+ item._apObj.item = null;
+
+ this._createNetworkItem(item._apObj, NUM_VISIBLE_NETWORKS-1);
+ } else {
+ log('The more... menu was existing and empty! This should not happen');
+ }
+ }
+
+ // This can happen if the removed connection is from the overflow
+ // menu, or if we just moved the last connection out from the menu
+ if (this._overflowItem.menu.numMenuItems == 0) {
+ this._overflowItem.destroy();
+ this._overflowItem = null;
+ }
+ }
+ this._networks.splice(res.network, 1);
+
+ } else if (apObj.item)
+ apObj.item.updateAccessPoints(apObj.accessPoints);
+ },
+
+ _createAPItem: function(connection, accessPointObj, useConnectionName) {
+ let item = new NMNetworkMenuItem(accessPointObj.accessPoints, useConnectionName ? connection._name : undefined);
+ item._connection = connection;
+ item.connect('activate', Lang.bind(this, function() {
+ let accessPoints = sortAccessPoints(accessPointObj.accessPoints);
+ for (let i = 0; i < accessPoints.length; i++) {
+ if (accessPoints[i].connection_valid(connection)) {
+ this._client.activate_connection_async(connection, this.device, accessPoints[i].path, null, null);
+ break;
+ }
+ }
+ }));
+ return item;
+ },
+
+ _clearSection: function() {
+ NMDevice.prototype._clearSection.call(this);
+
+ for (let i = 0; i < this._networks.length; i++)
+ this._networks[i].item = null;
+ this._overflowItem = null;
+ },
+
+ removeConnection: function(connection) {
+ if (!connection._uuid)
+ return;
+ let pos = this._findConnection(connection._uuid);
+ if (pos == -1) {
+ // removing connection that was never added
+ return;
+ }
+
+ this._connections.splice(pos, 1);
+
+ let anyauto = false, forceupdate = false;
+ for (let i = 0; i < this._networks.length; i++) {
+ let apObj = this._networks[i];
+ let connections = apObj.connections;
+ for (let k = 0; k < connections.length; k++) {
+ if (connections[k]._uuid == connection._uuid) {
+ // remove the connection from the access point group
+ connections.splice(k);
+ anyauto = connections.length == 0;
+
+ if (anyauto) {
+ // this potentially changes the sorting order
+ forceupdate = true;
+ break;
+ }
+ if (apObj.item) {
+ if (apObj.item instanceof PopupMenu.PopupSubMenuMenuItem) {
+ let items = apObj.item.menu.getMenuItems();
+ if (items.length == 2) {
+ // we need to update the connection list to convert this to a normal item
+ forceupdate = true;
+ } else {
+ for (let j = 0; j < items.length; j++) {
+ if (items[j]._connection._uuid == connection._uuid) {
+ items[j].destroy();
+ break;
+ }
+ }
+ }
+ } else {
+ apObj.item.destroy();
+ apObj.item = null;
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (forceupdate || anyauto) {
+ this._networks.sort(this._networkSortFunction);
+ this._clearSection();
+ this._createSection();
+ }
+ },
+
+ addConnection: function(connection) {
+ // record the connection
+ let obj = {
+ connection: connection,
+ name: connection._name,
+ uuid: connection._uuid,
+ };
+ this._connections.push(obj);
+
+ // find an appropriate access point
+ let forceupdate = false;
+ for (let i = 0; i < this._networks.length; i++) {
+ let apObj = this._networks[i];
+
+ // Check if connection is valid for any of these access points
+ for (let k = 0; k < apObj.accessPoints.length; k++) {
+ let ap = apObj.accessPoints[k];
+ if (ap.connection_valid(connection)) {
+ apObj.connections.push(connection);
+ // this potentially changes the sorting order
+ forceupdate = true;
+ break;
+ }
+ }
+ }
+
+ if (forceupdate) {
+ this._networks.sort(this._networkSortFunction);
+ this._clearSection();
+ this._createSection();
+ }
+ },
+
+ _createActiveConnectionItem: function() {
+ if (this._activeConnection.connection) {
+ if (!this._activeNetwork) {
+ if (this.device.active_access_point) {
+ let networkPos = this._findNetwork(this.device.active_access_point);
+ if (networkPos == -1) // the connected access point is invisible
+ this._activeNetwork = null;
+ else
+ this._activeNetwork = this._networks[networkPos];
+ } else {
+ this._activeNetwork = null;
+ }
+ }
+
+ if (this._activeNetwork)
+ this._activeConnectionItem = new NMNetworkMenuItem(this._activeNetwork.accessPoints, undefined, { reactive: false });
+ else
+ this._activeConnectionItem = new PopupMenu.PopupImageMenuItem(this._activeConnection.connection._name, 'xsi-network-wireless-connected', { reactive: false });
+ } else {
+ // We cannot read the connection (due to ACL, or API incompatibility), but we still show signal if we have it
+ if (this._activeNetwork)
+ this._activeConnectionItem = new NMNetworkMenuItem(this._activeNetwork.accessPoints, undefined,
+ { reactive: false });
+ else
+ this._activeConnectionItem = new PopupMenu.PopupImageMenuItem(_("Connected (private)"),
+ 'xsi-network-wireless-connected',
+ { reactive: false });
+ }
+ this._activeConnectionItem.setShowDot(true);
+ this._activeConnectionItem.actor.add_style_class_name('popup-device-menu-item');
+ },
+
+ _createAutomaticConnection: function(apObj) {
+ let name;
+ let ssid = NM.utils_ssid_to_utf8(apObj.ssid.get_data());
+ if (ssid) {
+ /* Translators: this the automatic wireless connection name (including the network name) */
+ name = _("Auto %s").format(ssid);
+ } else
+ name = _("Auto wireless");
+
+ let connection = new NM.SimpleConnection();
+ connection.add_setting(new NM.SettingWireless());
+ let setting_conn = new NM.SettingConnection({
+ id: name,
+ autoconnect: true, // NetworkManager will know to ignore this if appropriate
+ uuid: NM.utils_uuid_generate(),
+ type: NM.SETTING_WIRELESS_SETTING_NAME
+ });
+ if (default_to_private_connections(this._client)) {
+ setting_conn.add_permission('user', GLib.get_user_name(), null);
+ if (apObj.security == NMAccessPointSecurity.WPA2_PSK ||
+ apObj.security == NMAccessPointSecurity.WPA_PSK) {
+ connection.add_setting(new NM.SettingWirelessSecurity({
+ psk_flags: NM.SettingSecretFlags.AGENT_OWNED
+ }));
+ }
+ if (apObj.security == NMAccessPointSecurity.WEP) {
+ connection.add_setting(new NM.SettingWirelessSecurity({
+ wep_key_flags: NM.SettingSecretFlags.AGENT_OWNED
+ }));
+ }
+ }
+ connection.add_setting(setting_conn);
+ return connection;
+ },
+
+ _createNetworkItem: function(apObj, position) {
+
+ if(!apObj.accessPoints || apObj.accessPoints.length == 0) {
+ // this should not happen, but I have no idea why it happens
+ return;
+ }
+
+ if(apObj.connections.length > 0) {
+ if (apObj.connections.length == 1) {
+ apObj.item = this._createAPItem(apObj.connections[0], apObj, false);
+ } else {
+ let title = apObj.ssidText;
+ apObj.item = new PopupMenu.PopupSubMenuMenuItem(title);
+ for (let i = 0; i < apObj.connections.length; i++)
+ apObj.item.menu.addMenuItem(this._createAPItem(apObj.connections[i], apObj, true));
+ }
+ } else {
+ apObj.item = new NMNetworkMenuItem(apObj.accessPoints);
+ apObj.item.connect('activate', Lang.bind(this, function() {
+ let accessPoints = sortAccessPoints(apObj.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 cinnamon-settings
+ Util.spawn(['cinnamon-settings', 'network', 'connect-8021x-wifi',
+ this.device.get_path(), accessPoints[0].path]);
+ } else {
+ let connection = this._createAutomaticConnection(apObj);
+ this._client.add_and_activate_connection_async(connection, this.device, accessPoints[0].path, null, null);
+ }
+ }));
+ }
+ apObj.item._apObj = apObj;
+
+ if (position < NUM_VISIBLE_NETWORKS) {
+ apObj.isMore = false;
+ this.section.addMenuItem(apObj.item, position);
+ } else {
+ if (!this._overflowItem) {
+ this._overflowItem = new PopupMenu.PopupSubMenuMenuItem(_("More"));
+ this.section.addMenuItem(this._overflowItem);
+ }
+ this._overflowItem.menu.addMenuItem(apObj.item, position - NUM_VISIBLE_NETWORKS);
+ apObj.isMore = true;
+ }
+ },
+
+ _createSection: function() {
+ if (!this._shouldShowConnectionList())
+ return;
+
+ if(this._activeConnection) {
+ this._createActiveConnectionItem();
+ this.section.addMenuItem(this._activeConnectionItem);
+ }
+
+ let activeOffset = this._activeConnectionItem ? 1 : 0;
+
+ for(let j = 0; j < this._networks.length; j++) {
+ let apObj = this._networks[j];
+ if (apObj == this._activeNetwork) {
+ activeOffset = 0; // we no longer need to account for this
+ continue;
+ }
+
+ this._createNetworkItem(apObj, j + activeOffset);
+ }
+ },
+};
+
+function NMMessageTraySource() {
+ this._init();
+}
+
+NMMessageTraySource.prototype = {
+ __proto__: MessageTray.Source.prototype,
+
+ _init: function() {
+ MessageTray.Source.prototype._init.call(this, _("Network Manager"));
+
+ let icon = new St.Icon({ icon_name: 'xsi-network-transmit-receive',
+ icon_type: St.IconType.SYMBOLIC,
+ icon_size: this.ICON_SIZE
+ });
+ this._setSummaryIcon(icon);
+ }
+};
+
+function CinnamonNetworkApplet(metadata, orientation, panel_height, instance_id) {
+ this._init(metadata, orientation, panel_height, instance_id);
+}
+
+CinnamonNetworkApplet.prototype = {
+ __proto__: Applet.IconApplet.prototype,
+
+ _init: function(metadata, orientation, panel_height, instance_id) {
+ Applet.IconApplet.prototype._init.call(this, orientation, panel_height, instance_id);
+
+ try {
+ this.metadata = metadata;
+ Main.systrayManager.registerTrayIconReplacement("network", metadata.uuid);
+ Main.systrayManager.registerTrayIconReplacement("nm-applet", metadata.uuid);
+
+ this.menuManager = new PopupMenu.PopupMenuManager(this);
+ this.menu = new Applet.AppletPopupMenu(this, orientation);
+ this.menuManager.addMenu(this.menu);
+
+ this._currentIconName = undefined;
+ this._setIcon('xsi-network-offline');
+
+ this.settings = new Settings.AppletSettings(this, metadata.uuid, this.instance_id);
+ this.settings.bind("keyOpen", "keyOpen", this._setKeybinding);
+ this._setKeybinding();
+
+ NM.Client.new_async(null, Lang.bind(this, this._clientGot));
+ }
+ catch (e) {
+ global.logError(e);
+ }
+ },
+
+ _setKeybinding() {
+ Main.keybindingManager.addXletHotKey(this, "network-open", this.keyOpen, Lang.bind(this, this._openMenu));
+ },
+
+ _clientGot: function(obj, result) {
+ try {
+ this._client = NM.Client.new_finish(result);
+ this._v1_28_0 = Util.version_exceeds(this._client.get_version(), "1.28.0");
+
+ this._nm_proxy = null;
+
+ if (!this._v1_28_0) {
+ new NMDBus(Lang.bind(this, function(proxy, error) {
+ this._nm_proxy = proxy;
+ }));
+ }
+
+ this._statusSection = new PopupMenu.PopupMenuSection();
+ this._statusItem = new PopupMenu.PopupMenuItem('', { style_class: 'popup-inactive-menu-item', reactive: false });
+ this._statusSection.addMenuItem(this._statusItem);
+ this._statusSection.addAction(_("Enable networking"), Lang.bind(this, function() {
+ this._client.networking_enabled = true;
+ }));
+ this._statusSection.actor.hide();
+ this.menu.addMenuItem(this._statusSection);
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._devices = { };
+
+ this._devices.wired = {
+ section: new PopupMenu.PopupMenuSection(),
+ devices: [ ],
+ item: new NMWiredSectionTitleMenuItem(_("Wired"))
+ };
+
+ this._devices.wired.section.addMenuItem(this._devices.wired.item);
+ this._devices.wired.section.actor.hide();
+ this.menu.addMenuItem(this._devices.wired.section);
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._devices.wireless = {
+ section: new PopupMenu.PopupMenuSection(),
+ devices: [ ],
+ item: this._makeToggleItem('wireless', _("Wireless"))
+ };
+ this._devices.wireless.section.addMenuItem(this._devices.wireless.item);
+ this._devices.wireless.section.actor.hide();
+ this.menu.addMenuItem(this._devices.wireless.section);
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._devices.wwan = {
+ section: new PopupMenu.PopupMenuSection(),
+ devices: [ ],
+ item: this._makeToggleItem('wwan', _("Mobile broadband"))
+ };
+ this._devices.wwan.section.addMenuItem(this._devices.wwan.item);
+ this._devices.wwan.section.actor.hide();
+ this.menu.addMenuItem(this._devices.wwan.section);
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._devices.vpn = {
+ section: new PopupMenu.PopupMenuSection(),
+ device: new NMDeviceVPN(this._client),
+ item: new NMWiredSectionTitleMenuItem(_("VPN Connections"))
+ };
+ this._devices.vpn.device.connect('active-connections-changed', Lang.bind(this, function() {
+ this._devices.vpn.item.updateForDevice(this._devices.vpn.device);
+ }));
+ this._devices.vpn.item.updateForDevice(this._devices.vpn.device);
+ this._devices.vpn.section.addMenuItem(this._devices.vpn.item);
+ this._devices.vpn.section.addMenuItem(this._devices.vpn.device.section);
+ this._devices.vpn.section.actor.hide();
+ this.menu.addMenuItem(this._devices.vpn.section);
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this._devices.wireguard = {
+ section: new PopupMenu.PopupMenuSection(),
+ device: new NMDeviceWIREGUARD(this._client),
+ item: new NMWiredSectionTitleMenuItem(_("WIREGUARD Connections"))
+ };
+ this._devices.wireguard.device.connect('active-connections-changed', () => {
+ this._devices.wireguard.item.updateForDevice(this._devices.wireguard.device);
+ });
+ this._devices.wireguard.item.updateForDevice(this._devices.wireguard.device);
+ this._devices.wireguard.section.addMenuItem(this._devices.wireguard.item);
+ this._devices.wireguard.section.addMenuItem(this._devices.wireguard.device.section);
+ this._devices.wireguard.section.actor.hide();
+ this.menu.addMenuItem(this._devices.wireguard.section);
+ this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
+
+ this.menu.addSettingsAction(_("Network Settings"), 'network');
+ this.menu.addAction(_("Network Connections"), Lang.bind(this, function() {
+ Util.spawnCommandLine("nm-connection-editor");
+ }));
+
+ this.menu.connect("open-state-changed", Lang.bind(this, this._onMenuOpenStateChanged));
+
+ this._periodicTimeoutId = 0;
+ this._updateFrequencySeconds = 0;
+ this._checkingConnectivity = false;
+ this._lastConnectivityState = NM.ConnectivityState.UNKNOWN;
+
+ this._activeConnections = [ ];
+ this._connections = [ ];
+
+ this._mainConnection = null;
+
+ // 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_WIRELESS_SETTING_NAME] = NMConnectionCategory.WIRELESS;
+ this._ctypes[NM.SETTING_WIRED_SETTING_NAME] = NMConnectionCategory.WIRED;
+ this._ctypes[NM.SETTING_PPPOE_SETTING_NAME] = NMConnectionCategory.WIRED;
+ this._ctypes[NM.SETTING_PPP_SETTING_NAME] = NMConnectionCategory.WIRED;
+ 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.WIREGUARD;
+
+ this._readConnections();
+ this._readDevices();
+ this._syncNMState();
+
+ this._client.connect('notify::nm-running', Lang.bind(this, this._syncNMState));
+ this._client.connect('notify::networking-enabled', Lang.bind(this, this._syncNMState));
+ this._client.connect('notify::state', Lang.bind(this, this._syncNMState));
+ this._client.connect('notify::active-connections', Lang.bind(this, this._updateIcon));
+ this._client.connect('device-added', Lang.bind(this, this._deviceAdded));
+ this._client.connect('device-removed', Lang.bind(this, this._deviceRemoved));
+ this._client.connect('connection-added', Lang.bind(this, this._connectionAdded));
+ this._client.connect('connection-removed', Lang.bind(this, this._connectionRemoved));
+
+ this._inited = true;
+
+ this._periodicUpdateIcon();
+
+ }
+ catch (e) {
+ global.logError(e);
+ }
+ },
+
+ _setIcon: function(name) {
+ if (this._currentIconName !== name) {
+ this.set_applet_icon_symbolic_name(name);
+ this._currentIconName = name;
+ }
+ },
+
+ on_applet_clicked: function(event) {
+ this.menu.toggle();
+ },
+
+ _openMenu: function () {
+ this.menu.toggle();
+ },
+
+ _ensureSource: function() {
+ if (!this._source) {
+ this._source = new NMMessageTraySource();
+ this._source.connect('destroy', Lang.bind(this, function() {
+ this._source = null;
+ }));
+ if (Main.messageTray) Main.messageTray.add(this._source);
+ }
+ },
+
+ _makeToggleItem: function(type, title) {
+ let item = new NMWirelessSectionTitleMenuItem(this._client, type, title);
+ item.connect('enabled-changed', Lang.bind(this, function(item, enabled) {
+ let devices = this._devices[type].devices;
+ devices.forEach(function(dev) {
+ dev.setEnabled(enabled);
+ });
+ this._syncSectionTitle(type);
+ }));
+ return item;
+ },
+
+ _syncSectionTitle: function(category) {
+ let devices = this._devices[category].devices;
+ let vis_devices = [];
+
+ let item = this._devices[category].item;
+ let section = this._devices[category].section;
+
+ /* We don't want unmanaged entries to clutter the list, but we do need to
+ * keep them around in case they become manageable and need to be made visible
+ * later (some valid wifi hardware starts out initially unmanaged, for instance.)
+ * We'll only count managed devices for evaluating what entries to display, and
+ * when to show or hide section titles. */
+ if (devices) {
+ for (let i = 0; i < devices.length; i++) {
+ if (devices[i].device.state == NM.DeviceState.UNMANAGED) {
+ continue;
+ }
+
+ vis_devices.push(devices[i]);
+ }
+ }
+
+ if (vis_devices.length == 0) {
+ section.actor.hide();
+ } else {
+ section.actor.show();
+ if (vis_devices.length == 1) {
+ devices.forEach(function(dev) {
+ dev.statusItem.actor.hide();
+ });
+
+ let dev = vis_devices[0];
+ item.updateForDevice(dev);
+ } else {
+ devices.forEach(function(dev) {
+ dev.statusItem.actor.visible = (dev.device.state != NM.DeviceState.UNMANAGED);
+ });
+ // remove status text from the section title item
+ item.updateForDevice(null);
+ }
+ }
+ },
+
+ _readDevices: function() {
+ let devices = this._client.get_devices() || [ ];
+ for (let i = 0; i < devices.length; ++i) {
+ this._deviceAdded(this._client, devices[i]);
+ }
+ },
+
+ _notifyForDevice: function(device, iconName, title, text, urgency) {
+ if (device._notification)
+ device._notification.destroy();
+
+ /* must call after destroying previous notification,
+ or this._source will be cleared */
+ this._ensureSource();
+
+ let icon = new St.Icon({ icon_name: iconName,
+ icon_type: St.IconType.SYMBOLIC,
+ icon_size: this._source.ICON_SIZE
+ });
+ device._notification = new MessageTray.Notification(this._source, title, text,
+ { icon: icon });
+ device._notification.setUrgency(urgency);
+ device._notification.setTransient(true);
+ device._notification.connect('destroy', function() {
+ device._notification = null;
+ });
+ this._source.notify(device._notification);
+ },
+
+ _deviceAdded: function(client, device) {
+ 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, this._connections);
+
+ wrapper._activationFailedId = wrapper.connect('activation-failed', Lang.bind(this, function(device, reason) {
+ // XXX: nm-applet has no special text depending on reason
+ // but I'm not sure of this generic message
+ this._notifyForDevice(device, 'network-error',
+ _("Connection failed"),
+ _("Activation of network connection failed"),
+ MessageTray.Urgency.HIGH);
+ }));
+ wrapper._deviceStateChangedId = wrapper.connect('state-changed', Lang.bind(this, function(dev) {
+ this._syncSectionTitle(dev.category);
+ }));
+ wrapper._destroyId = wrapper.connect('destroy', function(wrapper) {
+ wrapper.disconnect(wrapper._activationFailedId);
+ wrapper.disconnect(wrapper._deviceStateChangedId);
+ wrapper.disconnect(wrapper._destroyId);
+ });
+ let section = this._devices[wrapper.category].section;
+ let devices = this._devices[wrapper.category].devices;
+
+ section.addMenuItem(wrapper.section, 1);
+ section.addMenuItem(wrapper.statusItem, 1);
+ devices.push(wrapper);
+
+ this._syncSectionTitle(wrapper.category);
+ }
+ },
+
+ _deviceRemoved: function(client, device) {
+ if (!device._delegate) {
+ return;
+ }
+
+ let wrapper = device._delegate;
+ wrapper.destroy();
+
+ let devices = this._devices[wrapper.category].devices;
+ let pos = devices.indexOf(wrapper);
+ devices.splice(pos, 1);
+
+ this._syncSectionTitle(wrapper.category);
+ },
+
+ _syncActiveConnections: function() {
+ let closedConnections = [];
+ let newActiveConnections = this._client.get_active_connections() || [];
+
+ for (let a of this._activeConnections) {
+ if (!newActiveConnections.includes(a))
+ closedConnections.push(a);
+ }
+
+ for (let active of closedConnections) {
+ if (active._primaryDevice) {
+ if (active._type == NM.SETTING_VPN_SETTING_NAME)
+ this._devices.vpn.device.setActiveConnections([]);
+ else if (active._type == NM.SETTING_WIREGUARD_SETTING_NAME)
+ this._devices.wireguard.device.setActiveConnections([]);
+ else
+ active._primaryDevice.setActiveConnection(null);
+
+ active._primaryDevice = null;
+ }
+
+ if (active._inited) {
+ active.disconnect(active._notifyStateId);
+ active._inited = false;
+ }
+ }
+
+ this._activeConnections = newActiveConnections;
+ this._mainConnection = null;
+
+ let activating = null;
+ let activated = null;
+ let default_ip4 = null;
+ let default_ip6 = null;
+
+ let vpnConnections = [];
+ let wireguardConnections = [];
+
+ for (let a of this._activeConnections) {
+ if (!a._inited) {
+ a._notifyStateId = a.connect('notify::state', Lang.bind(this, this._notifyActivated));
+ a._inited = true;
+ }
+
+ if (!a._type) {
+ a._type = a.connection._type;
+ a._section = this._ctypes[a._type];
+ if (a._errorLogged) {
+ log('network applet: Found connection for active');
+ a._errorLogged = false;
+ }
+ }
+
+ if (!a._section){
+ // Do not take connections which section is undefined into account
+ // For instance, in Mint 18, when the "vpn" is "activated", we sometimes see a "tun" act as the default ipv4 connection.
+ continue;
+ }
+
+ if (a.state == NM.ActiveConnectionState.ACTIVATED) {
+ if (!default_ip4) {
+ // We didn't find the default IPV4 device yet..
+ if (!default_ip6) {
+ // We didn't find the default IPV6 device either so consider this one.
+ activated = a;
+ }
+ else {
+ // We already found the default IPV6 device, so only use this one if it's the default IPV4 device
+ if (a['default']) {
+ activated = a;
+ }
+ }
+ }
+ }
+ if (a.state == NM.ActiveConnectionState.ACTIVATING) {
+ activating = a;
+ }
+
+ if (a['default']) {
+ default_ip4 = a;
+ }
+ if (a.default6) {
+ default_ip6 = a;
+ }
+
+ if (!a._primaryDevice) {
+ if (a._type != NM.SETTING_VPN_SETTING_NAME && a._type != NM.SETTING_WIREGUARD_SETTING_NAME) {
+ // find a good device to be considered primary
+ a._primaryDevice = null;
+ let devices = a.get_devices() || [ ];
+ for (let j = 0; j < devices.length; j++) {
+ let d = devices[j];
+ if (d._delegate) {
+ a._primaryDevice = d._delegate;
+ break;
+ }
+ }
+ } else {
+ if (a._type == NM.SETTING_VPN_SETTING_NAME)
+ a._primaryDevice = this._devices.vpn.device;
+ else {
+ a._primaryDevice = this._devices.wireguard.device;
+ }
+ }
+
+ if (a.state == NM.ActiveConnectionState.ACTIVATED &&
+ a._primaryDevice && a._primaryDevice._notification) {
+ a._primaryDevice._notification.destroy();
+ a._primaryDevice._notification = null;
+ }
+ }
+
+ if (a._primaryDevice) {
+ if (a._type == NM.SETTING_VPN_SETTING_NAME)
+ vpnConnections.push(a);
+ else if (a._type == NM.SETTING_WIREGUARD_SETTING_NAME)
+ wireguardConnections.push(a);
+ else
+ a._primaryDevice.setActiveConnection(a);
+ }
+ }
+
+ if (this._devices.vpn && this._devices.vpn.device)
+ this._devices.vpn.device.setActiveConnections(vpnConnections);
+
+ if (this._devices.wireguard && this._devices.wireguard.device)
+ this._devices.wireguard.device.setActiveConnections(wireguardConnections);
+
+ this._mainConnection = activated || activating || default_ip4 || default_ip6 || null;
+ },
+
+ _notifyActivated: function(activeConnection) {
+ if (activeConnection.state == NM.ActiveConnectionState.ACTIVATED &&
+ activeConnection._primaryDevice && activeConnection._primaryDevice._notification) {
+ activeConnection._primaryDevice._notification.destroy();
+ activeConnection._primaryDevice._notification = null;
+ }
+
+ this._updateIcon();
+ },
+
+ _readConnections: function() {
+ let connections = this._client.get_connections();
+ for (let i = 0; i < connections.length; i++) {
+ let connection = connections[i];
+ if (connection._uuid) {
+ // connection was already seen (for example because NetworkManager was restarted)
+ continue;
+ }
+ connection._updatedId = connection.connect('changed', Lang.bind(this, this._updateConnection));
+
+ this._updateConnection(connection);
+ this._connections.push(connection);
+ }
+ },
+
+ _connectionAdded: function(client, connection) {
+ if (connection._uuid) {
+ // connection was already seen
+ return;
+ }
+
+ connection._updatedId = connection.connect('changed', Lang.bind(this, this._updateConnection));
+
+ this._updateConnection(connection);
+ this._connections.push(connection);
+
+ this._updateIcon();
+ },
+
+ _connectionRemoved: function(client, connection) {
+ let pos = this._connections.indexOf(connection);
+ if (pos != -1)
+ this._connections.splice(pos, 1);
+
+ let section = connection._section;
+
+ if (section == NMConnectionCategory.VPN) {
+ this._devices.vpn.device.removeConnection(connection);
+ if (this._devices.vpn.device.empty)
+ this._devices.vpn.section.actor.hide();
+ } else if (section == NMConnectionCategory.WIREGUARD) {
+ this._devices.wireguard.device.removeConnection(connection);
+ if (this._devices.wireguard.device.empty)
+ this._devices.wireguard.section.actor.hide();
+ } else if (section != NMConnectionCategory.INVALID) {
+ let devices = this._devices[section].devices;
+ for (let i = 0; i < devices.length; i++)
+ devices[i].removeConnection(connection);
+ }
+
+ connection._uuid = null;
+ connection.disconnect(connection._updatedId);
+ },
+
+ _updateConnection: function(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;
+ connection._name = connectionSettings.id;
+ connection._uuid = connectionSettings.uuid;
+ connection._timestamp = connectionSettings.timestamp;
+
+ let section = connection._section;
+
+ if (connection._section == NMConnectionCategory.INVALID)
+ return;
+ if (section == NMConnectionCategory.VPN) {
+ this._devices.vpn.device.checkConnection(connection);
+ this._devices.vpn.section.actor.show();
+ } else if (section == NMConnectionCategory.WIREGUARD) {
+ this._devices.wireguard.device.checkConnection(connection);
+ this._devices.wireguard.section.actor.show();
+ } else {
+ let devices = this._devices[section].devices;
+ for (let i = 0; i < devices.length; i++) {
+ devices[i].checkConnection(connection);
+ }
+ }
+ },
+
+ _hideDevices: function() {
+ this._devicesHidden = true;
+
+ for (let category in this._devices)
+ this._devices[category].section.actor.hide();
+ },
+
+ _showNormal: function() {
+ if (!this._devicesHidden) // nothing to do
+ return;
+ this._devicesHidden = false;
+
+ this._statusSection.actor.hide();
+
+ this._syncSectionTitle('wired');
+ this._syncSectionTitle('wireless');
+ this._syncSectionTitle('wwan');
+
+ if (!this._devices.vpn.device.empty)
+ this._devices.vpn.section.actor.show();
+ if (!this._devices.wireguard.device.empty)
+ this._devices.wireguard.section.actor.show();
+ },
+
+ _syncNMState: function() {
+ if (!this._client.nm_running) {
+ log('NetworkManager is not running, hiding...');
+ this.menu.close();
+ this.actor.hide();
+ return;
+ } else
+ this.actor.show();
+
+ if (!this._client.networking_enabled) {
+ this._setIcon('xsi-network-offline');
+ this._hideDevices();
+ this._statusItem.label.text = _("Networking is disabled");
+ this._statusSection.actor.show();
+ return;
+ }
+
+ this._showNormal();
+ this._updateIcon();
+ },
+
+ _updateIcon: function() {
+ let new_delay = DEFAULT_PERIODIC_UPDATE_FREQUENCY_SECONDS;
+
+ try {
+ this._syncActiveConnections();
+ let mc = this._mainConnection;
+
+ if (!mc) {
+ this._setIcon('xsi-network-offline');
+ this.set_applet_tooltip(_("No connection"));
+ } else if (mc.state == NM.ActiveConnectionState.ACTIVATING) {
+ new_delay = FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS;
+ switch (mc._section) {
+ case NMConnectionCategory.WWAN:
+ this._setIcon('xsi-network-cellular-acquiring');
+ this.set_applet_tooltip(_("Connecting to the cellular network..."));
+ break;
+ case NMConnectionCategory.WIRELESS:
+ this._setIcon('xsi-network-wireless-acquiring');
+ this.set_applet_tooltip(_("Connecting to the wireless network..."));
+ break;
+ case NMConnectionCategory.WIRED:
+ this._setIcon('xsi-network-wired-acquiring');
+ this.set_applet_tooltip(_("Connecting to the wired network..."));
+ break;
+ case NMConnectionCategory.VPN:
+ this._setIcon('xsi-network-vpn-acquiring');
+ this.set_applet_tooltip(_("Connecting to the VPN..."));
+ break;
+ case NMConnectionCategory.WIREGUARD:
+ this._setIcon('xsi-network-vpn-acquiring');
+ this.set_applet_tooltip(_("Connecting to WIREGUARD..."));
+ break;
+ default:
+ // fallback to a generic connected icon
+ // (it could be a private connection of some other user)
+ this._setIcon('xsi-network-wired-acquiring');
+ this.set_applet_tooltip(_("Connecting to the network..."));
+ }
+ } else {
+ let dev;
+ let limited_conn = false;
+ // let limited_conn = this._correctStateForTunnel(this._client.get_connectivity()) !== NM.ConnectivityState.FULL;
+
+ switch (mc._section) {
+ case NMConnectionCategory.WIRELESS:
+ dev = mc._primaryDevice;
+ if (dev) {
+ let ap = dev.device.active_access_point;
+ let mode = dev.device.mode;
+ if (!ap) {
+ if (mode != NM80211Mode.ADHOC) {
+ log('An active wireless connection, in infrastructure mode, involves no access point?');
+ break;
+ }
+ this._setIcon('xsi-network-wireless-connected');
+ this.set_applet_tooltip(_("Connected to the wireless network"));
+ } else {
+ if (limited_conn) {
+ this._setIcon('xsi-network-wireless-no-route');
+ this.set_applet_tooltip(_("Wireless connection") + ": " + ssidToLabel(ap.get_ssid()) + " " + _("(Limited connectivity)"));
+ } else {
+ this._setIcon('xsi-network-wireless-signal-' + signalToIcon(ap.strength));
+ this.set_applet_tooltip(_("Wireless connection") + ": " + ssidToLabel(ap.get_ssid()) + " ("+ ap.strength +"%)");
+ }
+ }
+ } else {
+ log('Active connection with no primary device?');
+ }
+ break;
+ case NMConnectionCategory.WIRED:
+ if (limited_conn) {
+ this._setIcon('xsi-network-wired-no-route');
+ this.set_applet_tooltip(_("Connected to the wired network") + " " + _("(Limited connectivity)"));
+
+ } else {
+ this._setIcon('xsi-network-wired');
+ this.set_applet_tooltip(_("Connected to the wired network"));
+ }
+ break;
+ case NMConnectionCategory.WWAN:
+ dev = mc._primaryDevice;
+ if (!dev) {
+ log('Active connection with no primary device?');
+ break;
+ }
+ if (!dev.mobileDevice) {
+ // this can happen for bluetooth in PAN mode
+ this._setIcon('xsi-network-cellular-connected');
+ this.set_applet_tooltip(_("Connected to the cellular network"));
+ break;
+ }
+
+ if (limited_conn) {
+ this._setIcon('xsi-network-cellular-no-route');
+ this.set_applet_tooltip(_("Connected to the cellular network") + " " + _("(Limited connectivity)"));
+ } else {
+ this._setIcon('xsi-network-cellular-signal-' + signalToIcon(dev.mobileDevice.signal_quality));
+ this.set_applet_tooltip(_("Connected to the cellular network"));
+ }
+
+ break;
+ case NMConnectionCategory.VPN:
+ // Should we indicate limited connectivity for VPNs like we do above? What if the connection is to
+ // a local machine? Need to test.
+ this._setIcon('xsi-network-vpn');
+ this.set_applet_tooltip(_("Connected to the VPN"));
+ break;
+ case NMConnectionCategory.WIREGUARD:
+ // Should we indicate limited connectivity for WIREGUARDs like we do above? What if the connection is to
+ // a local machine? Need to test.
+ this._setIcon('xsi-network-vpn');
+ this.set_applet_tooltip(_("Connected to WIREGUARD"));
+ break;
+ default:
+ // fallback to a generic connected icon
+ // (it could be a private connection of some other user)
+ this._setIcon('xsi-network-wired');
+ this.set_applet_tooltip(_("Connected to the network"));
+ break;
+ }
+ }
+ if (this._devices.wireguard.item && this._devices.wireguard.item._switch.state) {
+ this._setIcon('xsi-network-vpn');
+ this.set_applet_tooltip(_("Connected to WIREGUARD"));
+ }
+ for (let i = 0; i < this._activeConnections.length; i++) {
+ const a = this._activeConnections[i];
+ if (a._section === NMConnectionCategory.VPN && a.state === NM.ActiveConnectionState.ACTIVATING) {
+ this._setIcon('xsi-network-vpn-acquiring');
+ this.set_applet_tooltip(_("Connecting to the VPN..."));
+ break;
+ }
+ else if (a._section === NMConnectionCategory.VPN && a.state === NM.ActiveConnectionState.ACTIVATED) {
+ let iconName = 'xsi-network-vpn';
+ if (mc._section == NMConnectionCategory.WIRELESS) {
+ const dev = mc._primaryDevice;
+ const ap = dev ? dev.device.active_access_point : null;
+ if (ap)
+ iconName = 'xsi-network-wireless-signal-' + signalToIcon(ap.strength) + '-secure';
+ }
+ this._setIcon(iconName);
+ this.set_applet_tooltip(_("Connected to the VPN"));
+ break;
+ }
+ }
+ }
+ catch (e) {
+ global.logError(e);
+ }
+
+ if (this.menu.isOpen) {
+ return FAST_PERIODIC_UPDATE_FREQUENCY_SECONDS;
+ }
+
+ return new_delay;
+ },
+
+ _periodicUpdateIcon: function() {
+ let new_delay = this._updateIcon();
+ this._lastConnectivityState = this._client.get_connectivity();
+
+ // TODO: This can be unreliable with multiple interfaces, and libnm
+ // can be flaky with its connectivity state - on one machine of mine,
+ // I observed regular switching between full and limited connectivity,
+ // when no noticeable change in my actual ability to reach the internet.
+ // We might re-implement to be independent of libnm. For now just disable it.
+ //
+ // if (!this._checkingConnectivity) {
+ // // https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/476
+ // if (this._v1_28_0) {
+ // this._checkingConnectivity = true;
+ // this._client.check_connectivity_async(null, Lang.bind(this, this._connectivityCheckCallback));
+ // } else
+ // if (this._nm_proxy != null) {
+ // this._checkingConnectivity = true;
+ // this._nm_proxy.CheckConnectivityRemote(Lang.bind(this, this._proxyConnectivityCheckCallback));
+ // }
+ // }
+
+ if (this._updateFrequencySeconds != new_delay) {
+ this._restartPeriodicUpdateTimer(new_delay);
+ } else {
+ return GLib.SOURCE_CONTINUE;
+ }
+
+ return GLib.SOURCE_REMOVE;
+ },
+
+ _correctStateForTunnel(state) {
+ if (state !== NM.ConnectivityState.LIMITED) {
+ return state;
+ }
+
+ // if our primary connection state is 'limited', check if there's a tunnel
+ // connection active, and assume 'full' if so - this prevents reporting limited
+ // connectivity when something like PIA vpn is active. This could end up reporting
+ // a full connection when there isn't one, depending on what the tunnel is for,
+ // but NM doesn't seem to provide any way to discern a relationship between a virtual
+ // device (tunnel) and the physical device in use. The physical device is always returned
+ // as the primary.
+
+ if (this._activeConnections.some(con => con.get_connection_type() === "tun")) {
+ return NM.ConnectivityState.FULL;
+ }
+
+ if (this._activeConnections.some(con => con.get_connection_type() === "wireguard")) {
+ return NM.ConnectivityState.FULL;
+ }
+
+ return state;
+ },
+
+ _proxyConnectivityCheckCallback(new_state) {
+ let state = this._correctStateForTunnel(new_state[0]);
+
+ if (state !== this._lastConnectivityState) {
+ this._updateIcon();
+ }
+
+ this._checkingConnectivity = false;
+ },
+
+ _connectivityCheckCallback(source, result) {
+ let new_state = NM.ConnectivityState.UNKNOWN;
+
+ try {
+ new_state = source.check_connectivity_finish(result);
+ } catch (e) {
+ log(e);
+ }
+
+ let state = this._correctStateForTunnel(new_state);
+
+ if (state !== this._lastConnectivityState) {
+ this._updateIcon();
+ }
+
+ this._checkingConnectivity = false;
+ },
+
+ _restartPeriodicUpdateTimer: function(new_delay) {
+ if (this._periodicTimeoutId > 0) {
+ Mainloop.source_remove(this._periodicTimeoutId);
+ }
+
+ this._updateFrequencySeconds = new_delay;
+
+ this._periodicTimeoutId = Mainloop.timeout_add_seconds(this._updateFrequencySeconds, Lang.bind(this, this._periodicUpdateIcon));
+ },
+
+ _rescanAccessPoints: function() {
+ try {
+ let devices = this._devices.wireless.devices;
+
+ for (let i = 0; i < devices.length; i++) {
+ if (devices[i]._client.wireless_get_enabled()) {
+ devices[i].device.request_scan(null);
+ }
+ }
+ } catch (gerror) {
+ if (gerror.code == NM.DeviceError.NOTALLOWED) {
+ // NM only allows a rescan every 10 seconds
+ return;
+ } else {
+ log(gerror);
+ }
+ }
+ },
+
+ _onMenuOpenStateChanged: function(popup, open) {
+ this._periodicUpdateIcon();
+
+ if (open) {
+ this._rescanAccessPoints();
+ }
+ },
+
+ on_applet_removed_from_panel: function() {
+ Main.systrayManager.unregisterTrayIconReplacement(this.metadata.uuid);
+ Main.keybindingManager.removeXletHotKey(this, "network-open");
+ if (this._periodicTimeoutId){
+ Mainloop.source_remove(this._periodicTimeoutId);
+ }
+ },
+
+};
+
+function main(metadata, orientation, panel_height, instance_id) {
+ return new CinnamonNetworkApplet(metadata, orientation, panel_height, instance_id);
+}