Skip to content

Commit f904f87

Browse files
committed
feat(volumeMixer): enhance Volume Mixer with settings link and icon loading functionality
1 parent 0b708ab commit f904f87

4 files changed

Lines changed: 119 additions & 14 deletions

File tree

Lines changed: 12 additions & 0 deletions
Loading

src/modules/volumeMixer/volumeMixer.ts

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,47 @@
22
import '@girs/gjs';
33

44
import St from '@girs/st-17';
5+
import Gio from '@girs/gio-2.0';
56

67
import type { QuickSlider } from '@girs/gnome-shell/ui/quickSettings';
78
import * as Main from '@girs/gnome-shell/ui/main';
89
import * as PopupMenu from '@girs/gnome-shell/ui/popupMenu';
9-
1010
import { Module } from '~/module.ts';
1111
import { VolumeMixerPanel } from '~/modules/volumeMixer/mixerPanel.ts';
12+
import { loadIcon } from '~/shared/icons.ts';
1213

1314
/**
1415
* Volume Mixer Module
1516
*
1617
* Adds a toggle button beside the output slider's device-list icon in Quick
17-
* Settings. Clicking it opens the slider menu with an additional section that
18-
* shows per-application volume sliders.
18+
* Settings. Clicking it opens the slider menu with the following layout:
19+
*
20+
* [header] "Volume Mixer"
21+
* [_menuSection] per-application sliders ← before separator
22+
* [separator] ─────────────────────────
23+
* [_settingsSection] "Sound Settings" link ← after separator
24+
*
25+
* OutputStreamSlider already calls setHeader() in its own _init(), so by the
26+
* time _attachToSlider() runs the menu already contains:
27+
* header(0) · separator(1) · deviceSection(2) · …
28+
* We insert _menuSection at 1 (shifting separator to 2) and _settingsSection
29+
* at 3 (between separator and deviceSection).
1930
*/
2031
export class VolumeMixer extends Module {
2132
private _panel: InstanceType<typeof VolumeMixerPanel> | null = null;
2233
private _toggleButton: St.Button | null = null;
2334
private _menuSection: InstanceType<typeof PopupMenu.PopupMenuSection> | null =
2435
null;
36+
private _settingsSection: InstanceType<typeof PopupMenu.PopupMenuSection> | null =
37+
null;
2538
private _outputSlider: QuickSlider | null = null;
2639
private _menuClosedId = 0;
2740
private _gridChildAddedId = 0;
41+
private _quickSettings: Main.QuickSettings | null = null;
2842

2943
override enable(): void {
44+
this._quickSettings = Main.panel.statusArea.quickSettings;
45+
3046
const outputSlider = this._findOutputSlider();
3147
if (outputSlider) {
3248
this._attachToSlider(outputSlider);
@@ -71,6 +87,11 @@ export class VolumeMixer extends Module {
7187
this._menuSection = null;
7288
}
7389

90+
if (this._settingsSection) {
91+
this._settingsSection.destroy();
92+
this._settingsSection = null;
93+
}
94+
7495
if (this._panel) {
7596
this._panel.destroy();
7697
this._panel = null;
@@ -80,8 +101,12 @@ export class VolumeMixer extends Module {
80101
}
81102

82103
private _findOutputSlider(): QuickSlider | null {
83-
const quickSettings = Main.panel.statusArea.quickSettings;
84-
const grid = quickSettings.menu._grid;
104+
const grid = this._quickSettings?.menu?._grid;
105+
106+
if (!grid) {
107+
console.error('Aurora Shell: VolumeMixer could not find quick settings grid');
108+
return null;
109+
}
85110

86111
for (const child of grid.get_children()) {
87112
if (child.constructor.name === 'OutputStreamSlider') {
@@ -92,22 +117,36 @@ export class VolumeMixer extends Module {
92117
return null;
93118
}
94119

95-
/**
96-
* Attaches the volume mixer toggle button and panel to the output stream
97-
* slider's popup menu. Clicking the button opens a menu showing only
98-
* per-application volume sliders.
99-
*/
100120
private _attachToSlider(slider: QuickSlider): void {
101121
this._outputSlider = slider;
102122
this._panel = new VolumeMixerPanel();
103123
this._menuSection = new PopupMenu.PopupMenuSection();
124+
104125
this._menuSection.box.add_child(this._panel);
126+
slider.menu.addMenuItem(this._menuSection, 1);
105127
this._menuSection.box.hide();
106128

107-
slider.menu.addMenuItem(this._menuSection, 2);
129+
this._settingsSection = new PopupMenu.PopupMenuSection();
130+
const settingsItem = new PopupMenu.PopupMenuItem(
131+
_('Sound Settings'),
132+
);
133+
settingsItem.connect('activate', () => {
134+
try {
135+
Gio.Subprocess.new(
136+
['gnome-control-center', 'sound'],
137+
Gio.SubprocessFlags.NONE,
138+
);
139+
} catch (e) {
140+
console.error(`Aurora Shell: Failed to open sound settings: ${e}`);
141+
}
142+
this._quickSettings?.menu.close(true);
143+
});
144+
this._settingsSection.addMenuItem(settingsItem);
145+
slider.menu.addMenuItem(this._settingsSection, 3);
146+
this._settingsSection.box.hide();
108147

109148
this._toggleButton = new St.Button({
110-
child: new St.Icon({ icon_name: 'open-menu-symbolic' }),
149+
child: new St.Icon({ gicon: loadIcon('volume-mixer-symbolic') }),
111150
style_class: 'icon-button flat',
112151
can_focus: true,
113152
x_expand: false,
@@ -118,18 +157,20 @@ export class VolumeMixer extends Module {
118157
slider.child.add_child(this._toggleButton);
119158

120159
this._toggleButton.connect('clicked', () => {
121-
if (!this._panel || !this._menuSection) return;
160+
if (!this._panel || !this._menuSection || !this._settingsSection) return;
122161

123162
this._menuSection.box.show();
163+
this._settingsSection.box.show();
124164
slider._deviceSection?.box.hide();
125165
slider.menu._setSettingsVisibility?.(false);
126166
slider.menu.setHeader('audio-speakers-symbolic', _('Volume Mixer'));
127167
slider.menu.open(true);
128168
});
129169

130170
this._menuClosedId = slider.menu.connect('menu-closed', () => {
131-
if (!this._menuSection) return;
171+
if (!this._menuSection || !this._settingsSection) return;
132172
this._menuSection.box.hide();
173+
this._settingsSection.box.hide();
133174
slider._deviceSection?.box.show();
134175
slider.menu._setSettingsVisibility?.(Main.sessionMode.allowSettings);
135176
slider.menu.setHeader('audio-headphones-symbolic', _('Sound Output'));

src/shared/icons.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import Gio from '@girs/gio-2.0';
2+
import { Extension } from '@girs/gnome-shell/extensions/extension';
3+
4+
const ICON_CONTEXTS = [
5+
'apps',
6+
'categories',
7+
'devices',
8+
'emblems',
9+
'mimetypes',
10+
'places',
11+
'status',
12+
];
13+
14+
/**
15+
* Loads a Gio.Icon from either a bundled extension icon name or a file path.
16+
*
17+
* - Icon name (e.g. 'volume-mixer-symbolic'): searches the extension's
18+
* icons/hicolor/scalable/<context>/ directories in order, falling back to
19+
* a system themed icon with that name if none is found.
20+
*
21+
* - File path (starts with '/'): loads the icon directly from disk.
22+
*/
23+
export function loadIcon(nameOrPath: string): Gio.Icon {
24+
if (nameOrPath.startsWith('/')) {
25+
const file = Gio.File.new_for_path(nameOrPath);
26+
if (file.query_exists(null)) {
27+
return new Gio.FileIcon({ file });
28+
}
29+
return Gio.Icon.new_for_string('image-missing-symbolic');
30+
}
31+
32+
// @ts-ignore: Extension.lookupByURL is not properly typed in @girs/gnome-shell
33+
const ext = Extension.lookupByURL(import.meta.url);
34+
if (ext) {
35+
for (const ctx of ICON_CONTEXTS) {
36+
const file = ext.dir.get_child(
37+
`icons/hicolor/scalable/${ctx}/${nameOrPath}.svg`,
38+
);
39+
if (file.query_exists(null)) {
40+
// @ts-ignore: Gio.FileIcon is not properly typed in @girs/gio-2.0
41+
return new Gio.FileIcon({ file });
42+
}
43+
}
44+
}
45+
46+
return Gio.Icon.new_for_string(nameOrPath);
47+
}

src/types/globals.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ declare global {
99
*/
1010
const global: Shell.Global;
1111
}
12+
13+
// GJS ESM modules expose import.meta.url (the file:// URI of the current module)
14+
interface ImportMeta {
15+
url: string;
16+
}

0 commit comments

Comments
 (0)