Source code for wolfhece.dialog_provider

from __future__ import annotations

import logging
from contextlib import contextmanager
from dataclasses import dataclass
from typing import Literal

import wx


@dataclass
[docs] class ProgressHandle: """Simple wrapper around wx.ProgressDialog for easier mocking in tests."""
[docs] dialog: wx.ProgressDialog
[docs] def update(self, value: int, message: str | None = None) -> bool: """Update progress and return wx's continuation flag.""" if message is None: cont, _ = self.dialog.Update(value) else: cont, _ = self.dialog.Update(value, message) return bool(cont)
[docs] def value(self) -> int: """Return current progress value.""" return int(self.dialog.GetValue())
[docs] def close(self) -> None: """Destroy the wrapped progress dialog.""" self.dialog.Destroy()
[docs] class DialogStyles: """Named style presets used across dialog helpers and call sites."""
[docs] OK = wx.OK
[docs] CANCEL = wx.CANCEL
[docs] OK_CANCEL = wx.OK | wx.CANCEL
[docs] ICON_INFORMATION = wx.ICON_INFORMATION
[docs] ICON_QUESTION = wx.ICON_QUESTION
[docs] ICON_WARNING = wx.ICON_WARNING
[docs] ICON_ERROR = wx.ICON_ERROR
[docs] YES_NO = wx.YES_NO
[docs] YES_NO_QUESTION = wx.YES_NO | wx.ICON_QUESTION
[docs] YES_NO_DEFAULT_YES = wx.YES_NO | wx.YES_DEFAULT
[docs] YES_NO_DEFAULT_NO = wx.YES_NO | wx.NO_DEFAULT
[docs] YES_NO_QUESTION_DEFAULT_YES = wx.YES_NO | wx.ICON_QUESTION | wx.YES_DEFAULT
[docs] YES_NO_QUESTION_DEFAULT_NO = wx.YES_NO | wx.ICON_QUESTION | wx.NO_DEFAULT
[docs] OK_INFO = wx.OK | wx.ICON_INFORMATION
[docs] OK_CANCEL_WARNING = wx.OK | wx.CANCEL | wx.ICON_WARNING
[docs] class DialogProvider: """Wrapper around common wx dialogs used in the viewer. This class centralizes dialog interactions so tests can inject a mock implementation with deterministic answers. """
[docs] def ask_file_open( self, message: str, wildcard: str = "all (*.*)|*.*", default_path: str = "", style: int | None = None, parent=None, ) -> str | None: if style is None: style = wx.FD_OPEN dlg = wx.FileDialog(parent, message, defaultDir=default_path, wildcard=wildcard, style=style) if dlg.ShowModal() == wx.ID_CANCEL: dlg.Destroy() return None path = dlg.GetPath() dlg.Destroy() return path
[docs] def ask_file_open_with_filter( self, message: str, wildcard: str = "all (*.*)|*.*", default_path: str = "", style: int | None = None, parent=None, ) -> tuple[str, int] | None: """Open a file dialog and return (path, filter_index) or None on cancel.""" if style is None: style = wx.FD_OPEN dlg = wx.FileDialog(parent, message, defaultDir=default_path, wildcard=wildcard, style=style) if dlg.ShowModal() == wx.ID_CANCEL: dlg.Destroy() return None path = dlg.GetPath() filter_index = int(dlg.GetFilterIndex()) dlg.Destroy() return path, filter_index
[docs] def ask_file_save( self, message: str, wildcard: str = "all (*.*)|*.*", default_path: str = "", default_file: str = "", style: int | None = None, parent=None, ) -> str | None: if style is None: style = wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT dlg = wx.FileDialog( parent, message, defaultDir=default_path, defaultFile=default_file, wildcard=wildcard, style=style, ) if dlg.ShowModal() == wx.ID_CANCEL: dlg.Destroy() return None path = dlg.GetPath() dlg.Destroy() return path
[docs] def ask_directory( self, message: str, default_path: str = "", style: int = wx.DD_DEFAULT_STYLE, parent=None, ) -> str | None: dlg = wx.DirDialog(parent, message, defaultPath=default_path, style=style) if dlg.ShowModal() == wx.ID_CANCEL: dlg.Destroy() return None path = dlg.GetPath() dlg.Destroy() return path
[docs] def ask_yes_no( self, message: str, caption: str = "", style: int = DialogStyles.YES_NO_QUESTION, parent=None, ) -> bool: dlg = wx.MessageDialog(parent, message, caption, style=style) ret = dlg.ShowModal() dlg.Destroy() return ret == wx.ID_YES
[docs] def show_message( self, message: str, caption: str = "", style: int = DialogStyles.OK_INFO, parent=None, ) -> int: dlg = wx.MessageDialog(parent, message, caption, style=style) ret = dlg.ShowModal() dlg.Destroy() return ret
[docs] def ask_ok_cancel( self, message: str, caption: str = "", style: int = DialogStyles.OK_CANCEL, parent=None, ) -> bool: dlg = wx.MessageDialog(parent, message, caption, style=style) ret = dlg.ShowModal() dlg.Destroy() return ret == wx.ID_OK
[docs] def ask_single_choice( self, message: str, caption: str, choices: list[str], preselected: int | None = None, style: int = wx.CHOICEDLG_STYLE, parent=None, ) -> str | None: dlg = wx.SingleChoiceDialog(parent, message, caption, choices, style=style) if preselected is not None: dlg.SetSelection(preselected) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return None selection = dlg.GetStringSelection() dlg.Destroy() return selection
[docs] def ask_multi_choice( self, message: str, caption: str, choices: list[str], preselected: list[int] | tuple[int, ...] | None = None, style: int = wx.CHOICEDLG_STYLE, parent=None, ) -> list[int] | None: dlg = wx.MultiChoiceDialog(parent, message, caption, choices, style=style) if preselected: dlg.SetSelections(list(preselected)) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return None selection = list(dlg.GetSelections()) dlg.Destroy() return selection
[docs] def ask_text( self, message: str, caption: str = "", default: str = "", style: int = DialogStyles.OK_CANCEL, parent=None, ) -> str | None: dlg = wx.TextEntryDialog(parent, message, caption, value=default, style=style) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return None value = dlg.GetValue() dlg.Destroy() return value
[docs] def ask_float( self, message: str, caption: str = "", default: float | str = "", style: int = DialogStyles.OK_CANCEL, parent=None, ) -> float | None: value = self.ask_text(message, caption, default=str(default), style=style, parent=parent) if value is None: return None try: return float(value) except (TypeError, ValueError): logging.warning("Invalid float value for %s: %r", caption or message, value) return None
[docs] def ask_integer( self, message: str, prompt: str = "", caption: str = "", default: int = 0, min_value: int = 0, max_value: int = 0, parent=None, ) -> int | None: dlg = wx.NumberEntryDialog(parent, message, prompt, caption, default, min_value, max_value) if dlg.ShowModal() != wx.ID_OK: dlg.Destroy() return None try: value = int(dlg.GetValue()) except (TypeError, ValueError): logging.warning("Invalid integer value for %s: %r", caption or message, dlg.GetValue()) dlg.Destroy() return None dlg.Destroy() return value
[docs] def create_progress( self, title: str, message: str, maximum: int, style: int = wx.PD_APP_MODAL | wx.PD_AUTO_HIDE, parent=None, ) -> ProgressHandle: dlg = wx.ProgressDialog(title, message, maximum=maximum, parent=parent, style=style) return ProgressHandle(dlg)
@contextmanager
[docs] def show_busy(self, message: str, title: str = '', parent=None): """Context manager that shows an indeterminate progress bar while work runs.""" dlg = wx.ProgressDialog( title or message, message, maximum=100, parent=parent, style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE, ) dlg.Pulse() try: yield dlg finally: dlg.Destroy()
[docs] def ask_confirmation( self, message: str, caption: str = "", default: Literal["yes", "no"] = "yes", parent=None, ) -> bool: style = ( DialogStyles.YES_NO_QUESTION_DEFAULT_YES if default == "yes" else DialogStyles.YES_NO_QUESTION_DEFAULT_NO ) return self.ask_yes_no(message, caption=caption, style=style, parent=parent)