Skip to content

Commit 32ef88c

Browse files
committed
feat: add ServiceRegistry, ScopeWindowFactory, ButtonPanel, and System Monitor
Major architectural refactoring to add reusable services and components. NEW SERVICES: - ServiceRegistry: Centralized widget/service registration and lookup - AutoRegisterServiceMixin for automatic registration - Type-based service resolution - Breaks circular dependencies (no more floating_windows) - ScopeWindowFactory: Handler-based window creation system - Regex pattern matching for scope_ids - Handler registration with callback dispatch - Time-travel integration with object_state parameter - PersistentSystemMonitor: Background thread-based system monitoring - Non-blocking metric collection (CPU, RAM, GPU, VRAM) - Thread-safe history management - PyQt6 signal integration NEW COMPONENTS: - ButtonPanel: Reusable button panel with BUTTON_CONFIGS pattern - Extracted from AbstractManagerWidget - Declarative configuration: [(label, action_id, tooltip), ...] - Flexible layout: grid_columns parameter - SystemMonitorWidget: PyQt6 system monitor (migrated from Textual TUI) - Real-time graphs with PyQtGraph - Lazy pyqtgraph import (8+ second startup delay avoided) - Declarative button configuration LOG VIEWER SYSTEM: - LogStreamer: Stream log lines as JSONL chunks - LogHighlighter: Subprocess-based log line highlighting - Timestamps (gray), Log levels (color+bold), Logger names (purple) - File paths (green), Python strings (brown), Numbers (light gray-green) - LogLoader: Efficient log file loading - LogHighlightClient: Coordinates subprocess communication - LogViewerWidget: UI widget with syntax highlighting FLASH ANIMATION ENHANCEMENTS: - Widget-type-specific masking strategies - Checkbox: Square cutout (no rounding) for textless checkboxes - Label: Tight masking using sizeHint() - Help button: Fixed square masking when _square_size set - Function pane: Title row widgets masking - INVERSE mode: Masks title + leaf_widget + label_widget - get_child_mask_rect(): Single source of truth for child masking PARAMETER FORM LIFECYCLE: - Sequence number tracking (_pfm_seq) for form manager instances - Enhanced debug logging for async widget creation - Nested form manager tracking - Batch widget creation with progressive scope accent styling PERFORMANCE MONITORING: - Performance logger changed from DEBUG to WARNING - Timer context manager for performance tracking - Performance metrics for common operations documented SERVICE INTEGRATION: - All widget lookups migrated from QApplication.topLevelWidgets() to ServiceRegistry.get() - Removed floating_windows dictionary from main window - AutoRegisterServiceMixin for automatic service registration - WindowFactory.create_window_for_scope() with time-travel support DOCUMENTATION: - service_registry.rst: ServiceRegistry pattern and usage - scope_window_factory.rst: Generic scope window factory - button_panel.rst: ButtonPanel component documentation - system_monitor.rst: PyQt6 system monitor - system_monitor_core.rst: Framework-agnostic monitoring core - persistent_system_monitor.rst: Background thread monitor - log_viewer_system.rst: Log viewer with syntax highlighting - Updated flash_animation_system.rst: Widget-type-specific masking - Updated gui_performance_patterns.rst: Performance monitoring - Updated parameter_form_lifecycle.rst: Sequence tracking - Updated index.rst: New docs added Related to OpenHCS ServiceRegistry integration refactoring.
1 parent b15292e commit 32ef88c

48 files changed

Lines changed: 5662 additions & 869 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/architecture/button_panel.rst

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
Button Panel Component
2+
=====================
3+
4+
**Reusable button panel with declarative configuration.**
5+
6+
**Module**: ``pyqt_reactive.widgets.shared.button_panel``
7+
8+
Overview
9+
--------
10+
11+
``ButtonPanel`` provides a reusable button panel component that can be used by any widget without requiring inheritance. It uses a declarative ``BUTTON_CONFIGS`` format for specifying buttons.
12+
13+
This component was extracted from ``AbstractManagerWidget`` to allow widgets to use the same button panel pattern without inheriting from the full manager class.
14+
15+
Architecture
16+
------------
17+
18+
``ButtonPanel`` uses a simple declarative configuration:
19+
20+
.. code-block:: python
21+
22+
BUTTON_CONFIGS = [
23+
("Refresh", "refresh", "Refresh the display"),
24+
("Toggle", "toggle_layout", "Toggle between layouts"),
25+
("Export", "export", "Export data"),
26+
]
27+
28+
Each button configuration is a tuple of:
29+
- **label**: Button text (e.g., "Refresh")
30+
- **action_id**: Identifier passed to callback (e.g., "refresh")
31+
- **tooltip**: Tooltip text (e.g., "Refresh the display")
32+
33+
Usage
34+
-----
35+
36+
Basic Usage
37+
~~~~~~~~~~
38+
39+
.. code-block:: python
40+
41+
from pyqt_reactive.widgets.shared.button_panel import ButtonPanel
42+
from pyqt_reactive.theming import StyleSheetGenerator
43+
44+
# Define button configurations
45+
BUTTON_CONFIGS = [
46+
("Refresh", "refresh", "Refresh the display"),
47+
("Toggle", "toggle_layout", "Toggle between layouts"),
48+
("Export", "export", "Export data"),
49+
]
50+
51+
# Create button panel
52+
panel = ButtonPanel(
53+
button_configs=BUTTON_CONFIGS,
54+
on_action=self.handle_button_action,
55+
style_generator=self.style_generator,
56+
)
57+
58+
# Add panel to layout
59+
layout.addWidget(panel)
60+
61+
Action Handler
62+
~~~~~~~~~~~~~~~
63+
64+
The ``on_action`` callback receives the ``action_id`` from the clicked button:
65+
66+
.. code-block:: python
67+
68+
def handle_button_action(self, action_id: str):
69+
"""Handle button actions."""
70+
if action_id == "refresh":
71+
self.refresh_display()
72+
elif action_id == "toggle_layout":
73+
self.toggle_layout()
74+
elif action_id == "export":
75+
self.export_data()
76+
77+
Grid Layout
78+
~~~~~~~~~~~~
79+
80+
By default, buttons are laid out in a single horizontal row. You can specify a grid layout:
81+
82+
.. code-block:: python
83+
84+
panel = ButtonPanel(
85+
button_configs=self.BUTTON_CONFIGS,
86+
on_action=self.handle_button_action,
87+
grid_columns=2, # 2 columns
88+
)
89+
90+
This creates a grid with the specified number of columns:
91+
92+
.. list-table::
93+
:header-rows: 1
94+
95+
* - grid_columns
96+
- Layout
97+
* - 0
98+
- Single horizontal row
99+
* - 1
100+
- Single vertical column
101+
* - 2
102+
- 2x2 grid
103+
* - 3
104+
- 3x2 grid
105+
* - 4
106+
- 4x2 grid
107+
108+
Integration with SystemMonitor
109+
---------------------------
110+
111+
``SystemMonitor`` uses ``ButtonPanel`` for its action buttons:
112+
113+
.. code-block:: python
114+
115+
class SystemMonitorWidget(QWidget):
116+
"""System Monitor Widget."""
117+
118+
# Declarative button configuration
119+
BUTTON_CONFIGS = [
120+
("Global Config", "global_config", "Open global configuration editor"),
121+
("Log Viewer", "log_viewer", "Open log viewer window"),
122+
("Custom Functions", "custom_functions", "Manage custom functions"),
123+
("Test Plate", "test_plate", "Generate synthetic test plate"),
124+
]
125+
126+
BUTTON_GRID_COLUMNS = 0 # Single row
127+
128+
def __init__(self, color_scheme=None, config=None, parent=None):
129+
super().__init__(parent)
130+
131+
# Create button panel
132+
self.button_panel = ButtonPanel(
133+
button_configs=self.BUTTON_CONFIGS,
134+
style_generator=self.style_generator,
135+
grid_columns=self.BUTTON_GRID_COLUMNS,
136+
)
137+
138+
# Connect actions
139+
self.button_panel.action_triggered.connect(self.handle_button_action)
140+
141+
def handle_button_action(self, action_id: str):
142+
"""Handle button panel actions."""
143+
if action_id == "global_config":
144+
self.show_global_config.emit()
145+
elif action_id == "log_viewer":
146+
self.show_log_viewer.emit()
147+
# ... etc
148+
149+
Styling
150+
-------
151+
152+
``ButtonPanel`` integrates with ``StyleSheetGenerator`` for consistent styling:
153+
154+
.. code-block:: python
155+
156+
from pyqt_reactive.theming import StyleSheetGenerator, ColorScheme
157+
158+
color_scheme = ColorScheme()
159+
style_generator = StyleSheetGenerator(color_scheme)
160+
161+
panel = ButtonPanel(
162+
button_configs=self.BUTTON_CONFIGS,
163+
on_action=self.handle_button_action,
164+
style_generator=style_generator, # Apply styles
165+
)
166+
167+
Signals
168+
-------
169+
170+
**action_triggered** (pyqtSignal)
171+
172+
Emitted when a button is clicked. Provides the ``action_id``:
173+
174+
.. code-block:: python
175+
176+
panel.action_triggered.connect(lambda action_id: print(f"Action: {action_id}"))
177+
178+
Migration from AbstractManagerWidget
179+
----------------------------------
180+
181+
Before (AbstractManagerWidget):
182+
183+
.. code-block:: python
184+
185+
class MyWidget(AbstractManagerWidget):
186+
"""Widget with button panel."""
187+
188+
BUTTON_CONFIGS = [
189+
("Refresh", "refresh", "Refresh the display"),
190+
]
191+
192+
def __init__(self):
193+
super().__init__()
194+
# Button panel created automatically by AbstractManagerWidget
195+
196+
After (ButtonPanel):
197+
198+
.. code-block:: python
199+
200+
class MyWidget(QWidget):
201+
"""Widget with button panel."""
202+
203+
def __init__(self):
204+
super().__init__()
205+
206+
# Create button panel manually
207+
self.button_panel = ButtonPanel(
208+
button_configs=[
209+
("Refresh", "refresh", "Refresh the display"),
210+
],
211+
on_action=self.handle_button_action,
212+
)
213+
214+
def handle_button_action(self, action_id: str):
215+
if action_id == "refresh":
216+
self.refresh()
217+
218+
Benefits
219+
---------
220+
221+
- **No inheritance required**: Use with any widget class
222+
- **Declarative configuration**: Define buttons in a list
223+
- **Flexible layout**: Single row or grid layout
224+
- **Consistent styling**: Integrates with StyleSheetGenerator
225+
- **Action-based**: Simple callback interface with action IDs
226+
227+
See Also
228+
--------
229+
230+
- :doc:`abstract_manager_widget` - Abstract manager widget (original button panel location)
231+
- :doc:`responsive_layout_widgets` - Responsive layout components
232+
- :doc:`system_monitor` - System monitor usage example

docs/architecture/flash_animation_system.rst

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,53 @@ Flash animations have three phases with configurable durations:
7373
2. **hold** (50ms): Hold at maximum intensity
7474
3. **fade_out** (350ms): Slow fade-out with InOutCubic easing
7575

76+
Widget-Type-Specific Masking
77+
--------------------------------
78+
79+
Flash animations use widget-type-specific masking strategies for precise visual feedback:
80+
81+
**Masking Strategies**:
82+
83+
- **Checkbox**: Tight mask for indicator + label text using Qt style subelement rects
84+
- **Label**: Tight mask using ``sizeHint()`` to avoid empty layout space
85+
- **Help Button**: Fixed square mask when ``_square_size`` is set
86+
- **All other widgets**: Full rectangle mask
87+
88+
**Checkbox Square Cutout**:
89+
90+
Textless checkboxes (no label) use square cutouts to avoid rounding:
91+
92+
.. code-block:: python
93+
94+
def _needs_square_checkbox_mask(widget: QWidget) -> bool:
95+
return isinstance(widget, QCheckBox) and not widget.text()
96+
97+
**Function Pane Title Masking**:
98+
99+
Function panes mask title row widgets tightly:
100+
101+
.. code-block:: python
102+
103+
def _get_function_pane_title_widgets(groupbox: QWidget) -> List[QWidget]:
104+
pane = groupbox
105+
while pane is not None:
106+
if hasattr(pane, "_flash_title_container") or hasattr(pane, "_module_path_label"):
107+
break
108+
pane = pane.parentWidget()
109+
110+
widgets = []
111+
module_label = getattr(pane, "_module_path_label", None)
112+
if module_label and module_label.isVisible():
113+
widgets.append(module_label)
114+
115+
title_container = getattr(pane, "_flash_title_container", None)
116+
if title_container and title_container.isVisible():
117+
for child in title_container.findChildren(QWidget):
118+
if child.isVisible() and isinstance(child, LEAF_WIDGET_TYPES):
119+
widgets.append(child)
120+
121+
return widgets
122+
76123
FlashElement Types
77124
------------------
78125

@@ -86,14 +133,38 @@ The system supports multiple element types via ``FlashElement`` dataclass:
86133
- Use Case
87134
* - Groupbox
88135
- ``create_groupbox_element()``
89-
- Form section headers
136+
- Form section headers (STANDARD mode masks all children, INVERSE mode masks title + leaf_widget)
137+
* - Groupbox (full rect)
138+
- ``create_groupbox_element(..., use_full_rect=True)``
139+
- Flash entire groupbox geometry (no margin-top offset)
90140
* - Tree Item
91141
- ``create_tree_item_element()``
92142
- Config hierarchy trees
93143
* - List Item
94144
- ``create_list_item_element()``
95145
- Step/function lists
96146

147+
INVERSE Mode with Label Widget Masking
148+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
149+
150+
INVERSE mode now masks title + leaf_widget + label_widget (not all title row widgets):
151+
152+
.. code-block:: python
153+
154+
self.register_flash_leaf(
155+
key="my_field",
156+
groupbox=my_groupbox,
157+
leaf_widget=my_widget,
158+
label_widget=my_label # NEW: mask label too
159+
)
160+
161+
This highlights "all fields that inherited the change" while keeping the changed field and its label visible.
162+
163+
**Masking Behavior**:
164+
165+
- **STANDARD mode** (``leaf_widget=None``): Mask ALL children, flash only frame/background
166+
- **INVERSE mode** (``leaf_widget=widget``): Mask title + leaf_widget + label_widget, flash frame + all siblings
167+
97168
Usage with FlashMixin
98169
---------------------
99170

docs/architecture/gui_performance_patterns.rst

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,3 +1000,55 @@ When editing ``WellFilterConfig.well_filter`` in ``PipelineConfig``:
10001000
- Before: 3-5 sibling refreshes per keystroke (all siblings)
10011001
- After: 0-2 sibling refreshes per keystroke (only affected siblings)
10021002
- Measured improvement: ~5-10ms per keystroke in complex configs
1003+
1004+
Performance Monitoring
1005+
---------------------
1006+
1007+
The system includes a performance monitor for tracking widget creation and form operations.
1008+
1009+
**Performance Logger**:
1010+
1011+
.. code-block:: python
1012+
1013+
from pyqt_reactive.core.performance_monitor import perf_logger
1014+
1015+
perf_logger.debug(f"Operation took {duration_ms:.2f}ms")
1016+
1017+
**Logging Level**:
1018+
1019+
Performance logger uses ``WARNING`` level by default to reduce log noise:
1020+
1021+
.. code-block:: python
1022+
1023+
perf_logger.setLevel(logging.WARNING)
1024+
1025+
This suppresses routine performance measurements in normal operation while still logging performance issues when they occur.
1026+
1027+
**Timer Context Manager**:
1028+
1029+
.. code-block:: python
1030+
1031+
from pyqt_reactive.core.performance_monitor import timer
1032+
1033+
with timer("Widget creation", threshold_ms=5.0):
1034+
widget = create_complex_widget()
1035+
1036+
Operations slower than ``threshold_ms`` are logged to ``perf_logger``.
1037+
1038+
**Usage Guidelines**:
1039+
1040+
- Use ``timer`` for operations that may be slow (>5ms)
1041+
- Set appropriate ``threshold_ms`` for each context
1042+
- Only log operations that are likely to be performance bottlenecks
1043+
- Avoid excessive logging in hot paths (like paint events)
1044+
1045+
**Performance Metrics**:
1046+
1047+
Common operation timings:
1048+
1049+
- Widget creation: 1-50ms (depends on complexity)
1050+
- Form initialization: 10-200ms (depends on parameter count)
1051+
- Placeholder refresh: 1-10ms (per field)
1052+
- Cross-window update: 5-20ms (per affected window)
1053+
1054+
Operations exceeding these thresholds are flagged for investigation.

0 commit comments

Comments
 (0)