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._control = control;
|
||||||
|
|
||||||
this.item = new PopupMenu.PopupBaseMenuItem({ activate: false });
|
this.item = new PopupMenu.PopupMenuSection();
|
||||||
this.item.hide();
|
|
||||||
|
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._inDrag = false;
|
||||||
this._notifyVolumeChangeId = 0;
|
this._notifyVolumeChangeId = 0;
|
||||||
@ -62,18 +72,47 @@ var StreamSlider = class extends Signals.EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
|
this._icon = new St.Icon({ style_class: 'popup-menu-icon' });
|
||||||
this.item.add(this._icon);
|
sliderItem.add(this._icon);
|
||||||
this.item.add_child(this._slider);
|
sliderItem.add_child(this._slider);
|
||||||
this.item.connect('button-press-event',
|
sliderItem.connect('button-press-event',
|
||||||
(actor, event) => this._slider.startDragging(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));
|
(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));
|
(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._stream = null;
|
||||||
this._volumeCancellable = null;
|
this._volumeCancellable = null;
|
||||||
this._icons = [];
|
this._icons = [];
|
||||||
|
|
||||||
|
this._sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
get stream() {
|
get stream() {
|
||||||
@ -92,7 +131,7 @@ var StreamSlider = class extends Signals.EventEmitter {
|
|||||||
this.emit('stream-updated');
|
this.emit('stream-updated');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._updateVisibility();
|
this._sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
_connectStream(stream) {
|
_connectStream(stream) {
|
||||||
@ -101,13 +140,55 @@ var StreamSlider = class extends Signals.EventEmitter {
|
|||||||
'notify::volume', this._updateVolume.bind(this), this);
|
'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() {
|
_shouldBeVisible() {
|
||||||
return this._stream != null;
|
return this._stream != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
_updateVisibility() {
|
_sync() {
|
||||||
let visible = this._shouldBeVisible();
|
this.item.actor.visible = this._shouldBeVisible();
|
||||||
this.item.visible = visible;
|
this._menuButton.reactive = this._deviceItems.size > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
scroll(event) {
|
scroll(event) {
|
||||||
@ -216,7 +297,15 @@ var StreamSlider = class extends Signals.EventEmitter {
|
|||||||
var OutputStreamSlider = class extends StreamSlider {
|
var OutputStreamSlider = class extends StreamSlider {
|
||||||
constructor(control) {
|
constructor(control) {
|
||||||
super(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 = [
|
this._icons = [
|
||||||
'audio-volume-muted-symbolic',
|
'audio-volume-muted-symbolic',
|
||||||
'audio-volume-low-symbolic',
|
'audio-volume-low-symbolic',
|
||||||
@ -233,6 +322,14 @@ var OutputStreamSlider = class extends StreamSlider {
|
|||||||
this._portChanged();
|
this._portChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lookupDevice(id) {
|
||||||
|
return this._control.lookup_output_id(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activateDevice(device) {
|
||||||
|
this._control.change_output(device);
|
||||||
|
}
|
||||||
|
|
||||||
_findHeadphones(sink) {
|
_findHeadphones(sink) {
|
||||||
// This only works for external headphones (e.g. bluetooth)
|
// This only works for external headphones (e.g. bluetooth)
|
||||||
if (sink.get_form_factor() == 'headset' ||
|
if (sink.get_form_factor() == 'headset' ||
|
||||||
@ -263,9 +360,13 @@ var OutputStreamSlider = class extends StreamSlider {
|
|||||||
var InputStreamSlider = class extends StreamSlider {
|
var InputStreamSlider = class extends StreamSlider {
|
||||||
constructor(control) {
|
constructor(control) {
|
||||||
super(control);
|
super(control);
|
||||||
this._slider.accessible_name = _("Microphone");
|
|
||||||
|
this._slider.accessible_name = _('Microphone');
|
||||||
|
|
||||||
this._control.connectObject(
|
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-added', () => this._maybeShowInput(),
|
||||||
'stream-removed', () => this._maybeShowInput(),
|
'stream-removed', () => this._maybeShowInput(),
|
||||||
this);
|
this);
|
||||||
@ -284,6 +385,14 @@ var InputStreamSlider = class extends StreamSlider {
|
|||||||
this._maybeShowInput();
|
this._maybeShowInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lookupDevice(id) {
|
||||||
|
return this._control.lookup_input_id(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activateDevice(device) {
|
||||||
|
this._control.change_input(device);
|
||||||
|
}
|
||||||
|
|
||||||
_maybeShowInput() {
|
_maybeShowInput() {
|
||||||
// only show input widgets if any application is recording audio
|
// only show input widgets if any application is recording audio
|
||||||
let showInput = false;
|
let showInput = false;
|
||||||
@ -300,7 +409,7 @@ var InputStreamSlider = class extends StreamSlider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._showInput = showInput;
|
this._showInput = showInput;
|
||||||
this._updateVisibility();
|
this._sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
_shouldBeVisible() {
|
_shouldBeVisible() {
|
||||||
|
Loading…
Reference in New Issue
Block a user