AbstractManagerWidget Architecture

The Problem: Duplicated Manager Widget Code

Many manager-style widgets implement nearly identical CRUD operations (add, delete, edit, list items) with only domain-specific differences. This duplication creates maintenance burden: fixes must be applied repeatedly and feature additions require parallel edits. Implicit duck-typed hooks also make required subclass contracts unclear.

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 domain behavior (button configs, item hooks, preview fields) as class attributes rather than ad-hoc methods. This eliminates duplication, makes contracts explicit (ABCs), and enables extension without copy/paste.

Overview

The AbstractManagerWidget is a PyQt6 ABC that eliminates duck-typing and code duplication across manager widgets through declarative configuration and the template method pattern.

Architecture Pattern

The ABC uses the template method pattern with declarative configuration:

from pyqt_reactive.widgets.shared.abstract_manager_widget import AbstractManagerWidget

class MyManagerWidget(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 of ButtonConfig objects defining toolbar buttons

  • ITEM_HOOKS: ItemHooks dataclass with lambdas for item access

  • PREVIEW_FIELD_CONFIGS: List of tuples (field_path, formatter, scope_root) for cross-window previews

  • CODE_EDITOR_CONFIG: Optional CodeEditorConfig for 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

Flash Callback Integration

AbstractManagerWidget subscribes to ObjectState change notifications to trigger flash animations when parameters change:

# In _subscribe_flash_for_item()
state = ObjectStateRegistry.get_by_scope(scope_id)

def on_change(changed_paths: Set[str]):
    self.queue_flash(scope_id)  # Trigger flash in all windows
    self.queue_visual_update()   # Refresh list item text

state.on_resolved_changed(on_change)

See Flash Callback System for details on how the flash callback mechanism works.

See Also