Dialog Provider =============== .. currentmodule:: wolfhece.dialog_provider Overview -------- :class:`DialogProvider` is the central abstraction for all user-facing dialogs in the WOLF viewer. Instead of calling ``wx`` dialog classes directly, every component that needs to interact with the user does so through a ``DialogProvider`` instance stored as ``self._dialogs``. .. code-block:: text Business logic DialogProvider interface wx back-end ───────────────── ──────────────────────── ─────────── PyDraw / any view ──► self._dialogs.ask_yes_no() ──► wx.MessageDialog self._dialogs.ask_file_open()──► wx.FileDialog … … This indirection has two benefits: * **Testability** — unit tests inject a :class:`MockDialogProvider ` that never opens a real window. Responses are scripted in advance, and every call is recorded for later assertion. * **Portability** — the ``wx`` dependency is confined to a single module. Replacing the back-end (e.g. Qt, CLI, web) only requires a new subclass. .. note:: The convention ``self._dialogs.(parent, ...)`` replaces every direct ``wx.(parent, ...)`` call site. Do not bypass it. Class reference --------------- DialogProvider ~~~~~~~~~~~~~~ .. class:: DialogProvider Wrapper around common ``wx`` dialogs. All methods accept an optional *parent* window that is passed directly to the underlying wx constructor. File and directory dialogs ^^^^^^^^^^^^^^^^^^^^^^^^^^ .. method:: ask_file_open(message, wildcard="all (*.*)|*.*", default_path="", style=None, parent=None) -> str | None Open a *File → Open* dialog. Returns the selected path, or ``None`` if the user cancelled. :param message: Dialog title / prompt shown to the user. :param wildcard: Pipe-separated filter string (wx format), e.g. ``"Images (*.png)|*.png|All files (*.*)|*.*"``. :param default_path: Directory shown when the dialog opens. :param style: wx style flags; defaults to ``wx.FD_OPEN``. .. method:: ask_file_open_with_filter(message, wildcard="all (*.*)|*.*", default_path="", style=None, parent=None) -> tuple[str, int] | None Same as :meth:`ask_file_open`, but also returns the index of the selected wildcard filter. Returns ``(path, filter_index)`` or ``None`` on cancel. .. method:: ask_file_save(message, wildcard="all (*.*)|*.*", default_path="", default_file="", style=None, parent=None) -> str | None Open a *File → Save* dialog. Returns the path chosen by the user, or ``None`` on cancel. The default style is ``wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT``. .. method:: ask_directory(message, default_path="", style=wx.DD_DEFAULT_STYLE, parent=None) -> str | None Open a directory-chooser dialog. Returns the selected directory path, or ``None`` on cancel. Message dialogs ^^^^^^^^^^^^^^^ .. method:: show_message(message, caption="", style=DialogStyles.OK_INFO, parent=None) -> int Show a non-blocking informational (or warning/error) dialog with an **OK** button. Returns the wx modal return code (``wx.ID_OK``). .. method:: ask_yes_no(message, caption="", style=DialogStyles.YES_NO_QUESTION, parent=None) -> bool Ask the user a yes/no question. Returns ``True`` if the user clicked **Yes**, ``False`` otherwise. .. method:: ask_ok_cancel(message, caption="", style=DialogStyles.OK_CANCEL, parent=None) -> bool Show an **OK / Cancel** dialog. Returns ``True`` if the user clicked **OK**. .. method:: ask_confirmation(message, caption="", default="yes", parent=None) -> bool Convenience wrapper around :meth:`ask_yes_no` that sets the default focused button. :param default: ``"yes"`` (default) focuses the **Yes** button; ``"no"`` focuses the **No** button. Choice dialogs ^^^^^^^^^^^^^^ .. method:: ask_single_choice(message, caption, choices, preselected=None, style=wx.CHOICEDLG_STYLE, parent=None) -> str | None Show a list of items and let the user pick exactly one. Returns the selected string, or ``None`` on cancel. :param choices: Sequence of option strings. :param preselected: Index of the item selected by default. .. method:: ask_multi_choice(message, caption, choices, preselected=None, style=wx.CHOICEDLG_STYLE, parent=None) -> list[int] | None Show a list of items and let the user pick multiple items. Returns a list of selected **indices**, or ``None`` on cancel. :param preselected: Sequence of indices pre-checked when the dialog opens. Text and number entry ^^^^^^^^^^^^^^^^^^^^^ .. method:: ask_text(message, caption="", default="", style=DialogStyles.OK_CANCEL, parent=None) -> str | None Prompt the user for a text string. Returns the entered string, or ``None`` on cancel. .. method:: ask_float(message, caption="", default="", style=DialogStyles.OK_CANCEL, parent=None) -> float | None Prompt the user for a floating-point number (built on top of :meth:`ask_text`). Returns the parsed ``float``, or ``None`` on cancel or invalid input. .. method:: ask_integer(message, prompt="", caption="", default=0, min_value=0, max_value=0, parent=None) -> int | None Prompt the user for an integer using ``wx.NumberEntryDialog``. *min_value* and *max_value* bound the accepted range. Returns the entered integer, or ``None`` on cancel. Progress dialogs ^^^^^^^^^^^^^^^^ .. method:: create_progress(title, message, maximum, style=wx.PD_APP_MODAL|wx.PD_AUTO_HIDE, parent=None) -> ProgressHandle Create and display a determinate progress bar. Returns a :class:`ProgressHandle` that wraps the underlying dialog. Typical usage:: progress = self._dialogs.create_progress("Processing", "Loading…", 100) for i, item in enumerate(items): process(item) if not progress.update(i + 1): break # user clicked Cancel progress.close() .. method:: show_busy(message, title="", parent=None) Context manager that displays an indeterminate progress (pulse) dialog while the ``with`` block executes. The dialog is automatically destroyed on exit. :: with self._dialogs.show_busy("Recomputing…"): expensive_operation() ProgressHandle ~~~~~~~~~~~~~~ .. class:: ProgressHandle Thin wrapper returned by :meth:`DialogProvider.create_progress`. .. method:: update(value, message=None) -> bool Advance the bar to *value*. Pass *message* to change the displayed text. Returns ``False`` if the user clicked **Cancel**, ``True`` otherwise. .. method:: value() -> int Return the current progress value. .. method:: close() Destroy the underlying ``wx.ProgressDialog``. DialogStyles ~~~~~~~~~~~~ .. class:: DialogStyles Named constants that map to common ``wx`` style flag combinations. Use these instead of raw ``wx`` flags for clarity and to decouple call sites from the ``wx`` namespace. .. list-table:: :header-rows: 1 :widths: 35 65 * - Constant - Equivalent wx flags * - ``OK`` - ``wx.OK`` * - ``CANCEL`` - ``wx.CANCEL`` * - ``OK_CANCEL`` - ``wx.OK | wx.CANCEL`` * - ``ICON_INFORMATION`` - ``wx.ICON_INFORMATION`` * - ``ICON_QUESTION`` - ``wx.ICON_QUESTION`` * - ``ICON_WARNING`` - ``wx.ICON_WARNING`` * - ``ICON_ERROR`` - ``wx.ICON_ERROR`` * - ``YES_NO`` - ``wx.YES_NO`` * - ``YES_NO_QUESTION`` - ``wx.YES_NO | wx.ICON_QUESTION`` * - ``YES_NO_DEFAULT_YES`` - ``wx.YES_NO | wx.YES_DEFAULT`` * - ``YES_NO_DEFAULT_NO`` - ``wx.YES_NO | wx.NO_DEFAULT`` * - ``YES_NO_QUESTION_DEFAULT_YES`` - ``wx.YES_NO | wx.ICON_QUESTION | wx.YES_DEFAULT`` * - ``YES_NO_QUESTION_DEFAULT_NO`` - ``wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT`` * - ``OK_INFO`` - ``wx.OK | wx.ICON_INFORMATION`` * - ``OK_CANCEL_WARNING`` - ``wx.OK | wx.CANCEL | wx.ICON_WARNING`` MockDialogProvider (testing) ----------------------------- .. currentmodule:: tests.mock_dialog_provider .. class:: MockDialogProvider Scriptable subclass of :class:`~wolfhece.dialog_provider.DialogProvider` intended exclusively for unit tests. It never opens any real window. All dialog methods are overridden to pull scripted answers from a per-method FIFO queue. If a method is called but the queue is empty, an ``AssertionError`` is raised immediately, making test failures explicit. Every call is appended to :attr:`calls` for post-call assertion. .. attribute:: calls : list[dict] Ordered list of every dialog call recorded since the mock was created. Each entry is a ``dict`` with a ``"method"`` key and one key per argument passed to the method. .. method:: push(method_name, *responses) Queue one or more scripted responses for the named method. Responses are consumed in FIFO order. :param method_name: Name of the :class:`~wolfhece.dialog_provider.DialogProvider` method to script (e.g. ``"ask_yes_no"``). :param responses: Values to return, consumed one per call. :: mock = MockDialogProvider() mock.push("ask_yes_no", True) # first call → True mock.push("ask_yes_no", False, True) # second → False, third → True ``show_busy`` is also overridden: it records the call and yields ``None`` without displaying anything. Usage in tests -------------- A typical test: 1. Creates a :class:`MockDialogProvider` and pushes scripted answers. 2. Injects the mock as ``component._dialogs``. 3. Calls the component method under test. 4. Asserts on the result and optionally inspects ``mock.calls``. .. code-block:: python from tests.mock_dialog_provider import MockDialogProvider def test_save_aborts_on_cancel(viewer): mock = MockDialogProvider() mock.push("ask_file_save", None) # simulate Cancel viewer._dialogs = mock viewer.save_results() assert mock.calls[0]["method"] == "ask_file_save" # no file was written assert not output_path.exists() def test_overwrite_confirmed(viewer, tmp_path): mock = MockDialogProvider() mock.push("ask_file_save", str(tmp_path / "out.bin")) mock.push("ask_yes_no", True) # confirm overwrite viewer._dialogs = mock viewer.save_results() assert (tmp_path / "out.bin").exists() .. tip:: Always call :meth:`~MockDialogProvider.push` for every dialog that the code path under test will trigger. An uncaught ``AssertionError`` from the mock is a strong signal that the production code opened an unexpected dialog. Migration guide --------------- When converting a direct ``wx`` call to use ``DialogProvider``: 1. Identify the ``wx`` dialog call and its return value usage. 2. Replace it with the matching ``self._dialogs.(...)`` call. 3. Add a test that pushes a scripted response and verifies behaviour. 4. Do **not** change any business logic during the replacement. .. list-table:: :header-rows: 1 :widths: 50 50 * - Before - After * - ``wx.FileDialog(parent, msg, style=wx.FD_OPEN)`` - ``self._dialogs.ask_file_open(msg, parent=parent)`` * - ``wx.FileDialog(parent, msg, style=wx.FD_SAVE)`` - ``self._dialogs.ask_file_save(msg, parent=parent)`` * - ``wx.DirDialog(parent, msg)`` - ``self._dialogs.ask_directory(msg, parent=parent)`` * - ``wx.MessageDialog(parent, msg, caption, style=wx.YES_NO)`` - ``self._dialogs.ask_yes_no(msg, caption, parent=parent)`` * - ``wx.MessageDialog(parent, msg, caption, style=wx.OK)`` - ``self._dialogs.show_message(msg, caption, parent=parent)`` * - ``wx.TextEntryDialog(parent, msg, caption)`` - ``self._dialogs.ask_text(msg, caption, parent=parent)`` * - ``wx.SingleChoiceDialog(parent, msg, caption, choices)`` - ``self._dialogs.ask_single_choice(msg, caption, choices, parent=parent)`` * - ``wx.NumberEntryDialog(parent, msg, prompt, caption, default, lo, hi)`` - ``self._dialogs.ask_integer(msg, prompt, caption, default, lo, hi, parent=parent)`` * - ``wx.ProgressDialog(title, msg, maximum=n)`` - ``self._dialogs.create_progress(title, msg, n)``