network: Introduce a new, rewritten Wi-Fi section

Remove the Wi-Fi chooser from the menu and put it in a dialog instead.
This frees up the submenu to simply have three items: an rfkill toggle,
a button to show the dialog, and a button to show network settings.

Ideally, we'd autodetect the "needs network" case by user initiation
and automatically show the dialog if needed, but lower-level plumbing
is neccessary, so the menu item to show the dialog is an acceptable
compromise instead.

This is a part of the new system status design, see
https://wiki.gnome.org/GnomeShell/Design/Guidelines/SystemStatus/
for design details.

https://bugzilla.gnome.org/show_bug.cgi?id=704670
This commit is contained in:
Jasper St. Pierre 2013-06-12 00:03:36 -04:00
parent c8c839a569
commit 728c3a72ad
2 changed files with 578 additions and 7 deletions

View File

@ -262,6 +262,51 @@ StScrollBar StButton#vhandle:active {
background-size: contain; background-size: contain;
} }
/* Network */
.nm-dialog {
max-height: 400px;
}
.nm-dialog-content {
spacing: 8px;
}
.nm-dialog-header-hbox {
spacing: 4px;
}
.nm-dialog-header-icon {
icon-size: 32px;
}
.nm-dialog-scroll-view {
border: 2px solid #666;
border-radius: 6px;
}
.nm-dialog-header {
font-weight: bold;
}
.nm-dialog-item {
font-size: 12pt;
border-bottom: 1px solid #666;
padding: 12px;
}
.nm-dialog-item:checked {
background-color: #333;
}
.nm-dialog-icons {
spacing: .5em;
}
.nm-dialog-icon {
icon-size: 16px;
}
/* Buttons */ /* Buttons */
.candidate-page-button, .candidate-page-button,

View File

@ -1,7 +1,9 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
const GLib = imports.gi.GLib; const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject; const GObject = imports.gi.GObject;
const Gio = imports.gi.Gio; const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Lang = imports.lang; const Lang = imports.lang;
const NetworkManager = imports.gi.NetworkManager; const NetworkManager = imports.gi.NetworkManager;
const NMClient = imports.gi.NMClient; const NMClient = imports.gi.NMClient;
@ -15,6 +17,7 @@ const PanelMenu = imports.ui.panelMenu;
const PopupMenu = imports.ui.popupMenu; const PopupMenu = imports.ui.popupMenu;
const MessageTray = imports.ui.messageTray; const MessageTray = imports.ui.messageTray;
const NotificationDaemon = imports.ui.notificationDaemon; const NotificationDaemon = imports.ui.notificationDaemon;
const ModalDialog = imports.ui.modalDialog;
const ModemManager = imports.misc.modemManager; const ModemManager = imports.misc.modemManager;
const Util = imports.misc.util; const Util = imports.misc.util;
@ -497,6 +500,525 @@ const NMDeviceBluetooth = new Lang.Class({
} }
}); });
const NMWirelessDialogItem = new Lang.Class({
Name: 'NMWirelessDialogItem',
_init: function(network) {
this._network = network;
this._ap = network.accessPoints[0];
this.actor = new St.Button({ style_class: 'nm-dialog-item',
can_focus: true,
x_fill: true });
this.actor.connect('key-focus-in', Lang.bind(this, function() {
this.emit('selected');
}));
this.actor.connect('clicked', Lang.bind(this, function() {
this.actor.grab_key_focus();
}));
this._content = new St.BoxLayout({ style_class: 'nm-dialog-item-box' });
this.actor.set_child(this._content);
let title = ssidToLabel(this._ap.get_ssid());
this._label = new St.Label({ text: title });
this.actor.label_actor = this._label;
this._content.add(this._label, { expand: true, x_align: St.Align.START });
this._icons = new St.BoxLayout({ style_class: 'nm-dialog-icons' });
this._content.add(this._icons, { x_fill: false, x_align: St.Align.END });
this._secureIcon = new St.Icon({ style_class: 'nm-dialog-icon' });
if (this._ap._secType != NMAccessPointSecurity.NONE)
this._secureIcon.icon_name = 'network-wireless-encrypted-symbolic';
this._icons.add_actor(this._secureIcon);
this._signalIcon = new St.Icon({ icon_name: this._getIcon(),
style_class: 'nm-dialog-icon' });
this._icons.add_actor(this._signalIcon);
},
updateBestAP: function(ap) {
this._ap = ap;
this._signalIcon.icon_name = this._getIcon();
},
_getIcon: function() {
if (this._ap.mode == NM80211Mode.ADHOC)
return 'network-workgroup-symbolic';
else
return 'network-wireless-signal-' + signalToIcon(this._ap.strength) + '-symbolic';
}
});
Signals.addSignalMethods(NMWirelessDialogItem.prototype);
const NMWirelessDialog = new Lang.Class({
Name: 'NMWirelessDialog',
Extends: ModalDialog.ModalDialog,
_init: function(client, device, settings) {
this.parent({ styleClass: 'nm-dialog' });
this._client = client;
this._device = device;
this._networks = [];
this._buildLayout();
let connections = settings.list_connections();
this._connections = connections.filter(Lang.bind(this, function(connection) {
return device.connection_valid(connection);
}));
this._apAddedId = device.connect('access-point-added', Lang.bind(this, this._accessPointAdded));
this._apRemovedId = device.connect('access-point-removed', Lang.bind(this, this._accessPointRemoved));
// accessPointAdded will also create dialog items
let accessPoints = device.get_access_points() || [ ];
accessPoints.forEach(Lang.bind(this, function(ap) {
this._accessPointAdded(this._device, ap);
}));
this._selectedNetwork = null;
this._updateSensitivity();
},
destroy: function() {
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;
}
this.parent();
},
_updateSensitivity: function() {
let connectSensitive = this._selectedNetwork != null;
this._connectButton.reactive = connectSensitive;
this._connectButton.can_focus = connectSensitive;
},
_buildLayout: function() {
let headline = new St.BoxLayout({ style_class: 'nm-dialog-header-hbox' });
let icon = new St.Icon({ style_class: 'nm-dialog-header-icon',
icon_name: 'network-wireless-signal-excellent-symbolic' });
let titleBox = new St.BoxLayout({ vertical: true });
let title = new St.Label({ style_class: 'nm-dialog-header',
text: _("Wi-Fi Networks") });
let subtitle = new St.Label({ style_class: 'nm-dialog-subheader',
text: _("Select a network") });
titleBox.add(title);
titleBox.add(subtitle);
headline.add(icon);
headline.add(titleBox);
this.contentLayout.style_class = 'nm-dialog-content';
this.contentLayout.add(headline);
this._itemBox = new St.BoxLayout({ vertical: true });
this._scrollView = new St.ScrollView({ style_class: 'nm-dialog-scroll-view' });
this._scrollView.set_policy(Gtk.PolicyType.NEVER,
Gtk.PolicyType.AUTOMATIC);
this._scrollView.add_actor(this._itemBox);
this.contentLayout.add(this._scrollView);
this._disconnectButton = this.addButton({ action: Lang.bind(this, this.close),
label: _("Cancel"),
key: Clutter.Escape });
this._connectButton = this.addButton({ action: Lang.bind(this, this._connect),
label: _("Connect"),
key: Clutter.Return },
{ expand: true,
x_fill: false,
x_align: St.Align.END });
},
_connect: function() {
let network = this._selectedNetwork;
let accessPoints = network.accessPoints;
if (network.connections.length > 0) {
let connection = network.connections[0];
for (let i = 0; i < accessPoints.length; i++) {
if (accessPoints[i].connection_valid(connection)) {
this._client.activate_connection(connection, this._device, accessPoints[i].dbus_path, null);
break;
}
}
} else {
if ((accessPoints[0]._secType == NMAccessPointSecurity.WPA2_ENT)
|| (accessPoints[0]._secType == NMAccessPointSecurity.WPA_ENT)) {
// 802.1x-enabled APs require further configuration, so they're
// handled in gnome-control-center
Util.spawn(['gnome-control-center', 'network', 'connect-8021x-wifi',
this._device.get_path(), accessPoints[0].dbus_path]);
} else {
let connection = new NetworkManager.Connection();
this._client.add_and_activate_connection(connection, this._device, accessPoints[0].dbus_path, null)
}
}
this.close();
},
_notifySsidCb: function(accessPoint) {
if (accessPoint.get_ssid() != null) {
accessPoint.disconnect(accessPoint._notifySsidId);
accessPoint._notifySsidId = 0;
this._accessPointAdded(this._device, accessPoint);
}
},
_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;
let oneStrength = one.accessPoints[0].strength;
let twoStrength = two.accessPoints[0].strength;
// place stronger connections first
if (oneStrength != twoStrength)
return oneStrength < twoStrength ? 1 : -1;
let oneHasSecurity = one.security != NMAccessPointSecurity.NONE;
let twoHasSecurity = two.security != NMAccessPointSecurity.NONE;
// place secure connections first
// (we treat WEP/WPA/WPA2 the same as there is no way to
// take them apart from the UI)
if (oneHasSecurity && !twoHasSecurity)
return -1;
else if (!oneHasSecurity && twoHasSecurity)
return 1;
// sort alphabetically
return GLib.utf8_collate(one.ssidText, two.ssidText);
},
_networkCompare: function(network, accessPoint) {
if (!ssidCompare(network.ssid, 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 network = this._networks[i];
for (let j = 0; j < network.accessPoints.length; j++) {
if (network.accessPoints[j] == accessPoint)
return { network: i, ap: j };
}
}
return null;
},
_findNetwork: 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;
},
_checkConnections: function(network, accessPoint) {
this._connections.forEach(function(connection) {
if (accessPoint.connection_valid(connection) &&
network.connections.indexOf(connection) == -1) {
network.connections.push(connection);
}
});
},
_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 network;
if (pos != -1) {
network = this._networks[pos];
if (network.accessPoints.indexOf(accessPoint) != -1) {
log('Access point was already seen, not adding again');
return;
}
Util.insertSorted(network.accessPoints, accessPoint, function(one, two) {
return two.strength - one.strength;
});
network.item.updateBestAP(network.accessPoints[0]);
this._checkConnections(network, accessPoint);
this._resortItems();
} else {
network = { ssid: accessPoint.get_ssid(),
mode: accessPoint.mode,
security: this._getApSecurityType(accessPoint),
connections: [ ],
item: null,
accessPoints: [ accessPoint ]
};
network.ssidText = ssidToLabel(network.ssid);
this._checkConnections(network, accessPoint);
let newPos = Util.insertSorted(this._networks, network, this._networkSortFunction);
this._createNetworkItem(network);
this._itemBox.insert_child_at_index(network.item.actor, newPos);
}
},
_accessPointRemoved: function(device, accessPoint) {
let res = this._findExistingNetwork(accessPoint);
if (res == null) {
log('Removing an access point that was never added');
return;
}
let network = this._networks[res.network];
network.accessPoints.splice(res.ap, 1);
if (network.accessPoints.length == 0) {
network.item.destroy();
this._networks.splice(res.network, 1);
} else {
network.item.updateBestAP(network.accessPoints[0]);
this._resortItems();
}
},
_resortItems: function() {
let adjustment = this._scrollView.vscroll.adjustment;
let scrollValue = adjustment.value;
this._itemBox.remove_all_children();
this._networks.forEach(Lang.bind(this, function(network) {
this._itemBox.add_child(network.item.actor);
}));
adjustment.value = scrollValue;
},
_selectNetwork: function(network) {
if (this._selectedNetwork)
this._selectedNetwork.item.actor.checked = false;
this._selectedNetwork = network;
this._updateSensitivity();
if (this._selectedNetwork)
this._selectedNetwork.item.actor.checked = true;
},
_createNetworkItem: function(network) {
network.item = new NMWirelessDialogItem(network);
network.item.connect('selected', Lang.bind(this, function() {
Util.ensureActorVisibleInScrollView(this._scrollView, network.item.actor);
this._selectNetwork(network);
}));
},
});
const NMDeviceWireless = new Lang.Class({
Name: 'NMDeviceWireless',
category: NMConnectionCategory.WIRELESS,
_init: function(client, device, settings) {
this._client = client;
this._device = device;
this._settings = settings;
this._description = '';
this.item = new PopupMenu.PopupSubMenuMenuItem('', true);
this.item.menu.addAction(_("Select Network"), Lang.bind(this, this._showDialog));
this._toggleItem = new PopupMenu.PopupMenuItem('');
this._toggleItem.connect('activate', Lang.bind(this, this._toggleWifi));
this.item.menu.addMenuItem(this._toggleItem);
this.item.menu.addSettingsAction(_("Network Settings"), 'gnome-network-panel.desktop');
this._wirelessEnabledChangedId = this._device.connect('notify::wireless-enabled', Lang.bind(this, this._sync));
this._activeApChangedId = this._device.connect('notify::active-access-point', Lang.bind(this, this._activeApChanged));
this._stateChangedId = this._device.connect('state-changed', Lang.bind(this, this._deviceStateChanged));
this._sync();
},
destroy: function() {
if (this._activeApChangedId) {
GObject.Object.prototype.disconnect.call(this._device, this._activeApChangedId);
this._activeApChangedId = 0;
}
if (this._stateChangedId) {
GObject.Object.prototype.disconnect.call(this._device, this._stateChangedId);
this._stateChangedId = 0;
}
if (this._strengthChangedId > 0) {
this._activeAccessPoint.disconnect(this._strengthChangedId);
this._strengthChangedId = 0;
}
this.item.destroy();
},
_deviceStateChanged: function(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 == NetworkManager.DeviceState.FAILED &&
reason != NetworkManager.DeviceStateReason.NO_SECRETS) {
this.emit('activation-failed', reason);
}
this._sync();
},
_toggleWifi: function() {
this._client.wireless_enabled = !this._client.wireless_enabled;
},
_showDialog: function() {
this._dialog = new NMWirelessDialog(this._client, this._device, this._settings);
this._dialog.connect('closed', Lang.bind(this, this._dialogClosed));
this._dialog.open();
},
_dialogClosed: function() {
this._dialog.destroy();
this._dialog = null;
},
_strengthChanged: function() {
this.emit('icon-changed');
},
_activeApChanged: function() {
if (this._activeAccessPoint) {
this._activeAccessPoint.disconnect(this._strengthChangedId);
this._strengthChangedId = 0;
}
this._activeAccessPoint = this._device.active_access_point;
if (this._activeAccessPoint) {
this._strengthChangedId = this._activeAccessPoint.connect('notify::strength',
Lang.bind(this, this._strengthChanged));
}
this._sync();
},
_sync: function() {
this._toggleItem.label.text = this._client.wireless_enabled ? _("Turn Off") : _("Turn On");
this.item.status.text = this._getStatus();
this.item.icon.icon_name = this._getMenuIcon();
this.item.label.text = this._description;
},
setDeviceDescription: function(desc) {
this._description = desc;
this._sync();
},
_getStatus: function() {
let ap = this._device.active_access_point;
if (!ap)
return _("Off"); // XXX -- interpret actual status
return ssidToLabel(ap.get_ssid());
},
_getMenuIcon: function() {
if (this._device.active_connection)
return this.getIndicatorIcon();
else
return 'network-wireless-signal-none-symbolic';
},
getIndicatorIcon: function() {
if (this._device.active_connection.state == NetworkManager.ActiveConnectionState.ACTIVATING)
return 'network-wireless-acquiring-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?');
return 'network-wireless-connected-symbolic';
}
return 'network-wireless-signal-' + signalToIcon(ap.strength) + '-symbolic';
},
});
Signals.addSignalMethods(NMDeviceWireless.prototype);
const NMVPNConnectionItem = new Lang.Class({ const NMVPNConnectionItem = new Lang.Class({
Name: 'NMVPNConnectionItem', Name: 'NMVPNConnectionItem',
Extends: NMConnectionItem, Extends: NMConnectionItem,
@ -640,7 +1162,7 @@ const NMApplet = new Lang.Class({
// Device types // Device types
this._dtypes = { }; this._dtypes = { };
// this._dtypes[NetworkManager.DeviceType.WIFI] = NMDeviceWireless; this._dtypes[NetworkManager.DeviceType.WIFI] = NMDeviceWireless;
this._dtypes[NetworkManager.DeviceType.MODEM] = NMDeviceModem; this._dtypes[NetworkManager.DeviceType.MODEM] = NMDeviceModem;
this._dtypes[NetworkManager.DeviceType.BT] = NMDeviceBluetooth; this._dtypes[NetworkManager.DeviceType.BT] = NMDeviceBluetooth;
// TODO: WiMax support // TODO: WiMax support
@ -790,7 +1312,7 @@ const NMApplet = new Lang.Class({
let wrapperClass = this._dtypes[device.get_device_type()]; let wrapperClass = this._dtypes[device.get_device_type()];
if (wrapperClass) { if (wrapperClass) {
let wrapper = new wrapperClass(this._client, device); let wrapper = new wrapperClass(this._client, device, this._settings);
device._delegate = wrapper; device._delegate = wrapper;
this._addDeviceWrapper(wrapper); this._addDeviceWrapper(wrapper);
@ -798,10 +1320,12 @@ const NMApplet = new Lang.Class({
if (!skipSyncDeviceNames) if (!skipSyncDeviceNames)
this._syncDeviceNames(); this._syncDeviceNames();
if (wrapper instanceof NMConnectionSection) {
this._connections.forEach(function(connection) { this._connections.forEach(function(connection) {
wrapper.checkConnection(connection); wrapper.checkConnection(connection);
}); });
} }
}
}, },
_addDeviceWrapper: function(wrapper) { _addDeviceWrapper: function(wrapper) {
@ -1022,9 +1546,11 @@ const NMApplet = new Lang.Class({
this._vpnSection.removeConnection(connection); this._vpnSection.removeConnection(connection);
} else { } else {
let devices = this._devices[section].devices; let devices = this._devices[section].devices;
for (let i = 0; i < devices.length; i++) for (let i = 0; i < devices.length; i++) {
if (devices[i] instanceof NMConnectionSection)
devices[i].removeConnection(connection); devices[i].removeConnection(connection);
} }
}
connection.disconnect(connection._removedId); connection.disconnect(connection._removedId);
connection.disconnect(connection._updatedId); connection.disconnect(connection._updatedId);