v0.43.0: visualizer presets

Each visualizer style's settings page has a Preset drop-down and a
"Save as Preset…" button. The drop-down lists the built-in Default, your
presets, and "Edit Presets…" (rename in place, delete); it reads "Custom"
whenever the settings match no preset. Presets sync with the preferences;
the per-machine sync delay is never part of one.
This commit is contained in:
2026-10-10 01:41:01 -07:00
parent 8b42580f97
commit 599972fa1a
6 changed files with 499 additions and 2 deletions
+173
View File
@@ -866,3 +866,176 @@ class TestNewControls:
return sum(QColor(img.pixel(x, y)) != QColor("white")
for x in range(400) for y in range(0, 400, 2))
assert ink(0.5) < ink(1.0) < ink(2.0)
# --------------------------------------------------------------------------
# 8. Presets
# --------------------------------------------------------------------------
from lintunes.gui import visualizer_presets as presets
def _combo_texts(combo):
return [combo.itemText(i) for i in range(combo.count())]
class TestPresets:
def _dialog(self, prefs):
return TestPreferencesPage._dialog(None, prefs)
def test_every_style_has_a_preset_menu(self, qapp, tmp_path):
dialog = self._dialog(Preferences(tmp_path))
for style in ("bars", "radial", "laser"):
combo = dialog._preset_combos[style]
assert combo.currentText() == "Default" # untouched = Default
assert _combo_texts(combo)[-1] == "Edit Presets…"
def test_changing_a_setting_reads_custom(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
dialog = self._dialog(prefs)
dialog._spokes_slider.setValue(60)
assert dialog._preset_combos["radial"].currentText() == "Custom"
assert dialog._preset_combos["laser"].currentText() == "Default"
def test_save_and_apply(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
dialog = self._dialog(prefs)
dialog._angle_sliders["visualizer_laser_pitch"].setValue(40)
dialog._grid.setChecked(True)
dialog._speaker.setValues(150, 300)
dialog._width_box.setValue(3.5)
dialog._ask_preset_name = lambda suggestion: "Big Tilt"
dialog._save_preset("laser")
combo = dialog._preset_combos["laser"]
assert combo.currentText() == "Big Tilt"
# Back to Default: every control (and setting) follows.
dialog._on_preset_chosen("laser", combo.findData("Default"))
assert prefs.get("visualizer_laser_pitch") == 0
assert prefs.get("visualizer_laser_grid") is False
assert prefs.get("visualizer_laser_width") == 1.5
assert dialog._speaker.values() == (0, 300)
assert combo.currentText() == "Default"
dialog._on_preset_chosen("laser", combo.findData("Big Tilt"))
assert prefs.get("visualizer_laser_pitch") == 40
assert prefs.get("visualizer_laser_grid") is True
assert prefs.get("visualizer_laser_width") == 3.5
assert dialog._angle_sliders["visualizer_laser_pitch"].value() == 40
assert dialog._speaker.values() == (150, 300)
assert combo.currentText() == "Big Tilt"
def test_presets_survive_a_restart(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
dialog = self._dialog(prefs)
dialog._bars_fade_slider.setValue(20)
dialog._ask_preset_name = lambda suggestion: "Ghosty"
dialog._save_preset("bars")
again = self._dialog(Preferences(tmp_path))
assert "Ghosty" in _combo_texts(again._preset_combos["bars"])
def test_default_is_reserved(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
dialog = self._dialog(prefs)
warned = []
dialog._preset_warning = warned.append
dialog._ask_preset_name = lambda suggestion: "Default"
dialog._save_preset("radial")
assert warned and presets.presets(prefs, "radial") == {}
def test_overwrite_asks_first(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
dialog = self._dialog(prefs)
dialog._ask_preset_name = lambda suggestion: "Mine"
dialog._save_preset("radial")
dialog._spokes_slider.setValue(200)
dialog._confirm_overwrite = lambda name: False
dialog._save_preset("radial")
assert presets.presets(prefs, "radial")["Mine"][
"visualizer_radial_spokes"] == 120 # kept
dialog._confirm_overwrite = lambda name: True
dialog._save_preset("radial")
assert presets.presets(prefs, "radial")["Mine"][
"visualizer_radial_spokes"] == 200
def test_a_preset_only_holds_its_own_style(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
presets.save(prefs, "bars", "x")
assert set(presets.presets(prefs, "bars")["x"]) == {"visualizer_bars_fade"}
assert "visualizer_sync_delay_ms" not in presets.STYLE_KEYS["laser"]
def test_an_old_preset_takes_new_settings_defaults(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
prefs.set(presets.PRESETS_KEY,
{"laser": {"Old": {"visualizer_laser_pitch": 20}}})
values = presets.values_of(prefs, "laser", "Old")
assert values["visualizer_laser_pitch"] == 20
assert values["visualizer_laser_glow"] is True # default
def test_every_style_key_has_a_control(self, qapp, tmp_path):
dialog = self._dialog(Preferences(tmp_path))
for style, keys in presets.STYLE_KEYS.items():
for key in keys:
assert key in dialog._viz_setters or key in (
"visualizer_laser_low_hz", "visualizer_laser_high_hz")
class TestPresetEditor:
def _editor(self, prefs, style="laser"):
return presets.PresetEditor(prefs, style, "Laser")
def test_lists_only_user_presets(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
editor = self._editor(prefs)
assert editor.list.count() == 0 and not editor.empty.isHidden()
presets.save(prefs, "laser", "b")
presets.save(prefs, "laser", "A")
editor = self._editor(prefs)
assert [editor.list.item(i).text() for i in range(2)] == ["A", "b"]
def test_rename_in_place(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
presets.save(prefs, "laser", "Old")
editor = self._editor(prefs)
editor.list.item(0).setText("New")
assert list(presets.presets(prefs, "laser")) == ["New"]
assert editor.list.item(0).text() == "New"
def test_rename_to_a_taken_name_is_refused(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
presets.save(prefs, "laser", "One")
presets.save(prefs, "laser", "Two")
editor = self._editor(prefs)
warned = []
editor._warn = warned.append
editor.list.item(0).setText("Two")
assert warned
assert sorted(presets.presets(prefs, "laser")) == ["One", "Two"]
assert editor.list.item(0).text() == "One"
def test_rename_to_default_is_refused(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
presets.save(prefs, "laser", "One")
editor = self._editor(prefs)
editor._warn = lambda text: None
editor.list.item(0).setText("Default")
assert list(presets.presets(prefs, "laser")) == ["One"]
def test_delete_asks_then_deletes(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
presets.save(prefs, "laser", "Gone")
editor = self._editor(prefs)
editor._confirm = lambda text: False
editor._delete_current()
assert "Gone" in presets.presets(prefs, "laser")
editor._confirm = lambda text: True
editor._delete_current()
assert presets.presets(prefs, "laser") == {}
assert not editor.delete_button.isEnabled()
def test_editing_one_style_leaves_the_others(self, qapp, tmp_path):
prefs = Preferences(tmp_path)
presets.save(prefs, "bars", "Keep")
presets.save(prefs, "laser", "Drop")
presets.delete(prefs, "laser", "Drop")
assert "Keep" in presets.presets(prefs, "bars")