"""Declarative menu and action types for companion plugins."""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum, IntEnum
from typing import TYPE_CHECKING, Callable, Optional, Union
from .._action_kind import ActionKind
if TYPE_CHECKING:
from .._viewer_plugin_handlers import MouseContext
[docs]
class Keys(IntEnum):
"""Complete set of key codes, mirroring ``wx.WXK_*`` values.
Use these constants in key handlers instead of raw integers to avoid any
dependency on wx being imported at module level::
def _key(self, kb: KeyboardSnapshot) -> bool | StepTransition:
if kb.key_code == Keys.ESCAPE:
return StepTransition.CANCEL
if kb.key_code == Keys.CONTROL_Z:
self._undo()
return False
Aliases (same integer value as another member) are listed with a comment.
"""
# --- Control characters (Ctrl+letter, values 0–27) ---
[docs]
BACK = 8 # Backspace key (WXK_BACK); CONTROL_H shares this value
[docs]
BACKSPACE = 8 # alias for BACK
[docs]
TAB = 9 # also CONTROL_I
[docs]
RETURN = 13 # also CONTROL_M
# --- Special / system keys ---
[docs]
CONTROL = 308 # also COMMAND, RAW_CONTROL
[docs]
CAPITAL = 311 # Caps Lock
# --- Navigation ---
# --- Numpad digits ---
# --- Arithmetic operators (main keyboard) ---
# --- Function keys ---
# --- Lock keys ---
# --- Page navigation ---
# --- Extended numpad ---
[docs]
NUMPAD_PAGEUP = 380
[docs]
NUMPAD_PAGEDOWN = 381
# --- Windows keys ---
# --- OEM / special purpose ---
# --- Browser keys ---
[docs]
BROWSER_FAVORITES = 422
# --- Volume / media ---
# --- Launch keys ---
[docs]
LAUNCH_A = 442 # also LAUNCH_APP1
[docs]
LAUNCH_B = 443 # also LAUNCH_APP2
# --- Letter keys (ASCII ordinal of the uppercase letter) ---
# wx key events report letters as their uppercase ASCII value (65–90)
# regardless of Shift state; use kb.shift to distinguish case.
# --- Digit keys (ASCII ordinal) ---
@dataclass
[docs]
class Separator:
"""Singleton sentinel that inserts a separator line in menus."""
[docs]
_instance: 'Separator | None' = None
def __new__(cls) -> 'Separator':
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __repr__(self) -> str:
return 'SEPARATOR'
[docs]
SEPARATOR: Separator = Separator()
@dataclass
@dataclass
[docs]
class ActionSpec:
"""Declarative interactive-action entry."""
[docs]
action_id: str | ActionKind
[docs]
rdown: Optional[Callable] = None
[docs]
motion: Optional[Callable] = None
[docs]
ldown: Optional[Callable] = None
[docs]
key: Optional[Callable] = None
[docs]
paint: Optional[Callable] = None
[docs]
start_message: str = ''
[docs]
class StepTransition(str, Enum):
"""Transition directives returned by multi-step handlers."""
@dataclass
[docs]
class StepSpec:
"""Single step declaration used by :class:`MultiStepSpec`."""
[docs]
rdown: Optional[Callable] = None
[docs]
motion: Optional[Callable] = None
[docs]
ldown: Optional[Callable] = None
[docs]
key: Optional[Callable] = None
[docs]
paint: Optional[Callable] = None
@dataclass
[docs]
class MultiStepSpec:
"""Declarative multi-step interactive action.
Phase 1 behaviour:
- startup selection is declarative (``primary`` + ``start_message``),
- action registration uses handlers from the first step.
"""
[docs]
action_id: str | ActionKind
[docs]
start_message: str = ''
[docs]
finish_message: str = ''
def __post_init__(self) -> None:
if not self.steps:
raise ValueError('MultiStepSpec.steps must contain at least one StepSpec')
@property
[docs]
def first_step(self) -> StepSpec:
return self.steps[0]
@property
[docs]
def effective_start_message(self) -> str:
return self.start_message or self.first_step.hint