"""Configuration and data structures for scope-based coloring."""
from dataclasses import dataclass
from enum import Enum
from typing import Optional
[docs]
@dataclass
class ScopeVisualConfig:
"""Static configuration values for scope-based visuals."""
ORCHESTRATOR_ITEM_BG_SATURATION: int = 50
ORCHESTRATOR_ITEM_BG_VALUE: int = 85
ORCHESTRATOR_ITEM_BORDER_SATURATION: int = 60
ORCHESTRATOR_ITEM_BORDER_VALUE: int = 80
ORCHESTRATOR_ITEM_BG_OPACITY: float = 0.10 # 0.0-1.0
STEP_ITEM_BG_SATURATION: int = 50
STEP_ITEM_BG_VALUE: int = 85
STEP_ITEM_BG_OPACITY: float = 0.100 # 0.0-1.0
# Groupbox and tree background tint (subtle scope coloring)
GROUPBOX_BG_OPACITY: float = 0.05 # 0.0-1.0
TREE_BG_OPACITY: float = 0.05 # 0.0-1.0
STEP_WINDOW_BORDER_SATURATION: int = 60
STEP_WINDOW_BORDER_VALUE: int = 80
STEP_WINDOW_BORDER_WIDTH_PX: int = 4
STEP_WINDOW_BORDER_STYLE: str = "solid"
FLASH_DURATION_MS: int = 300
FLASH_COLOR_RGB: tuple[int, int, int] = (144, 238, 144)
LIST_ITEM_FLASH_ENABLED: bool = True
WIDGET_FLASH_ENABLED: bool = True
LIST_ITEM_LEADING_MARKER_COLOR_RGB: tuple[int, int, int] = (0, 230, 118)
LIST_ITEM_LEADING_MARKER_GUTTER_WIDTH_PX: int = 18
LIST_ITEM_LEADING_MARKER_STRIPE_LEFT_PX: int = 2
LIST_ITEM_LEADING_MARKER_STRIPE_TOP_MARGIN_PX: int = 3
LIST_ITEM_LEADING_MARKER_STRIPE_WIDTH_PX: int = 4
LIST_ITEM_LEADING_MARKER_STRIPE_RADIUS_PX: int = 2
LIST_ITEM_LEADING_MARKER_TRIANGLE_LEFT_PX: int = 9
LIST_ITEM_LEADING_MARKER_TRIANGLE_WIDTH_PX: int = 8
LIST_ITEM_LEADING_MARKER_TRIANGLE_HALF_HEIGHT_PX: int = 6
[docs]
@dataclass
class ScopeColorScheme:
"""Color scheme for a specific scope."""
scope_id: Optional[str]
hue: int
orchestrator_item_bg_rgb: tuple[int, int, int]
orchestrator_item_border_rgb: tuple[int, int, int]
step_window_border_rgb: tuple[int, int, int]
step_item_bg_rgb: Optional[tuple[int, int, int]]
step_border_width: int = 0
step_border_layers: list = None
base_color_rgb: tuple[int, int, int] = (128, 128, 128)
fixed_accent_rgb: Optional[tuple[int, int, int]] = None
def __post_init__(self) -> None:
if self.step_border_layers is None:
self.step_border_layers = []
[docs]
def to_qcolor_orchestrator_bg(self):
"""QColor for orchestrator item background with alpha."""
from PyQt6.QtGui import QColor
config = get_scope_visual_config()
r, g, b = self.orchestrator_item_bg_rgb
return QColor(r, g, b, int(255 * config.ORCHESTRATOR_ITEM_BG_OPACITY))
[docs]
def to_qcolor_orchestrator_border(self):
"""QColor for orchestrator item border."""
from PyQt6.QtGui import QColor
return QColor(*self.orchestrator_item_border_rgb)
[docs]
def to_qcolor_step_window_border(self):
"""QColor for step window border."""
from PyQt6.QtGui import QColor
return QColor(*self.step_window_border_rgb)
[docs]
def to_qcolor_step_item_bg(self):
"""QColor for step list background (alpha) or None."""
if self.step_item_bg_rgb is None:
return None
from PyQt6.QtGui import QColor
config = get_scope_visual_config()
r, g, b = self.step_item_bg_rgb
return QColor(r, g, b, int(255 * config.STEP_ITEM_BG_OPACITY))
[docs]
def border_layer_qcolor(self, layer):
"""Return the rendered border/accent color for one border layer."""
from PyQt6.QtGui import QColor
if self.fixed_accent_rgb is not None:
return QColor(*self.fixed_accent_rgb)
from pyqt_reactive.widgets.shared.scope_color_utils import tint_color_perceptual
_, tint_index, _ = (layer + ("solid",))[:3]
return tint_color_perceptual(self.base_color_rgb, tint_index).darker(120)
[docs]
def accent_qcolor(self):
"""Return the canonical accent/flash color for this scope."""
from PyQt6.QtGui import QColor
if self.fixed_accent_rgb is not None:
return QColor(*self.fixed_accent_rgb)
if self.step_border_layers:
return self.border_layer_qcolor(self.step_border_layers[0])
return self.to_qcolor_orchestrator_border()
[docs]
def background_tint_qcolor(self, layers, opacity: float):
"""Return the canonical subtle background tint for this scope."""
from PyQt6.QtGui import QColor
if self.fixed_accent_rgb is not None:
r, g, b = self.fixed_accent_rgb
return QColor(r, g, b, int(255 * opacity))
from pyqt_reactive.widgets.shared.scope_color_utils import tint_color_perceptual
if layers:
_, tint_index, _ = (layers[0] + ("solid",))[:3]
else:
tint_index = 1
color = tint_color_perceptual(self.base_color_rgb, tint_index)
color.setAlphaF(opacity)
return color
[docs]
def to_stylesheet_step_window_border(self) -> str:
"""Reserve border space via stylesheet for step windows."""
if not self.step_border_layers:
r, g, b = self.step_window_border_rgb
return f"border: 4px solid rgb({r}, {g}, {b});"
total_width = sum(layer[0] for layer in self.step_border_layers)
return f"border: {total_width}px solid transparent;"
[docs]
class RootScopeColorScheme:
"""Visual identity for the ObjectState root/global scope."""
scope_id = ""
accent_rgb = (255, 255, 255)
[docs]
@classmethod
def matches(cls, scope_id: Optional[str]) -> bool:
return scope_id == cls.scope_id
[docs]
@classmethod
def create(cls) -> ScopeColorScheme:
return ScopeColorScheme(
scope_id=cls.scope_id,
hue=0,
orchestrator_item_bg_rgb=cls.accent_rgb,
orchestrator_item_border_rgb=cls.accent_rgb,
step_window_border_rgb=cls.accent_rgb,
step_item_bg_rgb=cls.accent_rgb,
step_border_width=3,
step_border_layers=[(3, 1, "solid")],
base_color_rgb=cls.accent_rgb,
fixed_accent_rgb=cls.accent_rgb,
)
[docs]
class ListItemType(Enum):
"""Type of list item for scope-based coloring."""
ORCHESTRATOR = "to_qcolor_orchestrator_bg"
STEP = "to_qcolor_step_item_bg"
[docs]
def get_background_color(self, color_scheme: ScopeColorScheme):
method = getattr(color_scheme, self.value)
return method()
_config_instance: Optional[ScopeVisualConfig] = None
[docs]
def get_scope_visual_config() -> ScopeVisualConfig:
"""Singleton access for ScopeVisualConfig."""
global _config_instance
if _config_instance is None:
_config_instance = ScopeVisualConfig()
return _config_instance