AbstractManagerWidget Architecture
The Problem: Duplicated Manager Widget Code
PlateManagerWidget and PipelineEditorWidget implement nearly identical CRUD operations (add, delete, edit, list items) with only domain-specific differences. This duplication (~1000 lines) creates maintenance burden: bug fixes must be applied twice, and adding new features requires changes in multiple places. Additionally, both widgets use duck-typing (implicit interfaces), making it hard to understand what methods subclasses must implement.
The Solution: Template Method Pattern with Declarative Configuration
AbstractManagerWidget uses the template method pattern to define the CRUD workflow once, with declarative configuration via class attributes. Subclasses specify their domain-specific behavior (button configs, item hooks, preview fields) as class attributes rather than implementing methods. This eliminates duplication, makes the interface explicit (ABC contracts), and enables easy extension.
Overview
The AbstractManagerWidget is a PyQt6 ABC that eliminates duck-typing and code duplication
between PlateManagerWidget and PipelineEditorWidget through declarative configuration
and the template method pattern.
Architecture Pattern
The ABC uses the template method pattern with declarative configuration:
from openhcs.pyqt_gui.widgets.shared.abstract_manager_widget import AbstractManagerWidget
class PipelineEditorWidget(AbstractManagerWidget):
# Declarative configuration via class attributes
TITLE = "Pipeline Editor"
BUTTON_CONFIGS = [
ButtonConfig(text="Add Step", action="add", icon="plus"),
ButtonConfig(text="Delete Step", action="delete", icon="trash"),
ButtonConfig(text="Edit Step", action="edit", icon="edit"),
]
ITEM_HOOKS = ItemHooks(
get_items=lambda self: self.pipeline_steps,
set_items=lambda self, items: setattr(self, 'pipeline_steps', items),
get_selected_index=lambda self: self.step_list.currentRow(),
)
PREVIEW_FIELD_CONFIGS = [
('napari_streaming_config.enabled', lambda v: 'NAP' if v else None, 'step'),
('fiji_streaming_config.enabled', lambda v: 'FIJI' if v else None, 'step'),
]
# Implement abstract hooks for domain-specific behavior
def _perform_delete(self, index: int) -> None:
"""Delete step at index."""
del self.pipeline_steps[index]
def _show_item_editor(self, item: Any, index: int) -> None:
"""Show step editor dialog."""
dialog = StepEditorDialog(item, parent=self)
dialog.exec()
def _format_list_item(self, item: Any, index: int) -> str:
"""Format step for display in list."""
return f"{index + 1}. {item.name}"
Declarative Configuration
Class Attributes:
TITLE: Widget title (str)BUTTON_CONFIGS: List ofButtonConfigobjects defining toolbar buttonsITEM_HOOKS:ItemHooksdataclass with lambdas for item accessPREVIEW_FIELD_CONFIGS: List of tuples(field_path, formatter, scope_root)for cross-window previewsCODE_EDITOR_CONFIG: OptionalCodeEditorConfigfor code editing support
ButtonConfig:
@dataclass
class ButtonConfig:
text: str
action: str # Maps to action_{action} method
icon: Optional[str] = None
tooltip: Optional[str] = None
ItemHooks:
@dataclass
class ItemHooks:
get_items: Callable[[Any], List[Any]]
set_items: Callable[[Any, List[Any]], None]
get_selected_index: Callable[[Any], int]
get_item_at_index: Optional[Callable[[Any, int], Any]] = None
Template Methods
The ABC provides template methods that orchestrate the workflow:
CRUD Operations:
action_add(): Add new item (calls_create_new_item()hook)action_delete(): Delete selected item (calls_perform_delete()hook)action_edit(): Edit selected item (calls_show_item_editor()hook)update_item_list(): Refresh list widget (calls_format_list_item()hook)
Code Editing:
action_view_code(): Show code editor dialog_handle_edited_code(code): Execute edited code and apply to widget state
Cross-Window Previews:
_init_cross_window_preview_mixin(): Initialize preview system_process_pending_preview_updates(): Apply incremental preview updates
Abstract Hooks
Subclasses must implement these abstract methods:
@abstractmethod
def _perform_delete(self, index: int) -> None:
"""Delete item at index."""
...
@abstractmethod
def _show_item_editor(self, item: Any, index: int) -> None:
"""Show editor dialog for item."""
...
@abstractmethod
def _format_list_item(self, item: Any, index: int) -> str:
"""Format item for display in list widget."""
...
@abstractmethod
def _get_context_stack_for_resolution(self) -> List[Any]:
"""Get context stack for lazy config resolution."""
...
Optional hooks with default implementations:
_create_new_item() -> Any: Create new item (default: None)_get_code_editor_title() -> str: Code editor title (default: “Code Editor”)_apply_extracted_variables(vars: Dict[str, Any]): Apply code execution results
See Also
Widget Protocol System - ABC contracts for widget operations
UI Services Architecture - Service layer for ParameterFormManager
GUI Performance Patterns - Cross-window preview system
Batch Workflow Service - Unified compile/execute orchestration service
ZMQ Server Browser System - ZMQ browser abstraction and OpenHCS adapter
Parametric Widget Creation - Widget creation configuration