329 lines
11 KiB
Python
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)
|