From d783dfe9c203b986a00cbf86edaf4ceda0dcda73 Mon Sep 17 00:00:00 2001 From: smb Date: Mon, 7 Sep 2026 23:33:50 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(inspector):=20let=20persiste?= =?UTF-8?q?d=20parameters=20be=20put=20back?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 🐛 #27 fixed the wheel-scroll bug that changed whatever field the pointer passed over; the damage it left could not be undone. Inspector parameters persist in QSettings, and nothing in the interface reset them — an fps of 11 and an alpha threshold of 0.4 that nobody chose followed the user into every later session and every later clip. - 🐛 Validation cannot catch this: `parameters_from_values` range-checks each persisted value, and every scrolled value is *inside* its range. They are legal values the user did not choose. - ✨ A "Reset to defaults" action in the inspector, beside the parameters it resets, describing its own scope — the creative parameters, not the output directory and not the machine settings that now live in Preferences. - 🔄 The reset travels through the reducer as a parameter event, so it invalidates a stale preview and persists like every other change instead of reaching into QSettings behind the store's back. Trigger: repro — scroll the inspector panel on a build before #27, then restart: the values stay. Closes #56 --- resources/matteloop_de.qm | Bin 58261 -> 58773 bytes resources/matteloop_de.ts | 109 ++++++++++++++----------- resources/matteloop_en.qm | Bin 51741 -> 52215 bytes resources/matteloop_en.ts | 109 ++++++++++++++----------- src/matteloop/core/parameters.py | 41 ++++++++++ src/matteloop/ui/inspector.py | 20 ++--- src/matteloop/ui/inspector_reset.py | 30 +++++++ tests/core/test_parameters.py | 74 ++++++++++++++++- tests/ui/test_parameter_inspector.py | 95 ++++++++++++++++++++- tests/ui/test_parameter_persistence.py | 57 ++++++++++++- 10 files changed, 426 insertions(+), 109 deletions(-) create mode 100644 src/matteloop/ui/inspector_reset.py diff --git a/resources/matteloop_de.qm b/resources/matteloop_de.qm index 74cf4754041798c6fbde4f601b0fd7aede810bf8..76fb4560ff20a8a478e30b8e0b5d2402343f3b74 100644 GIT binary patch delta 4141 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 delta 3802 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 diff --git a/resources/matteloop_de.ts b/resources/matteloop_de.ts index 6bc6434..0981f08 100644 --- a/resources/matteloop_de.ts +++ b/resources/matteloop_de.ts @@ -118,13 +118,13 @@ - + Edge treatment Kantenbehandlung - + Output FPS Ausgabe-FPS @@ -158,7 +158,7 @@ - + Horizontal stretch Horizontale Streckung @@ -178,206 +178,221 @@ Maximale Größe - + Processing settings Verarbeitungseinstellungen - + Retry Rebuild Neuaufbau wiederholen - + Edited cut recovery Wiederherstellung bearbeiteter Schnitte - + Edited cut frames could not be validated. Retry the rebuild scan. Bearbeitete Schnittbilder konnten nicht validiert werden. Wiederholen Sie die Neuaufbauprüfung. - - + + Rebuild from edited cuts Aus bearbeiteten Schnitten neu aufbauen - - + + Manage Models… Modelle verwalten… - - + + Manage Workspaces… Arbeitsbereiche verwalten… - + + Reset to defaults + Auf Standardwerte zurücksetzen + + + + Reset inspector parameters + Inspector-Parameter zurücksetzen + + + + Compute acceleration, output directory, and output filename are not affected. + Rechenbeschleunigung, Ausgabeordner und Ausgabedateiname bleiben unverändert. + + + Segmentation model Segmentierungsmodell - + cached locally lokal zwischengespeichert - + not cached yet noch nicht zwischengespeichert - + Model status Modellstatus - + Model download Modelldownload - + Standard Standard - + Decontaminate colors Farben bereinigen - + fps FPS - + High output FPS may increase render cost Hohe Ausgabe-FPS können den Renderaufwand erhöhen - + Output FPS cost warning Warnung zum Aufwand der Ausgabe-FPS - + Global trim Globaler Randbeschnitt - + Global alpha trim Globaler Alpha-Beschnitt - + Padding pixels Randpixel - + Output directory Ausgabeordner - + Choose… Auswählen… - + Choose output directory Ausgabeordner auswählen - + Clear Zurücksetzen - + Clear output directory Ausgabeordner zurücksetzen - + Output filename Ausgabedateiname - - + + Use one non-empty filename ending in .webp; path separators are not allowed. Verwenden Sie einen einzelnen, nicht leeren Dateinamen mit der Endung .webp; Pfadtrenner sind nicht erlaubt. - + Maximum file size Maximale Dateigröße - + Unlimited Unbegrenzt - + MiB MiB - + Unlimited means no maximum file size. Unbegrenzt bedeutet: Es gibt keine maximale Dateigröße. - + %s — %s download required %s — %s Download erforderlich - + Filename must be a single non-empty .webp filename. Der Dateiname muss ein einzelner, nicht leerer .webp-Dateiname sein. - + s s - + Crop Zuschnitt - + Crop overlay Zuschnitt-Overlay - + Reset Crop Zuschnitt zurücksetzen - + Reset crop Zuschnitt zurücksetzen - + Crop %s Zuschnitt %s - + Model status: %s Modellstatus: %s diff --git a/resources/matteloop_en.qm b/resources/matteloop_en.qm index 1e24097a9031ae3b3042df36a6ac2d95e46f7957..bd24081b8e2ee5b60ff658d8770cea4d7f5c640f 100644 GIT binary patch delta 4149 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 diff --git a/resources/matteloop_en.ts b/resources/matteloop_en.ts index 8bd5e12..9dcf27a 100644 --- a/resources/matteloop_en.ts +++ b/resources/matteloop_en.ts @@ -118,13 +118,13 @@ - + Edge treatment Edge treatment - + Output FPS Output FPS @@ -158,7 +158,7 @@ - + Horizontal stretch Horizontal stretch @@ -178,206 +178,221 @@ Maximum size - + Processing settings Processing settings - + Retry Rebuild Retry Rebuild - + Edited cut recovery Edited cut recovery - + Edited cut frames could not be validated. Retry the rebuild scan. Edited cut frames could not be validated. Retry the rebuild scan. - - + + Rebuild from edited cuts Rebuild from edited cuts - - + + Manage Models… Manage Models… - - + + Manage Workspaces… Manage Workspaces… - + + Reset to defaults + Reset to defaults + + + + Reset inspector parameters + Reset inspector parameters + + + + Compute acceleration, output directory, and output filename are not affected. + Compute acceleration, output directory, and output filename are not affected. + + + Segmentation model Segmentation model - + cached locally cached locally - + not cached yet not cached yet - + Model status Model status - + Model download Model download - + Standard Standard - + Decontaminate colors Decontaminate colors - + fps fps - + High output FPS may increase render cost High output FPS may increase render cost - + Output FPS cost warning Output FPS cost warning - + Global trim Global trim - + Global alpha trim Global alpha trim - + Padding pixels Padding pixels - + Output directory Output directory - + Choose… Choose… - + Choose output directory Choose output directory - + Clear Clear - + Clear output directory Clear output directory - + Output filename Output filename - - + + Use one non-empty filename ending in .webp; path separators are not allowed. Use one non-empty filename ending in .webp; path separators are not allowed. - + Maximum file size Maximum file size - + Unlimited Unlimited - + MiB MiB - + Unlimited means no maximum file size. Unlimited means no maximum file size. - + %s — %s download required %s — %s download required - + Filename must be a single non-empty .webp filename. Filename must be a single non-empty .webp filename. - + s s - + Crop Crop - + Crop overlay Crop overlay - + Reset Crop Reset Crop - + Reset crop Reset crop - + Crop %s Crop %s - + Model status: %s Model status: %s diff --git a/src/matteloop/core/parameters.py b/src/matteloop/core/parameters.py index e70265c..a9a1e12 100644 --- a/src/matteloop/core/parameters.py +++ b/src/matteloop/core/parameters.py @@ -144,6 +144,11 @@ class OutputMaxSizeChanged: value: Decimal +@dataclass(frozen=True, slots=True) +class ParametersReset: + pass + + @dataclass(frozen=True, slots=True) class TransformChanged: transform: TransformSpec @@ -161,6 +166,7 @@ class TransformChanged: | OutputDirectoryChanged | OutputFilenameChanged | OutputMaxSizeChanged + | ParametersReset | TransformChanged ) @@ -174,6 +180,8 @@ def reduce_parameters(state: AppState, event: ParameterEvent) -> AppState: return state elif not capabilities(state).can_edit: return state + if isinstance(event, ParametersReset): + return _reduce_parameters_reset(state) if isinstance(event, ModelChanged): return _reduce_model(state, event) if isinstance(event, EdgeModeChanged): @@ -201,6 +209,39 @@ def reduce_parameters(state: AppState, event: ParameterEvent) -> AppState: return state +def _reduce_parameters_reset(state: AppState) -> AppState: + from matteloop.core.tokens import PreviewInvalidationReason + + defaults = ParameterState() + parameters = replace( + state.parameters, + model_id=defaults.model_id, + edge_mode=defaults.edge_mode, + fps=defaults.fps, + trim=defaults.trim, + alpha_threshold=defaults.alpha_threshold, + padding=defaults.padding, + stretch_x=defaults.stretch_x, + max_mib=defaults.max_mib, + ) + timeline = state.timeline + if timeline is not None and timeline.fps != defaults.fps: + try: + timeline = timeline.set_fps(defaults.fps) + except ValueError: + return state + if parameters == state.parameters and timeline == state.timeline: + return state + model_available = state.model_available + if parameters.model_id != state.parameters.model_id: + model_available = False + return _invalidate( + replace(state, timeline=timeline, model_available=model_available), + parameters, + PreviewInvalidationReason.CROP_CLEANUP, + ) + + def _reduce_model(state: AppState, event: ModelChanged) -> AppState: from matteloop.core.state import PreviewInvalidated, reduce from matteloop.core.tokens import PreviewInvalidationReason diff --git a/src/matteloop/ui/inspector.py b/src/matteloop/ui/inspector.py index 8ed6e08..f8425f6 100644 --- a/src/matteloop/ui/inspector.py +++ b/src/matteloop/ui/inspector.py @@ -65,6 +65,7 @@ ) from matteloop.ui.crop_presentation import CropPresentation from matteloop.ui.inspector_disclosure import configure_disclosure, read_bool +from matteloop.ui.inspector_reset import build_reset_button from matteloop.ui.parameter_presentation import ( ParameterPresentation, decimal_from_widget_value, @@ -170,6 +171,7 @@ def _build_parameter_controls(self) -> None: self._build_sampling_parameter_controls() self._build_cleanup_parameter_controls() self._build_output_parameter_controls() + self.reset_parameters_button = build_reset_button(self.command_requested.emit) self._connect_parameter_controls() def _build_segmentation_parameter_controls(self) -> None: @@ -506,6 +508,7 @@ def apply_parameters( self.clear_output_directory_button, self.output_filename_edit, self.max_size_spinbox, + self.reset_parameters_button, ): widget.setEnabled(available) self.fps_warning.setVisible(available and presentation.fps > 60) @@ -764,6 +767,7 @@ def _cleanup_controls(self) -> QWidget: layout.addRow(self._form_label("Alpha threshold"), self.alpha_threshold_spinbox) layout.addRow(self._form_label("Padding"), self.padding_spinbox) layout.addRow(self._form_label("Horizontal stretch"), self.stretch_spinbox) + layout.addRow(self._form_label(""), self.reset_parameters_button) return controls def _output_controls(self) -> QWidget: @@ -825,37 +829,33 @@ def parameter_tab_widgets(self) -> tuple[QWidget, ...]: self.clear_output_directory_button, self.output_filename_edit, self.max_size_spinbox, + self.reset_parameters_button, ) def tab_widgets(self) -> tuple[QWidget, ...]: """Return the full inspector tab route in consequence order.""" return ( self.disclosures["segmentation"][0], - self.model_picker, - self.edge_picker, + self.model_picker, self.edge_picker, self.manage_models, self.disclosures["time_sampling"][0], - self.fps_spinbox, - self.start_spinbox, - self.end_spinbox, + self.fps_spinbox, self.start_spinbox, self.end_spinbox, self.duration_spinbox, self.disclosures["crop_cleanup"][0], *self.crop_tab_widgets(), - self.trim_checkbox, - self.alpha_threshold_spinbox, - self.padding_spinbox, + self.trim_checkbox, self.alpha_threshold_spinbox, self.padding_spinbox, self.stretch_spinbox, self.disclosures["transform"][0], *self.transform_group.tab_widgets(), self.disclosures["output"][0], - self.output_directory_button, - self.clear_output_directory_button, + self.output_directory_button, self.clear_output_directory_button, self.output_filename_edit, self.max_size_spinbox, self.disclosures["workspace"][0], self.edited_cut_recovery, self.rebuild_button, self.manage_workspaces, + self.reset_parameters_button, ) def crop_tab_widgets(self) -> tuple[QWidget, ...]: diff --git a/src/matteloop/ui/inspector_reset.py b/src/matteloop/ui/inspector_reset.py new file mode 100644 index 0000000..9aefd43 --- /dev/null +++ b/src/matteloop/ui/inspector_reset.py @@ -0,0 +1,30 @@ +"""Inspector action for restoring creative parameter defaults.""" + +from __future__ import annotations + +from collections.abc import Callable + +from PySide6.QtCore import QCoreApplication +from PySide6.QtWidgets import QPushButton + +from matteloop.core.parameters import ParametersReset + + +def build_reset_button(emit: Callable[[object], None]) -> QPushButton: + """Build the reset action with an honest description of its scope.""" + button = QPushButton( + QCoreApplication.translate("Inspector", "Reset to defaults") + ) + button.setObjectName("reset_parameters") + button.setAccessibleName( + QCoreApplication.translate("Inspector", "Reset inspector parameters") + ) + scope = QCoreApplication.translate( + "Inspector", + "Compute acceleration, output directory, and output filename are not " + "affected.", + ) + button.setToolTip(scope) + button.setAccessibleDescription(scope) + button.clicked.connect(lambda: emit(ParametersReset())) + return button diff --git a/tests/core/test_parameters.py b/tests/core/test_parameters.py index 522789b..30642b9 100644 --- a/tests/core/test_parameters.py +++ b/tests/core/test_parameters.py @@ -1,6 +1,6 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, replace from decimal import Decimal from fractions import Fraction from pathlib import Path @@ -19,12 +19,13 @@ OutputFpsChanged, OutputMaxSizeChanged, PaddingChanged, + ParametersReset, ParameterState, StretchChanged, TransformChanged, parameters_from_values, ) -from matteloop.core.specs import EdgeMode, TransformSpec +from matteloop.core.specs import CropSpec, EdgeMode, TransformSpec from matteloop.core.state import ( AppState, PreviewInvalidationReason, @@ -163,6 +164,75 @@ def test_output_fps_changes_sampling_and_stales_the_current_preview() -> None: assert changed.stale_category is PreviewInvalidationReason.SAMPLING +def test_reset_restores_inspector_parameters_and_preserves_other_state() -> None: + current = _current() + assert current.timeline is not None + parameters = ParameterState( + model_id="u2net", + edge_mode=EdgeMode.DECONTAMINATE_COLORS, + execution_provider="CUDAExecutionProvider", + fps=90, + trim=True, + alpha_threshold=Decimal("0.4"), + padding=1, + stretch_x=Decimal("1.2"), + output_directory=Path("exports"), + output_filename="chosen.webp", + max_mib=Decimal("12.5"), + transform=TransformSpec(first_frame=1, crop=CropSpec(4, 5, 80, 70)), + ) + timeline = replace( + current.timeline, + start=Fraction(1, 2), + end=Fraction(7, 2), + playhead=Fraction(1), + fps=90, + ) + state = replace( + current, + parameters=parameters, + timeline=timeline, + crop=CropSpec(8, 9, 100, 90), + crop_enabled=False, + ) + + reset = reduce(state, ParametersReset()) + defaults = ParameterState() + + for name in ( + "model_id", + "edge_mode", + "fps", + "trim", + "alpha_threshold", + "padding", + "stretch_x", + "max_mib", + ): + assert getattr(reset.parameters, name) == getattr(defaults, name) + assert reset.parameters.execution_provider == "CUDAExecutionProvider" + assert reset.parameters.output_directory == Path("exports") + assert reset.parameters.output_filename == "chosen.webp" + assert reset.parameters.transform == parameters.transform + assert reset.source_id == state.source_id + assert reset.source_value is state.source_value + assert reset.crop == state.crop + assert reset.crop_enabled is state.crop_enabled + assert reset.timeline is not None + assert reset.timeline.start == state.timeline.start + assert reset.timeline.end == state.timeline.end + assert reset.timeline.playhead == state.timeline.playhead + assert reset.timeline.fps == defaults.fps + assert reset.preview is PreviewState.STALE + assert reset.stale_category is PreviewInvalidationReason.CROP_CLEANUP + + +def test_reset_parameters_is_ignored_while_a_job_runs() -> None: + running = reduce(_ready(), RenderRequested("job", "request")) + + assert reduce(running, ParametersReset()) is running + + def test_output_parameter_changes_keep_a_current_preview_current() -> None: state = _current() diff --git a/tests/ui/test_parameter_inspector.py b/tests/ui/test_parameter_inspector.py index db97d70..fcfe090 100644 --- a/tests/ui/test_parameter_inspector.py +++ b/tests/ui/test_parameter_inspector.py @@ -8,8 +8,9 @@ from PySide6.QtCore import QSettings, Qt from PySide6.QtWidgets import QAbstractSpinBox, QComboBox -from matteloop.core.parameters import ParameterState -from matteloop.core.state import AppState +from matteloop.core.parameters import ParametersReset, ParameterState +from matteloop.core.specs import CropSpec, EdgeMode, TransformSpec +from matteloop.core.state import AppState, SourceState from matteloop.core.timeline import TimelineState from matteloop.ui.aligned_rows import ( ACCESSIBLE_DESCRIPTION_ROLE, @@ -17,8 +18,10 @@ STATUS_ROLE, RowStatus, ) +from matteloop.ui.controller import SourceController from matteloop.ui.inspector import Inspector from matteloop.ui.parameter_presentation import present_parameters +from matteloop.ui.store import ReducerStore def _settings() -> QSettings: @@ -268,6 +271,94 @@ def test_inspector_emits_edge_mode_from_the_standard_combo(qtbot) -> None: assert [type(command).__name__ for command in commands] == ["EdgeModeChanged"] +def test_inspector_exposes_reset_action_and_explains_what_it_preserves(qtbot) -> None: + inspector = Inspector(_settings()) + qtbot.addWidget(inspector) + commands: list[object] = [] + inspector.command_requested.connect(commands.append) + + assert inspector.reset_parameters_button.text() == "Reset to defaults" + assert inspector.reset_parameters_button.accessibleName() == ( + "Reset inspector parameters" + ) + assert inspector.reset_parameters_button.toolTip() == ( + "Compute acceleration, output directory, and output filename are not affected." + ) + + inspector.reset_parameters_button.click() + + assert commands == [ParametersReset()] + + +def test_inspector_reset_updates_widgets_without_disturbing_other_editors( + qtbot, +) -> None: + settings = _settings() + parameters = ParameterState( + model_id="u2net", + edge_mode=EdgeMode.DECONTAMINATE_COLORS, + execution_provider="CUDAExecutionProvider", + fps=90, + trim=True, + alpha_threshold=Decimal("0.4"), + padding=1, + stretch_x=Decimal("1.2"), + output_directory=Path("exports"), + output_filename="chosen.webp", + max_mib=Decimal("12.5"), + transform=TransformSpec(first_frame=1, crop=CropSpec(4, 5, 80, 70)), + ) + timeline = TimelineState( + Fraction(4), Fraction(1, 2), Fraction(7, 2), Fraction(1), fps=90 + ) + state = AppState( + source=SourceState.READY, + source_id="source", + source_value=object(), + timeline=timeline, + parameters=parameters, + crop=CropSpec(8, 9, 100, 90), + crop_enabled=False, + ) + store = ReducerStore(state) + controller = SourceController.__new__(SourceController) + controller._store = store + controller._settings = settings + controller._working_provider = parameters.execution_provider + controller._failed_provider = None + controller._closed = False + inspector = Inspector(settings) + qtbot.addWidget(inspector) + + def render(updated: AppState) -> None: + inspector.apply_parameters(present_parameters(updated), editable=True) + + store.subscribe(render) + inspector.command_requested.connect(controller.dispatch) + inspector.apply_parameters(present_parameters(state), editable=True) + inspector.reset_parameters_button.click() + + defaults = ParameterState() + assert inspector.model_picker.currentData() == defaults.model_id + assert inspector.edge_picker.currentData() == defaults.edge_mode.value + assert inspector.fps_spinbox.value() == defaults.fps + assert inspector.trim_checkbox.isChecked() is defaults.trim + assert inspector.alpha_threshold_spinbox.value() == float(defaults.alpha_threshold) + assert inspector.padding_spinbox.value() == defaults.padding + assert inspector.stretch_spinbox.value() == float(defaults.stretch_x) + assert inspector.max_size_spinbox.value() == float(defaults.max_mib) + assert store.state.parameters.execution_provider == parameters.execution_provider + assert store.state.parameters.output_directory == parameters.output_directory + assert store.state.parameters.output_filename == parameters.output_filename + assert store.state.parameters.transform == parameters.transform + assert store.state.crop == state.crop + assert store.state.crop_enabled is state.crop_enabled + assert store.state.timeline is not None + assert store.state.timeline.start == timeline.start + assert store.state.timeline.end == timeline.end + assert store.state.timeline.playhead == timeline.playhead + + def test_inspector_exposes_and_emits_output_directory_clear(qtbot) -> None: inspector = Inspector(_settings()) qtbot.addWidget(inspector) diff --git a/tests/ui/test_parameter_persistence.py b/tests/ui/test_parameter_persistence.py index d31c336..ab173ee 100644 --- a/tests/ui/test_parameter_persistence.py +++ b/tests/ui/test_parameter_persistence.py @@ -10,7 +10,11 @@ CUDA_EXECUTION_PROVIDER, ProviderOption, ) -from matteloop.core.parameters import OutputFilenameChanged, ParameterState +from matteloop.core.parameters import ( + OutputFilenameChanged, + ParametersReset, + ParameterState, +) from matteloop.core.specs import CropSpec, EdgeMode, TransformSpec from matteloop.core.state import AppState, SourceState from matteloop.ui.controller import SourceController @@ -103,3 +107,54 @@ def test_failed_provider_is_not_reintroduced_by_a_later_parameter_save() -> None controller._dispatch_parameter(OutputFilenameChanged("next.webp")) assert settings.value("parameters/execution_provider") == CPU_EXECUTION_PROVIDER + + +def test_reset_persists_inspector_defaults_without_overwriting_excluded_choices( +) -> None: + settings = _settings() + saved = ParameterState( + model_id="u2net", + edge_mode=EdgeMode.DECONTAMINATE_COLORS, + execution_provider=CUDA_EXECUTION_PROVIDER, + fps=90, + trim=True, + alpha_threshold=Decimal("0.4"), + padding=1, + stretch_x=Decimal("1.2"), + output_directory=Path("exports"), + output_filename="chosen.webp", + max_mib=Decimal("12.5"), + ) + persist_parameters(settings, saved) + store = ReducerStore( + AppState( + source=SourceState.READY, + source_id="source", + source_value=object(), + parameters=saved, + ) + ) + controller = SourceController.__new__(SourceController) + controller._store = store + controller._settings = settings + controller._working_provider = CUDA_EXECUTION_PROVIDER + controller._failed_provider = None + + controller._dispatch_parameter(ParametersReset()) + actual = load_parameters(settings) + defaults = ParameterState() + + for name in ( + "model_id", + "edge_mode", + "fps", + "trim", + "alpha_threshold", + "padding", + "stretch_x", + "max_mib", + ): + assert getattr(actual, name) == getattr(defaults, name) + assert actual.execution_provider == saved.execution_provider + assert actual.output_directory == saved.output_directory + assert actual.output_filename == saved.output_filename