From c1de2788b1714cf11d2bf380dcf9c3681916a01f Mon Sep 17 00:00:00 2001 From: Giovanni Campagna Date: Sun, 26 Aug 2012 16:05:46 +0200 Subject: [PATCH] Add a new lock screen menu to combine volume network and power The design has a combined volume-network-power indicator in the lock screen, which when opened shows a volume slider. Implement it by abstracting the volume menu into a PopupMenuSection, and by creating three StIcons bound to the real ones. https://bugzilla.gnome.org/show_bug.cgi?id=682540 --- js/Makefile.am | 1 + js/ui/panel.js | 12 ++--- js/ui/panelMenu.js | 3 +- js/ui/sessionMode.js | 11 ++-- js/ui/status/lockScreenMenu.js | 62 +++++++++++++++++++++ js/ui/status/network.js | 30 ++++------- js/ui/status/power.js | 22 ++++---- js/ui/status/volume.js | 99 ++++++++++++++++++++++++---------- 8 files changed, 172 insertions(+), 68 deletions(-) create mode 100644 js/ui/status/lockScreenMenu.js diff --git a/js/Makefile.am b/js/Makefile.am index 92e0e5993..330098105 100644 --- a/js/Makefile.am +++ b/js/Makefile.am @@ -85,6 +85,7 @@ nobase_dist_js_DATA = \ ui/shellDBus.js \ ui/status/accessibility.js \ ui/status/keyboard.js \ + ui/status/lockScreenMenu.js \ ui/status/network.js \ ui/status/power.js \ ui/status/volume.js \ diff --git a/js/ui/panel.js b/js/ui/panel.js index 943079f2d..631a0a11c 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -908,7 +908,7 @@ const Panel = new Lang.Class({ reactive: true }); this.actor._delegate = this; - this._statusArea = {}; + this.statusArea = {}; Main.overview.connect('shown', Lang.bind(this, function () { this.actor.add_style_class_name('in-overview'); @@ -1126,7 +1126,7 @@ const Panel = new Lang.Class({ }, addToStatusArea: function(role, indicator, position) { - if (this._statusArea[role]) + if (this.statusArea[role]) throw new Error('Extension point conflict: there is already a status indicator for role ' + role); if (!(indicator instanceof PanelMenu.Button)) @@ -1138,9 +1138,9 @@ const Panel = new Lang.Class({ if (indicator.menu) this._menus.addMenu(indicator.menu); - this._statusArea[role] = indicator; + this.statusArea[role] = indicator; let destroyId = indicator.connect('destroy', Lang.bind(this, function(emitter) { - delete this._statusArea[role]; + delete this.statusArea[role]; emitter.disconnect(destroyId); })); @@ -1155,7 +1155,7 @@ const Panel = new Lang.Class({ if (this._dateMenu) this._dateMenu.setLockedState(locked); - for (let id in this._statusArea) - this._statusArea[id].setLockedState(locked); + for (let id in this.statusArea) + this.statusArea[id].setLockedState(locked); }, }); diff --git a/js/ui/panelMenu.js b/js/ui/panelMenu.js index 29007bf26..3e48f6207 100644 --- a/js/ui/panelMenu.js +++ b/js/ui/panelMenu.js @@ -236,7 +236,8 @@ const SystemStatusButton = new Lang.Class({ this._box = new St.BoxLayout({ style_class: 'panel-status-button-box' }); this.actor.add_actor(this._box); - this.setIcon(iconName); + if (iconName) + this.setIcon(iconName); }, addIcon: function(gicon) { diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js index 1cc4ec346..55cfed776 100644 --- a/js/ui/sessionMode.js +++ b/js/ui/sessionMode.js @@ -11,6 +11,7 @@ const STANDARD_STATUS_AREA_SHELL_IMPLEMENTATION = { 'a11y': imports.ui.status.accessibility.ATIndicator, 'volume': imports.ui.status.volume.Indicator, 'battery': imports.ui.status.power.Indicator, + 'lockScreen': imports.ui.status.lockScreenMenu.Indicator, 'keyboard': imports.ui.status.keyboard.InputSourceIndicator, 'userMenu': imports.ui.userMenu.UserMenuButton }; @@ -44,12 +45,13 @@ const _modes = { statusArea: { order: [ 'a11y', 'display', 'keyboard', - 'volume', 'battery', 'powerMenu' + 'volume', 'battery', 'lockScreen', 'powerMenu' ], implementation: { 'a11y': imports.ui.status.accessibility.ATIndicator, 'volume': imports.ui.status.volume.Indicator, 'battery': imports.ui.status.power.Indicator, + 'lockScreen': imports.ui.status.lockScreenMenu.Indicator, 'keyboard': imports.ui.status.keyboard.InputSourceIndicator, 'powerMenu': imports.gdm.powerMenu.PowerMenuButton } @@ -68,12 +70,13 @@ const _modes = { extraStylesheet: null, statusArea: { order: [ - 'a11y', 'keyboard', 'volume' + 'a11y', 'keyboard', 'volume', 'lockScreen', ], implementation: { 'a11y': imports.ui.status.accessibility.ATIndicator, 'keyboard': imports.ui.status.keyboard.XKBIndicator, - 'volume': imports.ui.status.volume.Indicator + 'volume': imports.ui.status.volume.Indicator, + 'lockScreen': imports.ui.status.lockScreenMenu.Indicator, } } }, @@ -92,7 +95,7 @@ const _modes = { statusArea: { order: [ 'input-method', 'a11y', 'keyboard', 'volume', 'bluetooth', - 'network', 'battery', 'userMenu' + 'network', 'battery', 'lockScreen', 'userMenu' ], implementation: STANDARD_STATUS_AREA_SHELL_IMPLEMENTATION } diff --git a/js/ui/status/lockScreenMenu.js b/js/ui/status/lockScreenMenu.js new file mode 100644 index 000000000..6f8b897ae --- /dev/null +++ b/js/ui/status/lockScreenMenu.js @@ -0,0 +1,62 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const GObject = imports.gi.GObject; +const Lang = imports.lang; +const St = imports.gi.St; + +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; +const VolumeMenu = imports.ui.status.volume; + +const Indicator = new Lang.Class({ + Name: 'LockScreenMenuIndicator', + Extends: PanelMenu.SystemStatusButton, + + _init: function() { + this.parent(null, _("Volume, network, battery")); + this.actor.hide(); + + this._volume = Main.panel.statusArea.volume; + if (this._volume) { + this._volumeIcon = this.addIcon(null); + this._volume.mainIcon.bind_property('gicon', this._volumeIcon, 'gicon', + GObject.BindingFlags.SYNC_CREATE); + this._volume.mainIcon.bind_property('visible', this._volumeIcon, 'visible', + GObject.BindingFlags.SYNC_CREATE); + + this._volumeControl = VolumeMenu.getMixerControl(); + this._volumeMenu = new VolumeMenu.VolumeMenu(this._volumeControl); + this.menu.addMenuItem(this._volumeMenu); + } + + this._network = Main.panel.statusArea.network; + if (this._network) { + this._networkIcon = this.addIcon(null); + this._network.mainIcon.bind_property('gicon', this._networkIcon, 'gicon', + GObject.BindingFlags.SYNC_CREATE); + this._network.mainIcon.bind_property('visible', this._networkIcon, 'visible', + GObject.BindingFlags.SYNC_CREATE); + + this._networkSecondaryIcon = this.addIcon(null); + this._network.secondaryIcon.bind_property('gicon', this._networkSecondaryIcon, 'gicon', + GObject.BindingFlags.SYNC_CREATE); + this._network.secondaryIcon.bind_property('visible', this._networkSecondaryIcon, 'visible', + GObject.BindingFlags.SYNC_CREATE); + } + + this._battery = Main.panel.statusArea.battery; + if (this._battery) { + this._batteryIcon = this.addIcon(null); + this._battery.mainIcon.bind_property('gicon', this._batteryIcon, 'gicon', + GObject.BindingFlags.SYNC_CREATE); + this._battery.mainIcon.bind_property('visible', this._batteryIcon, 'visible', + GObject.BindingFlags.SYNC_CREATE); + } + }, + + setLockedState: function(locked) { + this.actor.visible = locked; + } +}); diff --git a/js/ui/status/network.js b/js/ui/status/network.js index a235beeca..72cea774c 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -1570,9 +1570,10 @@ const NMApplet = new Lang.Class({ _init: function() { this.parent('network-offline', _('Network')); - this._secondaryIcon = this.addIcon(new Gio.ThemedIcon({ name: 'network-vpn' })); - this._secondaryIcon.hide(); + this.secondaryIcon = this.addIcon(new Gio.ThemedIcon({ name: 'network-vpn' })); + this.secondaryIcon.hide(); + this._isLocked = false; this._client = NMClient.Client.new(); this._statusSection = new PopupMenu.PopupMenuSection(); @@ -1681,12 +1682,8 @@ const NMApplet = new Lang.Class({ }, setLockedState: function(locked) { - // FIXME: more design discussion is needed before we can - // expose part of this menu - - if (locked) - this.menu.close(); - this.actor.reactive = !locked; + this._isLocked = locked; + this._syncNMState(); }, _ensureSource: function() { @@ -2074,13 +2071,8 @@ const NMApplet = new Lang.Class({ }, _syncNMState: function() { - if (!this._client.manager_running) { - log('NetworkManager is not running, hiding...'); - this.menu.close(); - this.actor.hide(); - return; - } else - this.actor.show(); + this.mainIcon.visible = this._client.manager_running; + this.actor.visible = this.mainIcon.visible && !this._isLocked; if (!this._client.networking_enabled) { this.setIcon('network-offline'); @@ -2192,14 +2184,14 @@ const NMApplet = new Lang.Class({ // only show a separate icon when we're using a wireless/3g connection if (mc._section == NMConnectionCategory.WIRELESS || mc._section == NMConnectionCategory.WWAN) { - this._secondaryIcon.icon_name = vpnIconName; - this._secondaryIcon.visible = true; + this.secondaryIcon.icon_name = vpnIconName; + this.secondaryIcon.show(); } else { this.setIcon(vpnIconName); - this._secondaryIcon.visible = false; + this.secondaryIcon.hide(); } } else { - this._secondaryIcon.visible = false; + this.secondaryIcon.hide(); } // cleanup stale signal connections diff --git a/js/ui/status/power.js b/js/ui/status/power.js index 1c18b4d59..195422fad 100644 --- a/js/ui/status/power.js +++ b/js/ui/status/power.js @@ -56,6 +56,7 @@ const Indicator = new Lang.Class({ this._proxy = new PowerManagerProxy(Gio.DBus.session, BUS_NAME, OBJECT_PATH); + this._isLocked = false; this._deviceItems = [ ]; this._hasPrimary = false; this._primaryDeviceId = null; @@ -77,9 +78,8 @@ const Indicator = new Lang.Class({ }, setLockedState: function(locked) { - if (locked) - this.menu.close(); - this.actor.reactive = !locked; + this._isLocked = locked; + this._syncIcon(); }, _readPrimaryDevice: function() { @@ -150,16 +150,20 @@ const Indicator = new Lang.Class({ })); }, - _devicesChanged: function() { + _syncIcon: function() { let icon = this._proxy.Icon; - if (icon) { + let hasIcon = (icon != null); + + if (hasIcon) { let gicon = Gio.icon_new_for_string(icon); this.setGIcon(gicon); - this.actor.show(); - } else { - this.menu.close(); - this.actor.hide(); } + this.mainIcon.visible = hasIcon; + this.actor.visible = hasIcon && !this._isLocked; + }, + + _devicesChanged: function() { + this._syncIcon(); this._readPrimaryDevice(); this._readOtherDevices(); } diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js index 550788776..ab383dc4c 100644 --- a/js/ui/status/volume.js +++ b/js/ui/status/volume.js @@ -12,14 +12,27 @@ const VOLUME_ADJUSTMENT_STEP = 0.05; /* Volume adjustment step in % */ const VOLUME_NOTIFY_ID = 1; -const Indicator = new Lang.Class({ - Name: 'VolumeIndicator', - Extends: PanelMenu.SystemStatusButton, +// Each Gvc.MixerControl is a connection to PulseAudio, +// so it's better to make it a singleton +let _mixerControl; +function getMixerControl() { + if (_mixerControl) + return _mixerControl; - _init: function() { - this.parent('audio-volume-muted', _("Volume")); + _mixerControl = new Gvc.MixerControl({ name: 'GNOME Shell Volume Control' }); + _mixerControl.open(); - this._control = new Gvc.MixerControl({ name: 'GNOME Shell Volume Control' }); + return _mixerControl; +} + +const VolumeMenu = new Lang.Class({ + Name: 'VolumeMenu', + Extends: PopupMenu.PopupMenuSection, + + _init: function(control) { + this.parent(); + + this._control = control; this._control.connect('state-changed', Lang.bind(this, this._onControlStateChanged)); this._control.connect('default-sink-changed', Lang.bind(this, this._readOutput)); this._control.connect('default-source-changed', Lang.bind(this, this._readInput)); @@ -35,10 +48,10 @@ const Indicator = new Lang.Class({ this._outputSlider = new PopupMenu.PopupSliderMenuItem(0); this._outputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_output')); this._outputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange)); - this.menu.addMenuItem(this._outputTitle); - this.menu.addMenuItem(this._outputSlider); + this.addMenuItem(this._outputTitle); + this.addMenuItem(this._outputSlider); - this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this._input = null; this._inputVolumeId = 0; @@ -47,22 +60,11 @@ const Indicator = new Lang.Class({ this._inputSlider = new PopupMenu.PopupSliderMenuItem(0); this._inputSlider.connect('value-changed', Lang.bind(this, this._sliderChanged, '_input')); this._inputSlider.connect('drag-end', Lang.bind(this, this._notifyVolumeChange)); - this.menu.addMenuItem(this._inputTitle); - this.menu.addMenuItem(this._inputSlider); - - this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - this.menu.addSettingsAction(_("Sound Settings"), 'gnome-sound-panel.desktop'); - - this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); - this._control.open(); + this.addMenuItem(this._inputTitle); + this.addMenuItem(this._inputSlider); }, - setLockedState: function(locked) { - this.menu.setSettingsVisibility(!locked); - }, - - _onScrollEvent: function(actor, event) { - let direction = event.get_scroll_direction(); + scroll: function(direction) { let currentVolume = this._output.volume; if (direction == Clutter.ScrollDirection.DOWN) { @@ -88,9 +90,8 @@ const Indicator = new Lang.Class({ if (this._control.get_state() == Gvc.MixerControlState.READY) { this._readOutput(); this._readInput(); - this.actor.show(); } else { - this.actor.hide(); + this.emit('icon-changed', null); } }, @@ -109,7 +110,7 @@ const Indicator = new Lang.Class({ this._volumeChanged (null, null, '_output'); } else { this._outputSlider.setValue(0); - this.setIcon('audio-volume-muted-symbolic'); + this.emit('icon-changed', 'audio-volume-muted-symbolic'); } }, @@ -196,15 +197,55 @@ const Indicator = new Lang.Class({ slider.setValue(muted ? 0 : (this[property].volume / this._volumeMax)); if (property == '_output') { if (muted) - this.setIcon('audio-volume-muted'); + this.emit('icon-changed', 'audio-volume-muted'); else - this.setIcon(this._volumeToIcon(this._output.volume)); + this.emit('icon-changed', this._volumeToIcon(this._output.volume)); } }, _volumeChanged: function(object, param_spec, property) { this[property+'Slider'].setValue(this[property].volume / this._volumeMax); if (property == '_output' && !this._output.is_muted) - this.setIcon(this._volumeToIcon(this._output.volume)); + this.emit('icon-changed', this._volumeToIcon(this._output.volume)); + } +}); + +const Indicator = new Lang.Class({ + Name: 'VolumeIndicator', + Extends: PanelMenu.SystemStatusButton, + + _init: function() { + this.parent('audio-volume-muted', _("Volume")); + + this._isLocked = false; + + this._control = getMixerControl(); + this._volumeMenu = new VolumeMenu(this._control); + this._volumeMenu.connect('icon-changed', Lang.bind(this, function(menu, icon) { + this._hasPulseAudio = (icon != null); + this.setIcon(icon); + this._syncVisibility(); + })); + + this.menu.addMenuItem(this._volumeMenu); + + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addSettingsAction(_("Sound Settings"), 'gnome-sound-panel.desktop'); + + this.actor.connect('scroll-event', Lang.bind(this, this._onScrollEvent)); + }, + + setLockedState: function(locked) { + this._isLocked = locked; + this._syncVisibility(); + }, + + _syncVisibility: function() { + this.actor.visible = this._hasPulseAudio && !this._isLocked; + this.mainIcon.visible = this._hasPulseAudio; + }, + + _onScrollEvent: function(actor, event) { + this._volumeMenu.scroll(event.get_scroll_direction()); } });