Skip to main content
Glama
theme_system.py23 kB
"""Centralized Theme System for FreeCAD AI GUI Components - Material Design 3""" from enum import Enum from PySide2 import QtWidgets class Theme(Enum): """Available themes.""" LIGHT = "light" DARK = "dark" AUTO = "auto" # Follow system theme class ColorScheme: """Color scheme definitions.""" def __init__(self, theme: Theme = Theme.LIGHT): self.theme = theme self._load_colors() def _load_colors(self): """Load colors based on theme - Material Design 3 inspired.""" if self.theme == Theme.LIGHT: self.colors = { # Background colors - Material Design 3 surfaces "background_primary": "#fdfcff", "background_secondary": "#f5f5f5", "background_tertiary": "#eceff1", "background_accent": "#e7f2ff", "background_elevated": "#ffffff", "background_card": "#ffffff", # Text colors - MD3 on-surface variants "text_primary": "#1c1b1f", "text_secondary": "#49454f", "text_muted": "#73777f", "text_inverse": "#ffffff", "text_on_primary": "#ffffff", # Brand colors - MD3 primary/secondary scheme "primary": "#0061a6", "primary_container": "#d1e4ff", "on_primary": "#ffffff", "on_primary_container": "#001d35", "secondary": "#535f70", "secondary_container": "#d7e3f7", "on_secondary": "#ffffff", "on_secondary_container": "#101c2b", # Semantic colors "success": "#006e1c", "success_container": "#97f682", "on_success": "#ffffff", "on_success_container": "#002204", "warning": "#785900", "warning_container": "#ffdea6", "on_warning": "#ffffff", "on_warning_container": "#261900", "error": "#ba1a1a", "error_container": "#ffdad6", "on_error": "#ffffff", "on_error_container": "#410002", "info": "#0061a6", "info_container": "#d1e4ff", # Chat-specific colors - using MD3 containers "user_message_bg": "#d1e4ff", "ai_message_bg": "#97f682", "system_message_bg": "#ffdea6", "user_text": "#001d35", "ai_text": "#002204", "system_text": "#261900", # UI element colors "border": "#c4c6d0", "border_focus": "#0061a6", "border_light": "#e7e9f5", "divider": "#c4c6d0", "button_primary": "#0061a6", "button_success": "#006e1c", "button_warning": "#785900", "button_danger": "#ba1a1a", # Status colors "status_connected": "#006e1c", "status_warning": "#785900", "status_error": "#ba1a1a", "status_unknown": "#73777f", # Shadows and overlays "shadow": "rgba(0, 0, 0, 0.12)", "overlay": "rgba(0, 0, 0, 0.05)", } else: # DARK theme - Material Design 3 dark surfaces self.colors = { # Background colors - MD3 dark surfaces "background_primary": "#1c1b1f", "background_secondary": "#2b2930", "background_tertiary": "#44464f", "background_accent": "#003258", "background_elevated": "#2b2930", "background_card": "#2b2930", # Text colors - MD3 on-surface dark "text_primary": "#e6e1e5", "text_secondary": "#cac4d0", "text_muted": "#938f99", "text_inverse": "#1c1b1f", "text_on_primary": "#003258", # Brand colors - MD3 dark primary/secondary "primary": "#a0caff", "primary_container": "#004881", "on_primary": "#003258", "on_primary_container": "#d1e4ff", "secondary": "#bbc7db", "secondary_container": "#3c4858", "on_secondary": "#253140", "on_secondary_container": "#d7e3f7", # Semantic colors - MD3 dark "success": "#7cda64", "success_container": "#005313", "on_success": "#003a09", "on_success_container": "#97f682", "warning": "#f5bf42", "warning_container": "#5d4200", "on_warning": "#3f2e00", "on_warning_container": "#ffdea6", "error": "#ffb4ab", "error_container": "#93000a", "on_error": "#690005", "on_error_container": "#ffdad6", "info": "#a0caff", "info_container": "#004881", # Chat-specific colors - MD3 dark containers "user_message_bg": "#003258", "ai_message_bg": "#003a09", "system_message_bg": "#3f2e00", "user_text": "#d1e4ff", "ai_text": "#97f682", "system_text": "#ffdea6", # UI element colors "border": "#44464f", "border_focus": "#a0caff", "border_light": "#2b2930", "divider": "#44464f", "button_primary": "#a0caff", "button_success": "#7cda64", "button_warning": "#f5bf42", "button_danger": "#ffb4ab", # Status colors "status_connected": "#7cda64", "status_warning": "#f5bf42", "status_error": "#ffb4ab", "status_unknown": "#938f99", # Shadows and overlays "shadow": "rgba(0, 0, 0, 0.30)", "overlay": "rgba(255, 255, 255, 0.05)", } def get_color(self, color_name: str) -> str: """Get color by name.""" return self.colors.get(color_name, "#000000") def get_rgba(self, color_name: str, alpha: float = 1.0) -> str: """Get color as rgba string.""" hex_color = self.get_color(color_name) if hex_color.startswith("#"): hex_color = hex_color[1:] r = int(hex_color[0:2], 16) g = int(hex_color[2:4], 16) b = int(hex_color[4:6], 16) return f"rgba({r}, {g}, {b}, {alpha})" class StyleSheet: """Generate consistent stylesheets.""" def __init__(self, color_scheme: ColorScheme): self.colors = color_scheme def get_button_style(self, button_type: str = "primary", elevated: bool = False) -> str: """Get button stylesheet with Material Design 3 principles. Args: button_type: Type of button (primary, success, warning, danger) elevated: Whether to add shadow elevation (default: False for backward compatibility) """ color_map = { "primary": (self.colors.get_color("button_primary"), self.colors.get_color("on_primary")), "success": (self.colors.get_color("button_success"), self.colors.get_color("on_success")), "warning": (self.colors.get_color("button_warning"), self.colors.get_color("on_warning")), "danger": (self.colors.get_color("button_danger"), self.colors.get_color("on_error")), } base_color, text_color = color_map.get(button_type, (self.colors.get_color("primary"), self.colors.get_color("on_primary"))) hover_color = self._lighten_color(base_color, 0.15) pressed_color = self._darken_color(base_color, 0.15) shadow = "box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24);" if elevated else "" hover_shadow = "box-shadow: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);" if elevated else "" return f""" QPushButton {{ background-color: {base_color}; color: {text_color}; border: none; padding: 10px 24px; border-radius: 20px; font-weight: 500; font-size: 14px; min-height: 36px; {shadow} }} QPushButton:hover {{ background-color: {hover_color}; {hover_shadow} }} QPushButton:pressed {{ background-color: {pressed_color}; }} QPushButton:disabled {{ background-color: {self.colors.get_color("text_muted")}; color: {self.colors.get_color("background_primary")}; }} """ def get_compact_button_style(self, button_type: str = "primary") -> str: """Get compact circular button stylesheet for icons. Args: button_type: Type of button (primary, success, warning, danger) """ color_map = { "primary": (self.colors.get_color("button_primary"), self.colors.get_color("on_primary")), "success": (self.colors.get_color("button_success"), self.colors.get_color("on_success")), "warning": (self.colors.get_color("button_warning"), self.colors.get_color("on_warning")), "danger": (self.colors.get_color("button_danger"), self.colors.get_color("on_error")), } base_color, text_color = color_map.get(button_type, (self.colors.get_color("primary"), self.colors.get_color("on_primary"))) hover_color = self.colors.get_color("primary_container") if button_type == "primary" else self._lighten_color(base_color, 0.2) return f""" QPushButton {{ background-color: {base_color}; color: {text_color}; border: none; border-radius: 50%; font-size: 18px; font-weight: bold; }} QPushButton:hover {{ background-color: {hover_color}; }} QPushButton:pressed {{ background-color: {self._darken_color(base_color, 0.1)}; }} """ def get_input_style(self) -> str: """Get input field stylesheet with Material Design 3 outlined style.""" return f""" QLineEdit, QTextEdit, QPlainTextEdit {{ background-color: {self.colors.get_color("background_primary")}; border: 1px solid {self.colors.get_color("border")}; border-radius: 8px; padding: 12px 16px; color: {self.colors.get_color("text_primary")}; font-size: 14px; selection-background-color: {self.colors.get_color("primary_container")}; }} QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {{ border: 2px solid {self.colors.get_color("border_focus")}; padding: 11px 15px; }} QLineEdit:disabled, QTextEdit:disabled, QPlainTextEdit:disabled {{ background-color: {self.colors.get_color("background_secondary")}; color: {self.colors.get_color("text_muted")}; }} """ def get_groupbox_style(self) -> str: """Get group box stylesheet with modern card design.""" return f""" QGroupBox {{ background-color: {self.colors.get_color("background_card")}; border: 1px solid {self.colors.get_color("border_light")}; border-radius: 12px; margin-top: 16px; padding: 16px; color: {self.colors.get_color("text_primary")}; font-weight: 600; font-size: 14px; }} QGroupBox::title {{ subcontrol-origin: margin; subcontrol-position: top left; left: 16px; padding: 0 8px 0 8px; background-color: {self.colors.get_color("background_card")}; color: {self.colors.get_color("primary")}; }} """ def get_conversation_style(self) -> str: """Get conversation display stylesheet with modern styling.""" return f""" QTextBrowser {{ background-color: {self.colors.get_color("background_primary")}; border: 1px solid {self.colors.get_color("border_light")}; border-radius: 12px; padding: 16px; color: {self.colors.get_color("text_primary")}; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 14px; line-height: 1.5; }} """ def get_status_indicator_style(self, status: str) -> str: """Get status indicator stylesheet.""" status_colors = { "connected": self.colors.get_color("status_connected"), "warning": self.colors.get_color("status_warning"), "error": self.colors.get_color("status_error"), "unknown": self.colors.get_color("status_unknown"), } color = status_colors.get(status, self.colors.get_color("status_unknown")) return f""" QLabel {{ color: {color}; font-weight: bold; font-size: 16px; }} """ def get_tab_style(self) -> str: """Get tab widget stylesheet with modern Material Design.""" return f""" QTabWidget::pane {{ border: none; background-color: {self.colors.get_color("background_primary")}; border-radius: 12px; margin-top: 8px; }} QTabBar::tab {{ background-color: transparent; color: {self.colors.get_color("text_secondary")}; padding: 12px 24px; margin-right: 4px; border: none; border-top-left-radius: 12px; border-top-right-radius: 12px; font-weight: 500; font-size: 14px; min-width: 80px; }} QTabBar::tab:selected {{ background-color: {self.colors.get_color("primary")}; color: {self.colors.get_color("text_on_primary")}; }} QTabBar::tab:hover:!selected {{ background-color: {self.colors.get_rgba("primary", 0.12)}; color: {self.colors.get_color("primary")}; }} """ def _darken_color(self, hex_color: str, factor: float) -> str: """Darken a hex color by factor (0.0 to 1.0).""" if hex_color.startswith("#"): hex_color = hex_color[1:] r = int(hex_color[0:2], 16) g = int(hex_color[2:4], 16) b = int(hex_color[4:6], 16) r = max(0, int(r * (1 - factor))) g = max(0, int(g * (1 - factor))) b = max(0, int(b * (1 - factor))) return f"#{r:02x}{g:02x}{b:02x}" def _lighten_color(self, hex_color: str, factor: float) -> str: """Lighten a hex color by factor (0.0 to 1.0).""" if hex_color.startswith("#"): hex_color = hex_color[1:] r = int(hex_color[0:2], 16) g = int(hex_color[2:4], 16) b = int(hex_color[4:6], 16) r = min(255, int(r + (255 - r) * factor)) g = min(255, int(g + (255 - g) * factor)) b = min(255, int(b + (255 - b) * factor)) return f"#{r:02x}{g:02x}{b:02x}" def get_card_style(self, elevated: bool = True) -> str: """Get card container stylesheet with Material Design elevation.""" shadow = """ border: 1px solid rgba(0, 0, 0, 0.04); background-color: {bg}; """ if elevated else f"background-color: {self.colors.get_color('background_card')};" return f""" QFrame {{ {shadow.format(bg=self.colors.get_color('background_card'))} border-radius: 16px; padding: 16px; }} """ def get_chip_style(self, chip_type: str = "default") -> str: """Get chip/badge stylesheet for status indicators.""" type_colors = { "default": (self.colors.get_color("background_secondary"), self.colors.get_color("text_primary")), "primary": (self.colors.get_color("primary_container"), self.colors.get_color("on_primary_container")), "success": (self.colors.get_color("success_container"), self.colors.get_color("on_success_container")), "warning": (self.colors.get_color("warning_container"), self.colors.get_color("on_warning_container")), "error": (self.colors.get_color("error_container"), self.colors.get_color("on_error_container")), } bg_color, text_color = type_colors.get(chip_type, type_colors["default"]) return f""" QLabel {{ background-color: {bg_color}; color: {text_color}; padding: 4px 12px; border-radius: 16px; font-size: 12px; font-weight: 500; }} """ def get_combobox_style(self) -> str: """Get combobox dropdown stylesheet.""" return f""" QComboBox {{ background-color: {self.colors.get_color("background_primary")}; border: 1px solid {self.colors.get_color("border")}; border-radius: 8px; padding: 10px 32px 10px 16px; color: {self.colors.get_color("text_primary")}; font-size: 14px; min-height: 36px; }} QComboBox:hover {{ border-color: {self.colors.get_color("primary")}; }} QComboBox:focus {{ border: 2px solid {self.colors.get_color("border_focus")}; }} QComboBox::drop-down {{ border: none; width: 32px; }} QComboBox::down-arrow {{ image: none; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid {self.colors.get_color("text_secondary")}; margin-right: 12px; }} QComboBox QAbstractItemView {{ background-color: {self.colors.get_color("background_elevated")}; border: 1px solid {self.colors.get_color("border")}; border-radius: 8px; padding: 4px; selection-background-color: {self.colors.get_color("primary_container")}; selection-color: {self.colors.get_color("on_primary_container")}; }} """ class ThemeManager: """Manages themes across the application.""" def __init__(self): self.current_theme = Theme.LIGHT self.color_scheme = ColorScheme(self.current_theme) self.stylesheet = StyleSheet(self.color_scheme) self._theme_changed_callbacks = [] def set_theme(self, theme: Theme): """Set application theme.""" if theme == self.current_theme: return self.current_theme = theme self.color_scheme = ColorScheme(theme) self.stylesheet = StyleSheet(self.color_scheme) # Notify callbacks for callback in self._theme_changed_callbacks: try: callback(theme) except Exception as e: print(f"ThemeManager: Error in theme change callback: {e}") def register_theme_change_callback(self, callback): """Register callback for theme changes.""" self._theme_changed_callbacks.append(callback) def apply_theme_to_widget(self, widget: QtWidgets.QWidget): """Apply current theme to a widget.""" try: # Apply base styling widget.setStyleSheet( f""" QWidget {{ background-color: {self.color_scheme.get_color("background_primary")}; color: {self.color_scheme.get_color("text_primary")}; }} """ ) # Apply specific styling based on widget type self._apply_specific_styling(widget) except Exception as e: print(f"ThemeManager: Error applying theme to widget: {e}") def _apply_specific_styling(self, widget: QtWidgets.QWidget): """Apply theme-specific styling to widget types.""" # Find and style specific widget types buttons = widget.findChildren(QtWidgets.QPushButton) for button in buttons: # Determine button type from style or text if ( "danger" in button.objectName().lower() or "delete" in button.text().lower() or "remove" in button.text().lower() ): button.setStyleSheet(self.stylesheet.get_button_style("danger")) elif ( "success" in button.objectName().lower() or "save" in button.text().lower() or "connect" in button.text().lower() ): button.setStyleSheet(self.stylesheet.get_button_style("success")) elif "warning" in button.objectName().lower(): button.setStyleSheet(self.stylesheet.get_button_style("warning")) else: button.setStyleSheet(self.stylesheet.get_button_style("primary")) # Style input fields inputs = widget.findChildren(QtWidgets.QLineEdit) + widget.findChildren( QtWidgets.QTextEdit ) + widget.findChildren(QtWidgets.QPlainTextEdit) for input_widget in inputs: input_widget.setStyleSheet(self.stylesheet.get_input_style()) # Style combo boxes comboboxes = widget.findChildren(QtWidgets.QComboBox) for combobox in comboboxes: combobox.setStyleSheet(self.stylesheet.get_combobox_style()) # Style group boxes groupboxes = widget.findChildren(QtWidgets.QGroupBox) for groupbox in groupboxes: groupbox.setStyleSheet(self.stylesheet.get_groupbox_style()) # Style tab widgets tabwidgets = widget.findChildren(QtWidgets.QTabWidget) for tabwidget in tabwidgets: tabwidget.setStyleSheet(self.stylesheet.get_tab_style()) # Global theme manager instance _theme_manager = None def get_theme_manager() -> ThemeManager: """Get global theme manager instance.""" global _theme_manager if _theme_manager is None: _theme_manager = ThemeManager() return _theme_manager def apply_theme_to_widget(widget: QtWidgets.QWidget, theme: Theme = None): """Convenience function to apply theme to widget.""" manager = get_theme_manager() if theme and theme != manager.current_theme: manager.set_theme(theme) manager.apply_theme_to_widget(widget) def get_current_color_scheme() -> ColorScheme: """Get current color scheme.""" return get_theme_manager().color_scheme def get_current_stylesheet() -> StyleSheet: """Get current stylesheet generator.""" return get_theme_manager().stylesheet

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jango-blockchained/mcp-freecad'

If you have feedback or need assistance with the MCP directory API, please join our Discord server