@@ -231,6 +231,8 @@ def __init__(self, service_adapter, color_scheme=None, gui_config=None, parent=N
231231 self .gui_config = gui_config or self ._get_default_gui_config ()
232232 self .style_generator = StyleSheetGenerator (self .color_scheme ) # Create internally
233233 self .event_bus = service_adapter .get_event_bus () if service_adapter else None
234+ self .code_execution_workflow = None
235+ self .deletion_workflow = None
234236
235237 # UI components (created in setup_ui)
236238 self .buttons : Dict [str , QPushButton ] = {}
@@ -278,7 +280,7 @@ def __init__(self, service_adapter, color_scheme=None, gui_config=None, parent=N
278280 self ._init_cross_window_preview_mixin ()
279281
280282 # Register time travel callback for list refresh
281- ObjectStateRegistry .add_time_travel_complete_callback (self ._on_time_travel_complete )
283+ ObjectStateRegistry .add_time_travel_complete_callback (self .on_time_travel_complete )
282284
283285 def _get_default_gui_config (self ):
284286 """Get default GUI config fallback."""
@@ -463,7 +465,7 @@ def _on_registry_register(self, scope_key: str, state: 'ObjectState') -> None:
463465
464466 backing_items = self ._get_backing_items ()
465467 if item not in backing_items :
466- insert_idx = self ._get_item_insert_index (item , scope_key )
468+ insert_idx = self .get_item_insert_index (item , scope_key )
467469 if insert_idx is not None :
468470 backing_items .insert (insert_idx , item )
469471 else :
@@ -499,20 +501,28 @@ def _find_backing_item_by_scope(self, scope_key: str) -> Optional[Any]:
499501 return item
500502 return None
501503
502- def _on_time_travel_complete (self , dirty_states , triggering_scope ):
504+ def on_time_travel_complete (self , dirty_states , triggering_scope ):
503505 """Refresh list after time travel. Subclasses can override for custom reloads."""
504506 self .update_item_list ()
505507 if hasattr (self , "update_button_states" ):
506508 self .update_button_states ()
507509
508- def _get_item_insert_index (self , item : Any , scope_key : str ) -> Optional [int ]:
510+ def _on_time_travel_complete (self , dirty_states , triggering_scope ):
511+ """Compatibility shim for callers still bound to the old private hook."""
512+ self .on_time_travel_complete (dirty_states , triggering_scope )
513+
514+ def get_item_insert_index (self , item : Any , scope_key : str ) -> Optional [int ]:
509515 """Get the index at which to insert item during time-travel re-registration.
510516
511517 Subclass can override to maintain correct ordering.
512518 Default: returns None (append to end).
513519 """
514520 return None
515521
522+ def _get_item_insert_index (self , item : Any , scope_key : str ) -> Optional [int ]:
523+ """Compatibility shim for subclasses still overriding the old private hook."""
524+ return self .get_item_insert_index (item , scope_key )
525+
516526 # ========== Action Dispatch (Concrete) ==========
517527
518528 def handle_button_action (self , action : str ) -> None :
@@ -565,8 +575,8 @@ def action_delete(self) -> None:
565575 self .service_adapter .show_error_dialog (f"No { self .ITEM_NAME_PLURAL } selected" )
566576 return
567577
568- if self ._validate_delete (items ):
569- self ._perform_delete (items )
578+ if self .validate_delete (items ):
579+ self .perform_delete (items )
570580 self .update_item_list ()
571581 self ._emit_items_changed ()
572582 self .status_message .emit (f"Deleted { len (items )} { self .ITEM_NAME_PLURAL } " )
@@ -582,7 +592,7 @@ def action_edit(self) -> None:
582592 self .service_adapter .show_error_dialog (f"No { self .ITEM_NAME_SINGULAR } selected" )
583593 return
584594
585- self ._show_item_editor (items [0 ])
595+ self .show_item_editor (items [0 ])
586596
587597 def action_code (self ) -> None :
588598 """
@@ -1016,8 +1026,8 @@ def _handle_edited_code(self, code: str) -> None:
10161026
10171027 Subclasses implement hooks:
10181028 - _pre_code_execution() - Pre-processing (optional, default no-op)
1019- - _handle_code_execution_error (code, error, namespace) - Migration fallback (optional)
1020- - _apply_executed_code (namespace) -> bool - Extract and apply variables (REQUIRED)
1029+ - handle_code_execution_error (code, error, namespace) - Migration fallback (optional)
1030+ - apply_executed_code (namespace) -> bool - Extract and apply variables (REQUIRED)
10211031 - _post_code_execution() - Post-processing (optional, default no-op)
10221032 """
10231033 code_type = self ._get_code_type ()
@@ -1038,14 +1048,14 @@ def _handle_edited_code(self, code: str) -> None:
10381048 exec (code , namespace )
10391049 except TypeError as e :
10401050 # Migration fallback hook (returns new namespace or None to re-raise)
1041- migrated_namespace = self ._handle_code_execution_error (code , e , namespace )
1051+ migrated_namespace = self .handle_code_execution_error (code , e , namespace )
10421052 if migrated_namespace is not None :
10431053 namespace = migrated_namespace
10441054 else :
10451055 raise
10461056
10471057 # Apply extracted variables to state (subclass hook)
1048- if not self ._apply_executed_code (namespace ):
1058+ if not self .apply_executed_code (namespace ):
10491059 raise ValueError (self ._get_code_missing_error_message ())
10501060
10511061 # Post-processing: broadcast, trigger refresh
@@ -1069,7 +1079,7 @@ def _pre_code_execution(self) -> None:
10691079 """
10701080 pass # Default: no-op
10711081
1072- def _handle_code_execution_error (self , code : str , error : Exception , namespace : dict ) -> Optional [dict ]:
1082+ def handle_code_execution_error (self , code : str , error : Exception , namespace : dict ) -> Optional [dict ]:
10731083 """
10741084 Handle code execution error, optionally returning migrated namespace.
10751085
@@ -1078,9 +1088,16 @@ def _handle_code_execution_error(self, code: str, error: Exception, namespace: d
10781088 PipelineEditor: Handle old-format step constructors (group_by/variable_components)
10791089 PlateManager: Return None (no migration support)
10801090 """
1091+ workflow = self .code_execution_workflow
1092+ if workflow is not None and hasattr (workflow , "migration_namespace" ):
1093+ return workflow .migration_namespace (code , error )
10811094 return None # Default: re-raise error
10821095
1083- def _apply_executed_code (self , namespace : dict ) -> bool :
1096+ def _handle_code_execution_error (self , code : str , error : Exception , namespace : dict ) -> Optional [dict ]:
1097+ """Compatibility shim for subclasses still overriding the old private hook."""
1098+ return self .handle_code_execution_error (code , error , namespace )
1099+
1100+ def apply_executed_code (self , namespace : dict ) -> bool :
10841101 """
10851102 Apply executed code namespace to widget state.
10861103
@@ -1090,9 +1107,16 @@ def _apply_executed_code(self, namespace: dict) -> bool:
10901107 PipelineEditor: Extract 'pipeline_steps', update self.pipeline_steps
10911108 PlateManager: Extract 'plate_paths', 'pipeline_data', etc.
10921109 """
1093- logger .warning (f"{ type (self ).__name__ } ._apply_executed_code not implemented" )
1110+ workflow = self .code_execution_workflow
1111+ if workflow is not None and hasattr (workflow , "apply_namespace" ):
1112+ return workflow .apply_namespace (namespace )
1113+ logger .warning (f"{ type (self ).__name__ } .apply_executed_code not implemented" )
10941114 return False # Default: fail (subclass must override)
10951115
1116+ def _apply_executed_code (self , namespace : dict ) -> bool :
1117+ """Compatibility shim for subclasses still overriding the old private hook."""
1118+ return self .apply_executed_code (namespace )
1119+
10961120 def _get_code_missing_error_message (self ) -> str :
10971121 """
10981122 Error message when expected code variables are missing.
@@ -1407,7 +1431,7 @@ def update_item_list(self) -> None:
14071431 return
14081432
14091433 # Pre-update hook (collect live context, normalize state)
1410- update_context = self ._pre_update_list ()
1434+ update_context = self .prepare_list_update ()
14111435
14121436 # Clear scope cache at start of update cycle - will be populated lazily
14131437 self ._item_scope_cache .clear ()
@@ -1527,22 +1551,38 @@ def _get_item_from_list_item(self, list_item: QListWidgetItem) -> Any:
15271551 # Data is the item itself
15281552 return data
15291553
1530- def _validate_delete (self , items : List [Any ]) -> bool :
1554+ def validate_delete (self , items : List [Any ]) -> bool :
15311555 """Check if delete is allowed. Default: True. Override for restrictions."""
1556+ workflow = self .deletion_workflow
1557+ if workflow is not None and hasattr (workflow , "validate" ):
1558+ return workflow .validate (items )
15321559 return True
15331560
1534- @abstractmethod
1535- def _perform_delete (self , items : List [Any ]) -> None :
1561+ def _validate_delete (self , items : List [Any ]) -> bool :
1562+ """Compatibility shim for subclasses still overriding the old private hook."""
1563+ return self .validate_delete (items )
1564+
1565+ def perform_delete (self , items : List [Any ]) -> None :
15361566 """
15371567 Remove items from internal list.
15381568
15391569 PlateManager: Remove from self.plates, cleanup orchestrators
15401570 PipelineEditor: Remove from self.pipeline_steps, update orchestrator
15411571 """
1542- ...
1572+ workflow = self .deletion_workflow
1573+ if workflow is not None and hasattr (workflow , "delete" ):
1574+ workflow .delete (items )
1575+ return
1576+ raise NotImplementedError (
1577+ f"{ type (self ).__name__ } .perform_delete requires a deletion workflow or override"
1578+ )
1579+
1580+ def _perform_delete (self , items : List [Any ]) -> None :
1581+ """Compatibility shim for callers still bound to the old private hook."""
1582+ self .perform_delete (items )
15431583
15441584 @abstractmethod
1545- def _show_item_editor (self , item : Any ) -> None :
1585+ def show_item_editor (self , item : Any ) -> None :
15461586 """
15471587 Show editor for item.
15481588
@@ -1551,6 +1591,10 @@ def _show_item_editor(self, item: Any) -> None:
15511591 """
15521592 ...
15531593
1594+ def _show_item_editor (self , item : Any ) -> None :
1595+ """Compatibility shim for callers still bound to the old private hook."""
1596+ self .show_item_editor (item )
1597+
15541598 # === UI Hooks (declarative via ITEM_HOOKS) ===
15551599
15561600 def _get_item_id (self , item : Any ) -> str :
@@ -1680,7 +1724,7 @@ def _get_list_placeholder(self) -> Optional[Tuple[str, Any]]:
16801724 """
16811725 return None # Default: no placeholder
16821726
1683- def _pre_update_list (self ) -> Any :
1727+ def prepare_list_update (self ) -> Any :
16841728 """
16851729 Pre-update hook: normalize state, collect context.
16861730
@@ -1691,6 +1735,10 @@ def _pre_update_list(self) -> Any:
16911735 """
16921736 return None # Default: no context
16931737
1738+ def _pre_update_list (self ) -> Any :
1739+ """Compatibility shim for subclasses still overriding the old private hook."""
1740+ return self .prepare_list_update ()
1741+
16941742 def _post_update_list (self ) -> None :
16951743 """
16961744 Post-update hook: auto-select first if needed.
@@ -2313,5 +2361,5 @@ def _handle_full_preview_refresh(self) -> None:
23132361
23142362 def closeEvent (self , event ):
23152363 """Ensure time travel callbacks are unregistered."""
2316- ObjectStateRegistry .remove_time_travel_complete_callback (self ._on_time_travel_complete )
2364+ ObjectStateRegistry .remove_time_travel_complete_callback (self .on_time_travel_complete )
23172365 super ().closeEvent (event )
0 commit comments