Skip to content

Commit fd7b2e7

Browse files
committed
feat(libs/widgets.ts): add grouped sliders for applications mixer
1 parent c560e9d commit fd7b2e7

5 files changed

+223
-72
lines changed

extension.ts

+17-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
2525
import { QuickSettingsMenu } from 'resource:///org/gnome/shell/ui/quickSettings.js';
2626

2727
import { LibPanel, Panel } from './libs/libpanel/main.js';
28-
import { ApplicationsMixer, AudioProfileSwitcher, BalanceSlider, SinkMixer, waitProperty } from './libs/widgets.js';
28+
import { ApplicationsMixer, ApplicationsMixerToggle, AudioProfileSwitcher, BalanceSlider, SinkMixer, waitProperty } from './libs/widgets.js';
2929

3030
const DateMenu = Main.panel.statusArea.dateMenu;
3131
const QuickSettings = Main.panel.statusArea.quickSettings;
@@ -98,6 +98,7 @@ export default class QSAP extends Extension {
9898
const move_master_volume = this.settings.get_boolean('move-master-volume');
9999
const media_control_action = this.settings.get_string('media-control');
100100
const create_mixer_sliders = this.settings.get_boolean('create-mixer-sliders');
101+
const mixer_sliders_type = this.settings.get_string("mixer-sliders-type");
101102
const create_sink_mixer = this.settings.get_boolean('create-sink-mixer');
102103
const remove_output_slider = this.settings.get_boolean('remove-output-slider');
103104
const create_balance_slider = this.settings.get_boolean('create-balance-slider');
@@ -185,7 +186,7 @@ export default class QSAP extends Extension {
185186
} else if (widget === 'media' && media_control_action === 'duplicate') {
186187
this._create_media_controls(index);
187188
} else if (widget === 'mixer' && create_mixer_sliders) {
188-
this._create_app_mixer(index, filter_mode, filters);
189+
this._create_app_mixer(index, mixer_sliders_type, filter_mode, filters);
189190
} else if (widget === "sink-mixer" && create_sink_mixer) {
190191
this._create_sink_mixer(index, sink_filter_mode, sink_filters);
191192
} else if (widget === "balance-slider" && create_balance_slider) {
@@ -228,6 +229,12 @@ export default class QSAP extends Extension {
228229
this._applications_mixer = null;
229230
}
230231

232+
if (this._applications_mixer_combined) {
233+
this._panel.removeItem(this._applications_mixer_combined);
234+
this._applications_mixer_combined.destroy();
235+
this._applications_mixer_combined = null;
236+
}
237+
231238
if (this._media_section) {
232239
this._panel.removeItem(this._media_section);
233240
this._media_section = null;
@@ -294,8 +301,14 @@ export default class QSAP extends Extension {
294301
this._panel._grid.set_child_at_index(this._media_section, index);
295302
}
296303

297-
_create_app_mixer(index, filter_mode, filters) {
298-
this._applications_mixer = new ApplicationsMixer(this._panel, index, filter_mode, filters, this.settings);
304+
_create_app_mixer(index, type, filter_mode, filters) {
305+
if (type === "combined") {
306+
this._applications_mixer_combined = new ApplicationsMixerToggle(this.settings, filter_mode, filters);
307+
this._panel.addItem(this._applications_mixer_combined, 2);
308+
this._panel._grid.set_child_at_index(this._applications_mixer_combined, index);
309+
} else {
310+
this._applications_mixer = new ApplicationsMixer(this._panel, index, filter_mode, filters, this.settings);
311+
}
299312
}
300313

301314
_create_sink_mixer(index, filter_mode, filters) {

libs/widgets.ts

+102-10
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import St from 'gi://St';
77

88
import { gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
99
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
10-
import { Ornament, PopupMenuItem } from 'resource:///org/gnome/shell/ui/popupMenu.js';
11-
import { QuickMenuToggle, QuickSlider } from 'resource:///org/gnome/shell/ui/quickSettings.js';
10+
import { Ornament, PopupBaseMenuItem, PopupMenuItem, PopupMenuSection } from 'resource:///org/gnome/shell/ui/popupMenu.js';
11+
import { QuickMenuToggle, QuickSlider, QuickToggle } from 'resource:///org/gnome/shell/ui/quickSettings.js';
1212
import * as Volume from 'resource:///org/gnome/shell/ui/status/volume.js';
1313

1414
import { get_pactl_path, spawn } from "./utils.js";
@@ -427,12 +427,20 @@ class ApplicationsMixerManager {
427427
private _sa_event_id: number;
428428
private _sr_event_id: number;
429429

430-
public on_slider_added?: (slider: ApplicationVolumeSlider) => void;
431-
public on_slider_removed?: (slider: ApplicationVolumeSlider) => void;
430+
public on_slider_added: (slider: ApplicationVolumeSlider) => void;
431+
public on_slider_removed: (slider: ApplicationVolumeSlider) => void;
432432

433-
constructor(settings: Gio.Settings, filter_mode: string, filters: string[]) {
433+
constructor(
434+
settings: Gio.Settings,
435+
filter_mode: string,
436+
filters: string[],
437+
on_slider_added: (slider: ApplicationVolumeSlider) => void,
438+
on_slider_removed: (slider: ApplicationVolumeSlider) => void
439+
) {
434440
this._settings = settings;
435441
this._mixer_control = Volume.getMixerControl();
442+
this.on_slider_added = on_slider_added;
443+
this.on_slider_removed = on_slider_removed;
436444

437445
this._sliders = new Map();
438446
this._filter_mode = filter_mode;
@@ -470,14 +478,14 @@ class ApplicationsMixerManager {
470478
);
471479
this._sliders.set(id, slider);
472480

473-
this.on_slider_added?.(slider);
481+
this.on_slider_added(slider);
474482
}
475483

476484
private _stream_removed(_control: Gvc.MixerControl, id: number) {
477485
if (!this._sliders.has(id)) return;
478486

479487
const slider = this._sliders.get(id);
480-
this.on_slider_removed?.(slider);
488+
this.on_slider_removed(slider);
481489

482490
this._sliders.delete(id);
483491
slider.destroy();
@@ -513,9 +521,13 @@ export class ApplicationsMixer {
513521

514522
this._sliders_ordered = [placeholder];
515523

516-
this._slider_manager = new ApplicationsMixerManager(settings, filter_mode, filters);
517-
this._slider_manager.on_slider_added = this._slider_added.bind(this);
518-
this._slider_manager.on_slider_removed = this._slider_removed.bind(this);
524+
this._slider_manager = new ApplicationsMixerManager(
525+
settings,
526+
filter_mode,
527+
filters,
528+
this._slider_added.bind(this),
529+
this._slider_removed.bind(this)
530+
);
519531
}
520532

521533
_slider_added(slider: ApplicationVolumeSlider) {
@@ -541,6 +553,75 @@ export class ApplicationsMixer {
541553
}
542554
};
543555

556+
// Note: lot of code taken from https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/status/backgroundApps.js?ref_type=heads#L137
557+
export const ApplicationsMixerToggle = GObject.registerClass(class ApplicationsMixerToggle extends QuickToggle {
558+
private _slider_manager: ApplicationsMixerManager;
559+
private _slidersSection: PopupMenuSection;
560+
561+
constructor(settings: Gio.Settings, filter_mode: string, filters: string[]) {
562+
super({
563+
visible: false, hasMenu: true,
564+
// The background apps toggle looks like a flat menu, but doesn't
565+
// have a separate menu button. Fake it with an arrow icon.
566+
iconName: "go-next-symbolic",
567+
title: "Applications emitting sound"
568+
});
569+
570+
this.add_style_class_name("background-apps-quick-toggle");
571+
this._box.set_child_above_sibling(this._icon, null);
572+
573+
this.menu.setHeader("audio-volume-high-symbolic", _("Applications volumes"));
574+
this._slidersSection = new PopupMenuSection();
575+
this.menu.addMenuItem(this._slidersSection);
576+
577+
this.connect("popup-menu", () => this.menu.open(false));
578+
579+
this.menu.connect("open-state-changed", () => this._syncVisibility());
580+
Main.sessionMode.connect("updated", () => this._syncVisibility());
581+
582+
this._slider_manager = new ApplicationsMixerManager(
583+
settings,
584+
filter_mode,
585+
filters,
586+
this._slider_added.bind(this),
587+
this._slider_removed.bind(this)
588+
);
589+
}
590+
591+
_syncVisibility() {
592+
const { isLocked } = Main.sessionMode;
593+
const nSliders = this._slidersSection.numMenuItems;
594+
// We cannot hide the quick toggle while the menu is open, otherwise
595+
// the menu position goes bogus. We can't show it in locked sessions
596+
// either
597+
this.visible = !isLocked && (this.menu.isOpen || nSliders > 0);
598+
}
599+
600+
_slider_added(slider: ApplicationVolumeSlider) {
601+
let slider_item = new ApplicationVolumeSliderItem(slider);
602+
slider._item = slider_item;
603+
this._slidersSection.addMenuItem(slider_item);
604+
605+
this._syncVisibility();
606+
}
607+
608+
_slider_removed(slider: ApplicationVolumeSlider) {
609+
this._slidersSection.box.remove_child(slider._item);
610+
this._slidersSection.disconnect_object(slider._item);
611+
this._syncVisibility();
612+
}
613+
614+
vfunc_clicked() {
615+
this.menu.open(true);
616+
}
617+
618+
destroy() {
619+
this._slider_manager.destroy();
620+
621+
super.destroy();
622+
}
623+
});
624+
544625
const ApplicationVolumeSlider = GObject.registerClass(class ApplicationVolumeSlider extends StreamSlider {
545626
private _pactl_path: string | null;
546627
private _pactl_path_changed_id: number;
@@ -645,3 +726,14 @@ const ApplicationVolumeSlider = GObject.registerClass(class ApplicationVolumeSli
645726
this._setActiveDevice(device.get_id());
646727
}
647728
});
729+
730+
const ApplicationVolumeSliderItem = GObject.registerClass(class ApplicationVolumeSliderItem extends PopupBaseMenuItem {
731+
constructor(slider: ApplicationVolumeSlider) {
732+
super();
733+
slider.x_expand = true;
734+
// since it uses a quick settings menu it will be broken if opened in
735+
// another menu
736+
slider._menuButton.get_parent().remove_child(slider._menuButton);
737+
this.add_child(slider);
738+
}
739+
});

0 commit comments

Comments
 (0)