Skip to content

Commit eda98ae

Browse files
committed
Enhance curve and output preset handling in D-Bus control and UI
1 parent 9c397e2 commit eda98ae

6 files changed

Lines changed: 202 additions & 11 deletions

File tree

extensions/gnome-shell/mini-eq@bhack.github.io/extension.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,9 @@ class MiniEqIndicator extends PanelMenu.Button {
342342
? Boolean(unpackValue(state.analyzer_enabled))
343343
: true;
344344
const presetName = unpackValue(state.preset_name) || _('Current State');
345+
const curveLabel = unpackValue(state.curve_label) || presetName;
345346
const outputPresetName = unpackValue(state.output_preset_name) || '';
347+
const outputPresetLabel = unpackValue(state.output_preset_label) || outputPresetName;
346348
const capabilities = unpackValue(state.capabilities) || [];
347349
this._capabilities = new Set(Array.isArray(capabilities) ? capabilities : []);
348350
const canQuit = this._capabilities.has('quit');
@@ -367,9 +369,9 @@ class MiniEqIndicator extends PanelMenu.Button {
367369
this._routingItem.setSensitive(running);
368370
this._eqItem.setSensitive(running && routed);
369371
this._presetsItem.setSensitive(running);
370-
this._presetsItem.label.text = running ? _('Preset: %s').format(presetName) : _('Presets');
372+
this._presetsItem.label.text = running ? _('Curve: %s').format(curveLabel) : _('Presets');
371373
this._statusItem.label.text = this._statusText(running, routed, eqEnabled);
372-
this._outputPresetItem.label.text = this._outputPresetText(running, outputPresetName);
374+
this._outputPresetItem.label.text = this._outputPresetText(running, outputPresetLabel);
373375
this._quitItem.visible = running && canQuit;
374376
}
375377

src/mini_eq/dbus_control.py

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@
2424
"output-presets",
2525
"analyzer-levels",
2626
)
27+
CURVE_STATUS_BY_PRESET_STATE = {
28+
"preset": "preset",
29+
"modified": "modified",
30+
"neutral": "neutral",
31+
"unsaved": "unsaved",
32+
}
33+
OUTPUT_PRESET_STATUS_LABELS = {
34+
"applied": "Applied",
35+
"different": "Different",
36+
"linked": "Linked",
37+
"missing": "Missing",
38+
"modified": "Modified",
39+
}
2740

2841
INTROSPECTION_XML = f"""
2942
<node>
@@ -117,6 +130,93 @@ def quit_fully(self) -> None: ...
117130
def get_dbus_connection(self) -> Gio.DBusConnection | None: ...
118131

119132

133+
def curve_status_from_preset_state(value: object) -> str:
134+
return CURVE_STATUS_BY_PRESET_STATE.get(str(value or "").strip().casefold(), "unknown")
135+
136+
137+
def format_curve_label(name: str, status: str) -> str:
138+
label = name.strip() or "Current State"
139+
if status == "modified":
140+
return f"{label} (modified)"
141+
return label
142+
143+
144+
def window_curve_display_state(window: object | None) -> tuple[str, str, str]:
145+
if window is None:
146+
return "", "none", "Current State"
147+
148+
curve_name = ""
149+
curve_status = "unknown"
150+
panel_state_factory = getattr(window, "preset_panel_ui_state", None)
151+
if callable(panel_state_factory):
152+
try:
153+
panel_state = panel_state_factory()
154+
curve_name = str(getattr(panel_state, "current_curve_text", "") or "").strip()
155+
curve_status = curve_status_from_preset_state(getattr(panel_state, "preset_state_text", ""))
156+
except Exception:
157+
curve_name = ""
158+
curve_status = "unknown"
159+
160+
if not curve_name:
161+
curve_name = str(getattr(window, "current_preset_name", "") or "").strip() or "Current State"
162+
if curve_status == "unknown" and getattr(window, "current_preset_name", None):
163+
curve_status = "preset"
164+
165+
return curve_name, curve_status, format_curve_label(curve_name, curve_status)
166+
167+
168+
def window_output_preset_link_name(window: object | None) -> str:
169+
if window is None:
170+
return ""
171+
172+
output_preset_link_name = getattr(window, "output_preset_link_name", None)
173+
if not callable(output_preset_link_name):
174+
return ""
175+
176+
try:
177+
return str(output_preset_link_name() or "").strip()
178+
except Exception:
179+
return ""
180+
181+
182+
def window_preset_name_exists(window: object, preset_name: str) -> bool:
183+
preset_name_exists = getattr(window, "preset_name_exists", None)
184+
if not callable(preset_name_exists):
185+
return True
186+
187+
try:
188+
return bool(preset_name_exists(preset_name))
189+
except Exception:
190+
return True
191+
192+
193+
def window_output_preset_status(window: object | None, preset_name: str) -> str:
194+
if window is None or not preset_name:
195+
return "none"
196+
197+
if not window_preset_name_exists(window, preset_name):
198+
return "missing"
199+
if bool(getattr(window, "output_preset_auto_applied", False)):
200+
return "applied"
201+
202+
current_preset_name = getattr(window, "current_preset_name", None)
203+
if current_preset_name == preset_name:
204+
return "modified"
205+
if current_preset_name:
206+
return "different"
207+
return "linked"
208+
209+
210+
def format_output_preset_label(preset_name: str, status: str) -> str:
211+
if not preset_name:
212+
return ""
213+
214+
status_label = OUTPUT_PRESET_STATUS_LABELS.get(status)
215+
if status_label is None:
216+
return preset_name
217+
return f"{status_label} - {preset_name}"
218+
219+
120220
class MiniEqDbusControl:
121221
def __init__(self, app: ApplicationProtocol) -> None:
122222
self.app = app
@@ -148,11 +248,10 @@ def unregister(self) -> None:
148248
def state(self) -> dict[str, GLib.Variant]:
149249
controller = self.app.controller
150250
window = self.app.window
151-
output_preset_name = ""
152-
if window is not None:
153-
output_preset_link_name = getattr(window, "output_preset_link_name", None)
154-
if output_preset_link_name is not None:
155-
output_preset_name = output_preset_link_name() or ""
251+
curve_name, curve_status, curve_label = window_curve_display_state(window)
252+
output_preset_name = window_output_preset_link_name(window)
253+
output_preset_status = window_output_preset_status(window, output_preset_name)
254+
output_preset_label = format_output_preset_label(output_preset_name, output_preset_status)
156255

157256
return {
158257
"api_version": GLib.Variant("u", API_VERSION),
@@ -164,8 +263,13 @@ def state(self) -> dict[str, GLib.Variant]:
164263
"preset_name": GLib.Variant(
165264
"s", window.current_preset_name if window and window.current_preset_name else ""
166265
),
266+
"curve_name": GLib.Variant("s", curve_name),
267+
"curve_status": GLib.Variant("s", curve_status),
268+
"curve_label": GLib.Variant("s", curve_label),
167269
"output_sink": GLib.Variant("s", controller.output_sink if controller and controller.output_sink else ""),
168270
"output_preset_name": GLib.Variant("s", output_preset_name),
271+
"output_preset_status": GLib.Variant("s", output_preset_status),
272+
"output_preset_label": GLib.Variant("s", output_preset_label),
169273
"output_preset_auto_applied": GLib.Variant(
170274
"b",
171275
bool(window and getattr(window, "output_preset_auto_applied", False)),

src/mini_eq/window_graph.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,9 @@ def on_curve_metadata_refresh_idle(self) -> bool:
177177

178178
self.update_status_summary()
179179
self.update_preset_state()
180+
notify_control_state_changed = getattr(self, "notify_control_state_changed", None)
181+
if callable(notify_control_state_changed):
182+
notify_control_state_changed()
180183
return False
181184

182185
def schedule_band_engine_update(self, index: int) -> None:
@@ -461,7 +464,7 @@ def on_custom_band_mute_toggled(self, index: int, muted: bool) -> None:
461464
self.update_status_summary()
462465
self.invalidate_graph_response_cache()
463466
self.queue_response_draw()
464-
self.update_preset_state()
467+
self.schedule_curve_metadata_refresh()
465468

466469
def on_custom_band_solo_toggled(self, index: int, soloed: bool) -> None:
467470
if self.updating_ui:
@@ -480,7 +483,7 @@ def on_custom_band_solo_toggled(self, index: int, soloed: bool) -> None:
480483
self.update_status_summary()
481484
self.invalidate_graph_response_cache()
482485
self.queue_response_draw()
483-
self.update_preset_state()
486+
self.schedule_curve_metadata_refresh()
484487

485488
def on_selected_band_type_changed(self, combo: Gtk.DropDown, _param: object) -> None:
486489
if self.updating_ui:
@@ -505,7 +508,7 @@ def on_selected_band_type_changed(self, combo: Gtk.DropDown, _param: object) ->
505508
self.update_status_summary()
506509
self.invalidate_graph_response_cache()
507510
self.queue_response_draw()
508-
self.update_preset_state()
511+
self.schedule_curve_metadata_refresh()
509512

510513
def on_selected_band_frequency_changed(self, spin: Gtk.SpinButton) -> None:
511514
if self.updating_ui:

tests/test_mini_eq_dbus_control.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,11 @@ def __init__(self, controller: FakeController) -> None:
9898
self.route_switch = FakeSwitch(controller.routed)
9999
self.loaded_presets: list[str] = []
100100
self.update_count = 0
101+
self.current_curve_text = "Flat"
102+
self.preset_state_text = "Preset"
103+
self.output_preset_link = "Headphones"
101104
self.output_preset_auto_applied = False
105+
self.existing_presets = {"Flat", "Headphones"}
102106
self.visible = True
103107

104108
def sync_control_switches_from_controller(self, *, route: bool = True, eq: bool = True) -> None:
@@ -148,10 +152,21 @@ def refresh_after_eq_state_changed(
148152

149153
def load_library_preset(self, name: str) -> None:
150154
self.current_preset_name = name
155+
self.current_curve_text = name
156+
self.preset_state_text = "Preset"
151157
self.loaded_presets.append(name)
152158

153159
def output_preset_link_name(self) -> str | None:
154-
return "Headphones"
160+
return self.output_preset_link
161+
162+
def preset_panel_ui_state(self) -> SimpleNamespace:
163+
return SimpleNamespace(
164+
current_curve_text=self.current_curve_text,
165+
preset_state_text=self.preset_state_text,
166+
)
167+
168+
def preset_name_exists(self, preset_name: str) -> bool:
169+
return preset_name in self.existing_presets
155170

156171
def present(self) -> None:
157172
pass
@@ -214,8 +229,13 @@ def test_dbus_control_state_contains_shell_summary() -> None:
214229
"eq_enabled": True,
215230
"routed": False,
216231
"preset_name": "Flat",
232+
"curve_name": "Flat",
233+
"curve_status": "preset",
234+
"curve_label": "Flat",
217235
"output_sink": "alsa_output.test",
218236
"output_preset_name": "Headphones",
237+
"output_preset_status": "different",
238+
"output_preset_label": "Different - Headphones",
219239
"output_preset_auto_applied": False,
220240
"analyzer_enabled": False,
221241
"background_mode": True,
@@ -225,6 +245,46 @@ def test_dbus_control_state_contains_shell_summary() -> None:
225245
}
226246

227247

248+
def test_dbus_control_state_marks_modified_curve_for_shell() -> None:
249+
control, _controller, window = make_control()
250+
window.current_curve_text = "Flat"
251+
window.preset_state_text = "Modified"
252+
253+
state = {key: value.unpack() for key, value in control.state().items()}
254+
255+
assert state["curve_name"] == "Flat"
256+
assert state["curve_status"] == "modified"
257+
assert state["curve_label"] == "Flat (modified)"
258+
259+
260+
@pytest.mark.parametrize(
261+
("current_preset_name", "auto_applied", "existing_presets", "status", "label"),
262+
[
263+
("Headphones", True, {"Headphones"}, "applied", "Applied - Headphones"),
264+
("Headphones", False, {"Headphones"}, "modified", "Modified - Headphones"),
265+
("Flat", False, {"Flat", "Headphones"}, "different", "Different - Headphones"),
266+
(None, False, {"Headphones"}, "linked", "Linked - Headphones"),
267+
("Flat", False, {"Flat"}, "missing", "Missing - Headphones"),
268+
],
269+
)
270+
def test_dbus_control_state_describes_output_preset_for_shell(
271+
current_preset_name: str | None,
272+
auto_applied: bool,
273+
existing_presets: set[str],
274+
status: str,
275+
label: str,
276+
) -> None:
277+
control, _controller, window = make_control()
278+
window.current_preset_name = current_preset_name
279+
window.output_preset_auto_applied = auto_applied
280+
window.existing_presets = existing_presets
281+
282+
state = {key: value.unpack() for key, value in control.state().items()}
283+
284+
assert state["output_preset_status"] == status
285+
assert state["output_preset_label"] == label
286+
287+
228288
def test_dbus_control_compacts_analyzer_levels_for_shell_signal() -> None:
229289
control, _controller, window = make_control()
230290
window.analyzer_enabled = True

tests/test_mini_eq_window_graph.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,3 +289,20 @@ def test_curve_metadata_refresh_updates_preset_state_with_pending_idle(monkeypat
289289

290290
assert calls == ["preset-state"]
291291
assert test_window.curve_metadata_refresh_source_id == 42
292+
293+
294+
def test_curve_metadata_refresh_idle_notifies_control_clients() -> None:
295+
calls: list[str] = []
296+
test_window = SimpleNamespace(
297+
curve_metadata_refresh_source_id=42,
298+
ui_shutting_down=False,
299+
update_status_summary=lambda: calls.append("status"),
300+
update_preset_state=lambda: calls.append("preset-state"),
301+
notify_control_state_changed=lambda: calls.append("control-state"),
302+
)
303+
304+
keep_source = window_graph.MiniEqWindowGraphMixin.on_curve_metadata_refresh_idle(test_window)
305+
306+
assert keep_source is False
307+
assert test_window.curve_metadata_refresh_source_id == 0
308+
assert calls == ["status", "preset-state", "control-state"]

tools/gnome-shell-extension/fake_mini_eq_control.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,13 @@ def state(self) -> dict[str, GLib.Variant]:
119119
"eq_enabled": GLib.Variant("b", self.eq_enabled),
120120
"routed": GLib.Variant("b", self.routed),
121121
"preset_name": GLib.Variant("s", self.preset_name),
122+
"curve_name": GLib.Variant("s", self.preset_name),
123+
"curve_status": GLib.Variant("s", "preset"),
124+
"curve_label": GLib.Variant("s", self.preset_name),
122125
"output_sink": GLib.Variant("s", "Demo Output"),
123126
"output_preset_name": GLib.Variant("s", self.output_preset_name),
127+
"output_preset_status": GLib.Variant("s", "applied"),
128+
"output_preset_label": GLib.Variant("s", f"Applied - {self.output_preset_name}"),
124129
"output_preset_auto_applied": GLib.Variant("b", True),
125130
"analyzer_enabled": GLib.Variant("b", self.analyzer_enabled),
126131
"background_mode": GLib.Variant("b", True),

0 commit comments

Comments
 (0)