Source code for pyqt_reactive.theming.style_generator

"""
QStyleSheet Generator for OpenHCS PyQt6 GUI

Generates dynamic QStyleSheet strings from ColorScheme objects, enabling
centralized styling with theme support. Replaces hardcoded color strings with
semantic color scheme references.
"""

import logging
from enum import Enum

from PyQt6.QtGui import QColor

from pyqt_reactive.forms import layout_constants

from .color_scheme import ColorScheme

logger = logging.getLogger(__name__)


[docs] class StatusColorRole(Enum): """Closed status color roles supported by the theme.""" SUCCESS = "success" WARNING = "warning" ERROR = "error" INFO = "info"
[docs] @classmethod def from_name(cls, status_type: str) -> "StatusColorRole": try: return cls(status_type) except ValueError as exc: available = ", ".join(role.value for role in cls) raise KeyError( f"Unknown status color role {status_type!r}; " f"available roles: {available}." ) from exc
STATUS_COLOR_SELECTOR_BY_ROLE = { StatusColorRole.SUCCESS: lambda color_scheme: color_scheme.status_success, StatusColorRole.WARNING: lambda color_scheme: color_scheme.status_warning, StatusColorRole.ERROR: lambda color_scheme: color_scheme.status_error, StatusColorRole.INFO: lambda color_scheme: color_scheme.status_info, }
[docs] class StyleSheetGenerator: """ Generates QStyleSheet strings from ColorScheme objects. Provides methods to generate complete stylesheets for different widget types, replacing hardcoded colors with centralized color scheme references. """
[docs] def __init__(self, color_scheme: ColorScheme): """ Initialize the style generator with a color scheme. Args: color_scheme: ColorScheme instance to use for styling """ self.color_scheme = color_scheme
[docs] def update_color_scheme(self, color_scheme: ColorScheme): """ Update the color scheme used for style generation. Args: color_scheme: New ColorScheme instance """ self.color_scheme = color_scheme
[docs] def generate_dialog_style(self) -> str: """ Generate QStyleSheet for dialog windows. Returns: str: Complete QStyleSheet for dialog styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QDialog {{ background-color: {cs.to_hex(cs.window_bg)}; color: {cs.to_hex(cs.text_primary)}; }} QGroupBox {{ font-weight: bold; border: none; border-radius: {layout.dialog_groupbox_corner_radius}px; margin-top: 5px; padding-top: 5px; background-color: {cs.to_hex(cs.panel_bg)}; color: {cs.to_hex(cs.text_primary)}; }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 5px 0 5px; color: {cs.to_hex(cs.text_accent)}; }} QLabel {{ color: {cs.to_hex(cs.text_secondary)}; }} QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {{ background-color: {cs.to_hex(cs.input_bg)}; color: {cs.to_hex(cs.input_text)}; border: 1px solid {cs.to_hex(cs.input_border)}; border-radius: {layout.widget_corner_radius}px; padding: 5px; }} QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {{ border: 1px solid {cs.to_hex(cs.input_focus_border)}; }} QCheckBox {{ color: {cs.to_hex(cs.text_primary)}; }} """
[docs] def generate_tree_widget_style(self) -> str: """ Generate QStyleSheet for tree widgets and list widgets. Returns: str: Complete QStyleSheet for tree/list widget styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QTreeWidget, QListWidget {{ background-color: {cs.to_hex(cs.panel_bg)}; color: {cs.to_hex(cs.text_primary)}; border: none; border-radius: {layout.widget_corner_radius}px; selection-background-color: {cs.to_hex(cs.selection_bg)}; }} QTreeWidget::item, QListWidget::item {{ padding: 2px 4px; border-bottom: none; background-color: transparent; }} QTreeWidget::item:hover {{ background-color: {cs.to_hex(cs.hover_bg)}; }} QTreeWidget::item:selected {{ background-color: {cs.to_hex(cs.selection_bg)}; color: {cs.to_hex(cs.selection_text)}; }} QListWidget::item:hover, QListWidget::item:selected {{ background-color: transparent; }} QHeaderView::section {{ background-color: {cs.to_hex(cs.panel_bg)}; color: {cs.to_hex(cs.text_primary)}; padding: 4px; border: none; border-bottom: 1px solid {cs.to_hex(cs.border_color)}; font-weight: normal; }} """
[docs] def generate_list_widget_style(self) -> str: """Alias for generate_tree_widget_style (includes QListWidget styling).""" return self.generate_tree_widget_style()
[docs] def generate_table_widget_style(self) -> str: """ Generate QStyleSheet for table widgets. Returns: str: Complete QStyleSheet for table widget styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QTableWidget {{ background-color: {cs.to_hex(cs.panel_bg)}; color: {cs.to_hex(cs.text_primary)}; border: none; border-radius: {layout.widget_corner_radius}px; gridline-color: {cs.to_hex(cs.border_color)}; selection-background-color: {cs.to_hex(cs.selection_bg)}; }} QTableWidget::item {{ padding: 4px; border: none; qproperty-wordWrap: false; }} QTableWidget::item:hover {{ background-color: {cs.to_hex(cs.hover_bg)}; }} QTableWidget::item:selected {{ background-color: {cs.to_hex(cs.selection_bg)}; color: {cs.to_hex(cs.selection_text)}; }} QHeaderView::section {{ background-color: {cs.to_hex(cs.input_bg)}; color: {cs.to_hex(cs.text_primary)}; padding: 5px; border: none; border-bottom: 1px solid {cs.to_hex(cs.border_color)}; font-weight: bold; }} """
[docs] def generate_button_style(self) -> str: """ Generate QStyleSheet for buttons with all states. Returns: str: Complete QStyleSheet for button styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: {layout.widget_padding}px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} QPushButton:pressed {{ background-color: {cs.to_hex(cs.button_pressed_bg)}; }} QPushButton:disabled {{ background-color: {cs.to_hex(cs.button_disabled_bg)}; color: {cs.to_hex(cs.button_disabled_text)}; }} """
[docs] def generate_combobox_style(self) -> str: """ Generate QStyleSheet for combo boxes with dropdown styling. Returns: str: Complete QStyleSheet for combo box styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QComboBox {{ background-color: {cs.to_hex(cs.input_bg)}; color: {cs.to_hex(cs.input_text)}; border: 1px solid {cs.to_hex(cs.input_border)}; border-radius: {layout.widget_corner_radius}px; padding: {layout.widget_padding}px; }} QComboBox::drop-down {{ border: none; background-color: {cs.to_hex(cs.button_normal_bg)}; }} QComboBox::down-arrow {{ border: none; }} QComboBox QAbstractItemView {{ background-color: {cs.to_hex(cs.input_bg)}; color: {cs.to_hex(cs.input_text)}; selection-background-color: {cs.to_hex(cs.selection_bg)}; }} """
[docs] def generate_progress_bar_style(self) -> str: """ Generate QStyleSheet for progress bars. Returns: str: Complete QStyleSheet for progress bar styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QProgressBar {{ border: none; border-radius: {layout.widget_corner_radius}px; background-color: {cs.to_hex(cs.progress_bg)}; color: {cs.to_hex(cs.text_primary)}; text-align: center; }} QProgressBar::chunk {{ background-color: {cs.to_hex(cs.progress_fill)}; border-radius: {layout.progress_chunk_corner_radius}px; }} """
[docs] def generate_frame_style(self) -> str: """ Generate QStyleSheet for frames and panels. Returns: str: Complete QStyleSheet for frame styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QFrame {{ background-color: {cs.to_hex(cs.frame_bg)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: {layout.widget_padding}px; }} """
[docs] def generate_tab_widget_style(self) -> str: """ Generate QStyleSheet for tab widgets. Returns: str: Complete QStyleSheet for tab widget styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT # Use grey shades for tabs instead of blue selection color # Lighter grey for inactive tabs, darker grey for selected tab inactive_tab_bg = cs.button_normal_bg # Lighter grey selected_tab_bg = cs.panel_bg # Darker grey (same as panel background) return f""" QTabWidget::pane {{ border: 1px solid {cs.to_hex(cs.border_color)}; border-radius: {layout.widget_corner_radius}px; background-color: {cs.to_hex(cs.panel_bg)}; }} QTabBar::tab {{ background-color: {cs.to_hex(inactive_tab_bg)}; color: {cs.to_hex(cs.text_secondary)}; border: none; border-top-left-radius: {layout.widget_corner_radius}px; border-top-right-radius: {layout.widget_corner_radius}px; padding: 8px 16px; margin-right: 2px; }} QTabBar::tab:selected {{ background-color: {cs.to_hex(selected_tab_bg)}; color: {cs.to_hex(cs.text_primary)}; font-weight: bold; border-bottom: 2px solid {cs.to_hex(cs.text_accent)}; }} QTabBar::tab:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} """
[docs] def generate_system_monitor_style(self) -> str: """ Generate QStyleSheet for system monitor widget. Returns: str: Complete QStyleSheet for system monitor styling """ cs = self.color_scheme return f""" SystemMonitorWidget {{ background-color: {cs.to_hex(cs.window_bg)}; color: {cs.to_hex(cs.text_primary)}; }} QLabel {{ color: {cs.to_hex(cs.text_primary)}; }} #header_label {{ color: {cs.to_hex(cs.text_accent)}; font-weight: bold; font-size: 14px; }} #info_panel {{ background-color: {cs.to_hex(cs.panel_bg)}; }} #info_title {{ color: {cs.to_hex(cs.text_accent)}; font-weight: bold; font-size: 11px; }} #info_label_key {{ color: {cs.to_hex(cs.text_secondary)}; font-size: 10px; }} #info_label_value {{ color: {cs.to_hex(cs.text_primary)}; font-weight: bold; font-size: 11px; }} """
[docs] def generate_complete_application_style(self) -> str: """ Generate complete application-wide QStyleSheet. Returns: str: Complete QStyleSheet for entire application """ return ( self.generate_dialog_style() + "\n" + self.generate_tree_widget_style() + "\n" + self.generate_button_style() + "\n" + self.generate_combobox_style() + "\n" + self.generate_progress_bar_style() + "\n" + self.generate_frame_style() + "\n" + self.generate_system_monitor_style() )
[docs] def generate_config_window_style(self) -> str: """ Generate QStyleSheet for configuration windows with button panel. Returns: str: Complete QStyleSheet for config window styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT widget_padding = layout.widget_padding groupbox_margin_top = layout.groupbox_margin_top groupbox_padding_top = layout.groupbox_padding_top return f""" QDialog {{ background-color: {cs.to_hex(cs.window_bg)}; color: {cs.to_hex(cs.text_primary)}; }} QGroupBox {{ font-weight: bold; border: none; border-radius: {layout.groupbox_corner_radius}px; margin-top: {groupbox_margin_top}px; padding-top: {groupbox_padding_top}px; background-color: {cs.to_hex(cs.panel_bg)}; color: {cs.to_hex(cs.text_primary)}; }} QGroupBox::title {{ subcontrol-origin: margin; left: 10px; padding: 0 3px 0 3px; color: {cs.to_hex(cs.text_accent)}; }} QLabel {{ color: {cs.to_hex(cs.text_secondary)}; }} QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox {{ background-color: {cs.to_hex(cs.input_bg)}; color: {cs.to_hex(cs.input_text)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: {widget_padding}px; }} QLineEdit:focus, QSpinBox:focus, QDoubleSpinBox:focus, QComboBox:focus {{ border: 1px solid {cs.to_hex(cs.input_focus_border)}; }} QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: {widget_padding}px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} QPushButton:pressed {{ background-color: {cs.to_hex(cs.button_pressed_bg)}; }} QPushButton:disabled {{ background-color: {cs.to_hex(cs.button_disabled_bg)}; color: {cs.to_hex(cs.button_disabled_text)}; }} QCheckBox {{ color: {cs.to_hex(cs.text_primary)}; }} QFrame {{ background-color: {cs.to_hex(cs.panel_bg)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: 0px; }} """
[docs] def generate_config_button_styles(self) -> dict[str, str]: """ Generate individual button styles for config window buttons. Returns: dict: Dictionary with button styles for generic, reset, cancel, save """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return { "generic": f""" QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: 1px solid {cs.to_hex(cs.border_light)}; border-radius: {layout.widget_corner_radius}px; padding: 8px; font-size: 11px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} QPushButton:pressed {{ background-color: {cs.to_hex(cs.button_pressed_bg)}; }} """, "compact": f""" QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: 6px 10px; font-size: 11px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} QPushButton:pressed {{ background-color: {cs.to_hex(cs.button_pressed_bg)}; }} QPushButton:disabled {{ background-color: {cs.to_hex(cs.button_disabled_bg)}; color: {cs.to_hex(cs.button_disabled_text)}; border: none; }} """, "reset": f""" QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: 1px solid {cs.to_hex(cs.border_light)}; border-radius: {layout.widget_corner_radius}px; padding: 8px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} """, "cancel": f""" QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: 1px solid {cs.to_hex(cs.border_light)}; border-radius: {layout.widget_corner_radius}px; padding: 8px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} """, "save": f""" QPushButton {{ background-color: {cs.to_hex(cs.selection_bg)}; color: {cs.to_hex(cs.selection_text)}; border: 1px solid {cs.to_hex(cs.selection_bg)}; border-radius: {layout.widget_corner_radius}px; padding: 8px; }} QPushButton:hover {{ background-color: rgba({cs.selection_bg[0]}, {cs.selection_bg[1]}, {cs.selection_bg[2]}, 0.8); }} """ }
[docs] def generate_scope_accent_button_style(self, accent_color: QColor) -> str: """Generate a scoped accent button style using this theme's button states.""" cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT hex_color = accent_color.name() hex_lighter = accent_color.lighter(115).name() hex_darker = accent_color.darker(115).name() border = "none" text_color = "white" if ( accent_color.red() >= 230 and accent_color.green() >= 230 and accent_color.blue() >= 230 ): hex_color = "#555555" hex_lighter = "#666666" hex_darker = "#444444" border = "1px solid #ffffff" return f""" QPushButton {{ background-color: {hex_color}; color: {text_color}; border: {border}; border-radius: {layout.widget_corner_radius}px; padding: 8px; }} QPushButton:hover {{ background-color: {hex_lighter}; }} QPushButton:pressed {{ background-color: {hex_darker}; }} QPushButton:disabled {{ background-color: {cs.to_hex(cs.button_disabled_bg)}; color: {cs.to_hex(cs.button_disabled_text)}; border: none; }} """
[docs] def require_config_button_style(self, style_name: str) -> str: """Return a named config button style or fail on an undeclared style key.""" button_styles = self.generate_config_button_styles() if style_name not in button_styles: available_styles = ", ".join(sorted(button_styles)) raise KeyError( f"Unknown config button style {style_name!r}; " f"available styles: {available_styles}." ) return button_styles[style_name]
[docs] def generate_plate_manager_style(self) -> str: """ Generate QStyleSheet for plate manager widget with all components. Returns: str: Complete QStyleSheet for plate manager styling """ cs = self.color_scheme layout = layout_constants.CURRENT_LAYOUT return f""" QListWidget {{ background-color: {cs.to_hex(cs.panel_bg)}; color: {cs.to_hex(cs.text_primary)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: 5px; }} QListWidget::item {{ padding: 5px; border: none; background-color: transparent; }} QListWidget::item:selected {{ background-color: transparent; }} QListWidget::item:hover {{ background-color: transparent; }} QFrame {{ background-color: {cs.to_hex(cs.window_bg)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: 5px; }} QPushButton {{ background-color: {cs.to_hex(cs.button_normal_bg)}; color: {cs.to_hex(cs.button_text)}; border: none; border-radius: {layout.widget_corner_radius}px; padding: 5px; }} QPushButton:hover {{ background-color: {cs.to_hex(cs.button_hover_bg)}; }} QPushButton:pressed {{ background-color: {cs.to_hex(cs.button_pressed_bg)}; }} QPushButton:disabled {{ background-color: {cs.to_hex(cs.button_disabled_bg)}; color: {cs.to_hex(cs.button_disabled_text)}; }} QProgressBar {{ border: none; border-radius: {layout.widget_corner_radius}px; background-color: {cs.to_hex(cs.progress_bg)}; color: {cs.to_hex(cs.text_primary)}; text-align: center; }} QProgressBar::chunk {{ background-color: {cs.to_hex(cs.progress_fill)}; border-radius: {layout.progress_chunk_corner_radius}px; }} """
[docs] def get_status_color_hex(self, status_type: str) -> str: """ Get hex color string for status type. Args: status_type: Status type (success, warning, error, info) Returns: str: Hex color string for the status type """ role = StatusColorRole.from_name(status_type) color = STATUS_COLOR_SELECTOR_BY_ROLE[role](self.color_scheme) return self.color_scheme.to_hex(color)