pyPhotoAlbum/pyPhotoAlbum/decorators.py
Duncan Tourolle 5257c6654a
All checks were successful
Python CI / test (push) Successful in 55s
Lint / lint (push) Successful in 1m4s
Tests / test (3.10) (push) Successful in 42s
Tests / test (3.11) (push) Successful in 45s
Tests / test (3.9) (push) Successful in 41s
Improved undo-redo system
2025-10-28 21:03:30 +01:00

329 lines
11 KiB
Python

"""
Decorator system for pyPhotoAlbum ribbon UI
"""
from functools import wraps
from typing import Optional, Callable
class RibbonAction:
"""
Decorator to mark methods as ribbon actions.
This decorator stores metadata about UI actions that should appear in the ribbon.
The metadata is used to auto-generate the ribbon configuration.
Example:
@RibbonAction(
label="New",
tooltip="Create a new project",
tab="Home",
group="File",
icon="new.png",
shortcut="Ctrl+N"
)
def new_project(self):
...
"""
def __init__(
self,
label: str,
tooltip: str,
tab: str,
group: str,
icon: Optional[str] = None,
shortcut: Optional[str] = None,
requires_page: bool = False,
requires_selection: bool = False,
min_selection: int = 0
):
"""
Initialize the ribbon action decorator.
Args:
label: Button label text
tooltip: Tooltip text shown on hover
tab: Ribbon tab name (e.g., "Home", "Insert", "Layout")
group: Group name within the tab (e.g., "File", "Edit")
icon: Optional icon filename or path
shortcut: Optional keyboard shortcut (e.g., "Ctrl+N", "Ctrl+Shift+S")
requires_page: Whether this action requires an active page
requires_selection: Whether this action requires selected elements
min_selection: Minimum number of selected elements required
"""
self.label = label
self.tooltip = tooltip
self.tab = tab
self.group = group
self.icon = icon
self.shortcut = shortcut
self.requires_page = requires_page
self.requires_selection = requires_selection
self.min_selection = min_selection
def __call__(self, func: Callable) -> Callable:
"""
Decorate the function with ribbon action metadata.
Args:
func: The function to decorate
Returns:
The decorated function with metadata attached
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Store metadata on wrapper function
wrapper._ribbon_action = {
'label': self.label,
'tooltip': self.tooltip,
'tab': self.tab,
'group': self.group,
'icon': self.icon,
'shortcut': self.shortcut,
'action': func.__name__,
'requires_page': self.requires_page,
'requires_selection': self.requires_selection,
'min_selection': self.min_selection
}
return wrapper
def ribbon_action(
label: str,
tooltip: str,
tab: str,
group: str,
icon: Optional[str] = None,
shortcut: Optional[str] = None,
requires_page: bool = False,
requires_selection: bool = False,
min_selection: int = 0
) -> Callable:
"""
Convenience function for the RibbonAction decorator.
This provides a lowercase function-style interface to the decorator.
Args:
label: Button label text
tooltip: Tooltip text shown on hover
tab: Ribbon tab name
group: Group name within the tab
icon: Optional icon filename or path
shortcut: Optional keyboard shortcut
requires_page: Whether this action requires an active page
requires_selection: Whether this action requires selected elements
min_selection: Minimum number of selected elements required
Returns:
RibbonAction decorator instance
"""
return RibbonAction(
label=label,
tooltip=tooltip,
tab=tab,
group=group,
icon=icon,
shortcut=shortcut,
requires_page=requires_page,
requires_selection=requires_selection,
min_selection=min_selection
)
class NumericalInput:
"""
Decorator to mark methods that require numerical width/height inputs.
This decorator stores metadata about numerical input fields that should
be presented in dialogs for methods that work with page dimensions.
Example:
@numerical_input(
fields=[
('width', 'Width', 'mm', 10, 1000),
('height', 'Height', 'mm', 10, 1000)
]
)
def set_page_size(self, width, height):
...
"""
def __init__(self, fields: list):
"""
Initialize the numerical input decorator.
Args:
fields: List of tuples, each containing:
(param_name, label, unit, min_value, max_value)
"""
self.fields = fields
def __call__(self, func: Callable) -> Callable:
"""
Decorate the function with numerical input metadata.
Args:
func: The function to decorate
Returns:
The decorated function with metadata attached
"""
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
# Store metadata on wrapper function
wrapper._numerical_input = {
'fields': self.fields
}
return wrapper
def numerical_input(fields: list) -> Callable:
"""
Convenience function for the NumericalInput decorator.
This provides a lowercase function-style interface to the decorator.
Args:
fields: List of tuples, each containing:
(param_name, label, unit, min_value, max_value)
Returns:
NumericalInput decorator instance
"""
return NumericalInput(fields=fields)
class UndoableOperation:
"""
Decorator to automatically create undo/redo commands for operations.
This decorator captures state before and after an operation, then creates
a StateChangeCommand for undo/redo functionality.
Example:
@undoable_operation(capture='page_elements')
def apply_template(self):
# Just implement the operation
self.template_manager.apply_template(...)
# Decorator handles undo/redo automatically
"""
def __init__(self, capture: str = 'page_elements', description: str = None):
"""
Initialize the undoable operation decorator.
Args:
capture: What to capture for undo/redo:
- 'page_elements': Capture elements of current page
- 'custom': Operation provides its own capture logic
description: Human-readable description (defaults to function name)
"""
self.capture = capture
self.description = description
def __call__(self, func: Callable) -> Callable:
"""
Decorate the function with automatic undo/redo.
Args:
func: The function to decorate
Returns:
The decorated function
"""
@wraps(func)
def wrapper(self_instance, *args, **kwargs):
# Get description
description = self.description or func.__name__.replace('_', ' ').title()
# Capture before state
before_state = self._capture_state(self_instance, self.capture)
# Execute the operation
result = func(self_instance, *args, **kwargs)
# Capture after state
after_state = self._capture_state(self_instance, self.capture)
# Create restore function
def restore_state(state):
self._restore_state(self_instance, self.capture, state)
# Update view after restoring
if hasattr(self_instance, 'update_view'):
self_instance.update_view()
# Create and execute command
from pyPhotoAlbum.commands import StateChangeCommand
cmd = StateChangeCommand(description, restore_state, before_state, after_state)
if hasattr(self_instance, 'project') and hasattr(self_instance.project, 'history'):
self_instance.project.history.execute(cmd)
print(f"Undoable operation '{description}' executed")
return result
return wrapper
def _capture_state(self, instance, capture_type: str):
"""Capture current state based on capture type"""
if capture_type == 'page_elements':
# Capture elements from current page
current_page = instance.get_current_page() if hasattr(instance, 'get_current_page') else None
if current_page:
# Deep copy elements
import copy
return [copy.deepcopy(elem.serialize()) for elem in current_page.layout.elements]
return []
return None
def _restore_state(self, instance, capture_type: str, state):
"""Restore state based on capture type"""
if capture_type == 'page_elements':
# Restore elements to current page
current_page = instance.get_current_page() if hasattr(instance, 'get_current_page') else None
if current_page and state is not None:
# Clear existing elements
current_page.layout.elements.clear()
# Restore elements from serialized state
from pyPhotoAlbum.models import ImageData, PlaceholderData, TextBoxData
for elem_data in state:
elem_type = elem_data.get('type')
if elem_type == 'image':
elem = ImageData()
elif elem_type == 'placeholder':
elem = PlaceholderData()
elif elem_type == 'textbox':
elem = TextBoxData()
else:
continue
elem.deserialize(elem_data)
current_page.layout.add_element(elem)
def undoable_operation(capture: str = 'page_elements', description: str = None) -> Callable:
"""
Convenience function for the UndoableOperation decorator.
This provides a lowercase function-style interface to the decorator.
Args:
capture: What to capture for undo/redo
description: Human-readable description of the operation
Returns:
UndoableOperation decorator instance
"""
return UndoableOperation(capture=capture, description=description)