List Item Preview System
The list item preview system provides declarative, per-field styling for list items in manager widgets (PlateManager, PipelineEditor). It displays configuration previews with visual indicators for dirty state and signature differences.
Overview
List items in pyqt-reactive show rich previews of configuration state:
▶ MyPlate (W:8 | Seq:C,Z) ← Inline format
/path/to/plate
└─ wf:[3] | mat:ZARR | configs=[NAP, FIJI]
Visual semantics:
Underline = field differs from signature default (explicitly set)
Asterisk (*) = resolved value differs from saved (dirty/unsaved)
Name inherits styling if ANY field is dirty/sig-diff
Declarative Configuration
Each widget declares its format using ListItemFormat:
from pyqt_reactive.widgets.shared.abstract_manager_widget import ListItemFormat
class PlateManagerWidget(AbstractManagerWidget):
LIST_ITEM_FORMAT = ListItemFormat(
first_line=(), # Fields after name on line 1
preview_line=( # Fields on └─ preview line
'num_workers',
'vfs_config.materialization_backend',
'path_planning_config.well_filter',
),
detail_line_field='path', # Field for detail line
show_config_indicators=True, # Show NAP/FIJI/MAT configs
formatters={ # Custom formatters (optional)
'my_field': lambda v: f"custom:{v}" if v else None,
},
)
Field abbreviations are declared on config classes via @global_pipeline_config:
@global_pipeline_config(field_abbreviations={'well_filter': 'wf', 'output_dir_suffix': 'out'})
@dataclass(frozen=True)
class PathPlanningConfig:
well_filter: Optional[str] = None
output_dir_suffix: str = "_pyqt_reactive"
ListItemFormat Fields
Field |
Default |
Description |
|---|---|---|
|
|
Tuple of field paths shown after name on line 1 |
|
|
Tuple of field paths shown on └─ preview line |
|
|
Single field path for detail line (e.g., |
|
|
Whether to show config indicators (NAP, FIJI, MAT) |
|
|
Dict mapping field path to formatter function |
Visual Layout
Multiline Format
▶ ItemName (first_line_segments) ← Line 1: name + first_line fields
detail_line ← Line 2: e.g., path
└─ preview_segments | configs=[NAP, FIJI] ← Line 3: preview + configs
Inline Format
▶ ItemName (seg1 | seg2 | seg3) ← Single line with all segments
The format is determined by the StyledTextLayout.multiline flag, set automatically based on whether detail_line or preview_line are populated.
Field Types
Permanent vs Dynamic Fields
Type |
Source |
Behavior |
|---|---|---|
Declared |
|
Always shown (permanent) |
Config indicators |
|
Shown if config’s |
Sig-diff fields |
Auto-detected |
Dynamically added when value differs from signature default |
The auto-include logic (in _build_multiline_styled_text) automatically adds any field with a non-default value to the preview line, even if not explicitly declared.
Value Formatting
All field values are formatted by format_preview_value() in config_preview_formatters.py:
def format_preview_value(value: Any) -> Optional[str]:
if value is None:
return None
if isinstance(value, Enum):
return value.name if value.value else None
if isinstance(value, list):
if value and isinstance(value[0], Enum):
return ','.join(v.value for v in value)
return f'[{len(value)}]'
if callable(value) and not isinstance(value, type):
return getattr(value, '__name__', str(value))
return str(value)
Field abbreviations are declared on config classes via @global_pipeline_config(field_abbreviations=...):
@global_pipeline_config(field_abbreviations={'well_filter': 'wf', 'output_dir_suffix': 'out'})
@dataclass(frozen=True)
class PathPlanningConfig:
well_filter: Optional[str] = None
output_dir_suffix: str = "_pyqt_reactive"
Adding a New Preview Field
Add to ListItemFormat (in widget):
LIST_ITEM_FORMAT = ListItemFormat(
preview_line=(
'existing_field',
'my_new_config.some_field', # Add field path
),
formatters={
'some_field': lambda v: f"custom:{v}" if v else None, # Optional formatter
},
)
Add abbreviation (on config class):
@global_pipeline_config(field_abbreviations={'some_field': 'sf'})
@dataclass(frozen=True)
class MyNewConfig:
some_field: str = "default"
Adding a Config Indicator
Config indicators (NAP, FIJI, MAT) and field abbreviations are both declared via @global_pipeline_config:
from objectstate import global_pipeline_config
@global_pipeline_config(
preview_label='NEW',
field_abbreviations={'some_setting': 'set'}
)
@dataclass
class MyNewConfig:
enabled: bool = False
some_setting: int = 10
The config indicator appears in configs=[...] when enabled=True.
Structured Rendering Architecture
The system uses structured StyledTextLayout objects instead of string parsing:
@dataclass(frozen=True)
class Segment:
"""A styled text segment with field path for dirty/sig-diff matching."""
text: str
field_path: Optional[str] = None # None=no styling, ''=root path
@dataclass
class StyledTextLayout:
"""Structured layout for styled text rendering."""
name: Segment
status_prefix: str = ""
first_line_segments: List[Segment] = field(default_factory=list)
detail_line: str = ""
preview_segments: List[Segment] = field(default_factory=list)
config_segments: List[Segment] = field(default_factory=list)
multiline: bool = False
The delegate iterates segments directly and applies styling per-segment without any string matching:
Build
StyledTextLayoutwith all segmentsStore layout in item data (
LAYOUT_ROLE)Delegate reads layout and renders segments with appropriate styling
No string parsing required
See Also
AbstractManagerWidget Architecture - AbstractManagerWidget base class
GUI Performance Patterns - Cross-window preview system
configuration_framework - Config framework and lazy configs
Implementation References:
pyqt_reactive/widgets/shared/list_item_delegate.py- StyledTextLayout and renderingpyqt_reactive/widgets/shared/abstract_manager_widget.py- ListItemFormat and build methodspyqt_reactive/utils/preview_formatters.py- Centralized formatters