Files
pyPhotoAlbum/pyPhotoAlbum/decorators.py
T
dtourolle fae9e5bd2b
Python CI / test (push) Successful in 1m17s
Lint / lint (push) Successful in 1m32s
Tests / test (3.10) (push) Successful in 1m10s
Tests / test (3.9) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Additional refactoring
2025-11-27 21:57:57 +01:00

426 lines
14 KiB
Python

"""
Decorator system for pyPhotoAlbum ribbon UI
"""
import copy
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
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)
class DialogAction:
"""
Decorator to mark methods that should open a dialog.
This decorator automatically handles dialog creation and result processing,
separating UI presentation from business logic.
Example:
@dialog_action(dialog_class=PageSetupDialog)
def page_setup(self, values):
# Just implement the business logic
# Dialog presentation is handled automatically
self.apply_page_setup(values)
"""
def __init__(
self,
dialog_class: type,
requires_pages: bool = True
):
"""
Initialize the dialog action decorator.
Args:
dialog_class: The dialog class to instantiate
requires_pages: Whether this action requires pages to exist
"""
self.dialog_class = dialog_class
self.requires_pages = requires_pages
def __call__(self, func: Callable) -> Callable:
"""
Decorate the function with automatic dialog handling.
Args:
func: The function to decorate (receives dialog values)
Returns:
The decorated function
"""
@wraps(func)
def wrapper(self_instance, *args, **kwargs):
# Check preconditions
if self.requires_pages and not self_instance.project.pages:
return
# Get initial page index if available
initial_page_index = 0
if hasattr(self_instance, '_get_most_visible_page_index'):
initial_page_index = self_instance._get_most_visible_page_index()
# Create and show dialog
from pyPhotoAlbum.mixins.dialog_mixin import DialogMixin
# Create dialog
dialog = self.dialog_class(
parent=self_instance,
project=self_instance.project,
initial_page_index=initial_page_index,
**kwargs
)
# Show dialog and get result
from PyQt6.QtWidgets import QDialog
if dialog.exec() == QDialog.DialogCode.Accepted:
# Get values from dialog
if hasattr(dialog, 'get_values'):
values = dialog.get_values()
# Call the decorated function with values
return func(self_instance, values, *args, **kwargs)
else:
return func(self_instance, *args, **kwargs)
return None
return wrapper
def dialog_action(
dialog_class: type,
requires_pages: bool = True
) -> Callable:
"""
Convenience function for the DialogAction decorator.
This provides a lowercase function-style interface to the decorator.
Args:
dialog_class: The dialog class to instantiate
requires_pages: Whether this action requires pages to exist
Returns:
DialogAction decorator instance
"""
return DialogAction(dialog_class=dialog_class, requires_pages=requires_pages)