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>
This commit is contained in:
parent
cf404f76c0
commit
762b4c2066
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user