[gnome-shell] status/volume: Add device submenus
- From: Marge Bot <marge-bot src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-shell] status/volume: Add device submenus
- Date: Tue, 19 Jul 2022 13:06:31 +0000 (UTC)
commit 762b4c20664880af30149e7f6a097f68c27adf83
Author: Florian Müllner <fmuellner gnome org>
Date: Fri Jul 15 14:35:35 2022 +0200
status/volume: Add device submenus
In case where there are multiple in- or output devices, pulseaudio
or pipewire can pick the "wrong" one by default.
Allow users to change devices without opening sound settings by
adding a submenu to the sliders when there is more than one device.
Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2380>
js/ui/status/volume.js | 137 ++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 123 insertions(+), 14 deletions(-)
---
diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js
index 1babc440e7..9668e5bde5 100644
--- a/js/ui/status/volume.js
+++ b/js/ui/status/volume.js
@@ -38,8 +38,18 @@ var StreamSlider = class extends Signals.EventEmitter {
this._control = control;
- this.item = new PopupMenu.PopupBaseMenuItem({ activate: false });
- this.item.hide();
+ this.item = new PopupMenu.PopupMenuSection();
+
+ const sliderItem = new PopupMenu.PopupBaseMenuItem({activate: false});
+ this.item.addMenuItem(sliderItem);
+
+ const submenuItem = new PopupMenu.PopupSubMenuMenuItem('');
+ this.item.addMenuItem(submenuItem);
+
+ // HACK: Hide the submenu item, its menu is controlled from sliderItem
+ submenuItem.hide();
+
+ this.menu = submenuItem.menu;
this._inDrag = false;
this._notifyVolumeChangeId = 0;
@@ -62,18 +72,47 @@ var StreamSlider = class extends Signals.EventEmitter {
});
this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
- this.item.add(this._icon);
- this.item.add_child(this._slider);
- this.item.connect('button-press-event',
+ sliderItem.add(this._icon);
+ sliderItem.add_child(this._slider);
+ sliderItem.connect('button-press-event',
(actor, event) => this._slider.startDragging(event));
- this.item.connect('key-press-event',
+ sliderItem.connect('key-press-event',
(actor, event) => this._slider.emit('key-press-event', event));
- this.item.connect('scroll-event',
+ sliderItem.connect('scroll-event',
(actor, event) => this._slider.emit('scroll-event', event));
+ this._menuButton = new St.Button({
+ child: new St.Icon({
+ iconName: 'pan-end-symbolic',
+ style_class: 'popup-menu-arrow',
+ }),
+ y_expand: true,
+ });
+ sliderItem.add_child(this._menuButton);
+
+ this._menuButton.connect('clicked', () => this.menu.toggle());
+
+ // In order to keep sliders aligned, do not hide
+ // the menu button, but make it fully transparent
+ this._menuButton.bind_property_full('reactive',
+ this._menuButton, 'opacity',
+ GObject.BindingFlags.DEFAULT,
+ (bind, source) => [true, source ? 255 : 0],
+ null);
+
+ this._deviceItems = new Map();
+
+ this._deviceSection = new PopupMenu.PopupMenuSection();
+ this.menu.addMenuItem(this._deviceSection);
+
+ this.menu.addSettingsAction(_('Sound Settings'),
+ 'gnome-sound-panel.desktop');
+
this._stream = null;
this._volumeCancellable = null;
this._icons = [];
+
+ this._sync();
}
get stream() {
@@ -92,7 +131,7 @@ var StreamSlider = class extends Signals.EventEmitter {
this.emit('stream-updated');
}
- this._updateVisibility();
+ this._sync();
}
_connectStream(stream) {
@@ -101,13 +140,55 @@ var StreamSlider = class extends Signals.EventEmitter {
'notify::volume', this._updateVolume.bind(this), this);
}
+ _lookupDevice(_id) {
+ throw new GObject.NotImplementedError(
+ `_lookupDevice in ${this.constructor.name}`);
+ }
+
+ _activateDevice(_device) {
+ throw new GObject.NotImplementedError(
+ `_activateDevice in ${this.constructor.name}`);
+ }
+
+ _addDevice(id) {
+ if (this._deviceItems.has(id))
+ return;
+
+ const device = this._lookupDevice(id);
+ if (!device)
+ return;
+
+ const item = new PopupMenu.PopupImageMenuItem(
+ device.get_description(), device.get_gicon());
+ item.connect('activate', () => this._activateDevice(device));
+
+ this._deviceSection.addMenuItem(item);
+ this._deviceItems.set(id, item);
+
+ this._sync();
+ }
+
+ _removeDevice(id) {
+ this._deviceItems.get(id)?.destroy();
+ if (this._deviceItems.delete(id))
+ this._sync();
+ }
+
+ _setActiveDevice(activeId) {
+ for (const [id, item] of this._deviceItems) {
+ item.setOrnament(id === activeId
+ ? PopupMenu.Ornament.CHECK
+ : PopupMenu.Ornament.NONE);
+ }
+ }
+
_shouldBeVisible() {
return this._stream != null;
}
- _updateVisibility() {
- let visible = this._shouldBeVisible();
- this.item.visible = visible;
+ _sync() {
+ this.item.actor.visible = this._shouldBeVisible();
+ this._menuButton.reactive = this._deviceItems.size > 1;
}
scroll(event) {
@@ -216,7 +297,15 @@ var StreamSlider = class extends Signals.EventEmitter {
var OutputStreamSlider = class extends StreamSlider {
constructor(control) {
super(control);
- this._slider.accessible_name = _("Volume");
+
+ this._slider.accessible_name = _('Volume');
+
+ this._control.connectObject(
+ 'output-added', (c, id) => this._addDevice(id),
+ 'output-removed', (c, id) => this._removeDevice(id),
+ 'active-output-update', (c, id) => this._setActiveDevice(id),
+ this);
+
this._icons = [
'audio-volume-muted-symbolic',
'audio-volume-low-symbolic',
@@ -233,6 +322,14 @@ var OutputStreamSlider = class extends StreamSlider {
this._portChanged();
}
+ _lookupDevice(id) {
+ return this._control.lookup_output_id(id);
+ }
+
+ _activateDevice(device) {
+ this._control.change_output(device);
+ }
+
_findHeadphones(sink) {
// This only works for external headphones (e.g. bluetooth)
if (sink.get_form_factor() == 'headset' ||
@@ -263,9 +360,13 @@ var OutputStreamSlider = class extends StreamSlider {
var InputStreamSlider = class extends StreamSlider {
constructor(control) {
super(control);
- this._slider.accessible_name = _("Microphone");
+
+ this._slider.accessible_name = _('Microphone');
this._control.connectObject(
+ 'input-added', (c, id) => this._addDevice(id),
+ 'input-removed', (c, id) => this._removeDevice(id),
+ 'active-input-update', (c, id) => this._setActiveDevice(id),
'stream-added', () => this._maybeShowInput(),
'stream-removed', () => this._maybeShowInput(),
this);
@@ -284,6 +385,14 @@ var InputStreamSlider = class extends StreamSlider {
this._maybeShowInput();
}
+ _lookupDevice(id) {
+ return this._control.lookup_input_id(id);
+ }
+
+ _activateDevice(device) {
+ this._control.change_input(device);
+ }
+
_maybeShowInput() {
// only show input widgets if any application is recording audio
let showInput = false;
@@ -300,7 +409,7 @@ var InputStreamSlider = class extends StreamSlider {
}
this._showInput = showInput;
- this._updateVisibility();
+ this._sync();
}
_shouldBeVisible() {
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]