"""
Professional theming system for ThreePaneWindows.
This module provides comprehensive theming capabilities including color schemes,
typography, spacing, and platform-specific theme detection and management.
"""
import platform
import tkinter as tk
from dataclasses import dataclass, field
from enum import Enum
from tkinter import ttk
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
if TYPE_CHECKING:
from .custom_scrollbar import ThemedScrollbar
# Import platform-specific functionality
from .logging_config import get_logger
from .utils import platform_handler
# Initialize logger for this module
logger = get_logger(__name__)
[docs]
class ThemeType(Enum):
"""Enumeration of available theme types."""
LIGHT = "light"
DARK = "dark"
BLUE = "blue"
GREEN = "green"
PURPLE = "purple"
CUSTOM = "custom"
SYSTEM = "system"
NATIVE = "native"
NATIVE_LIGHT = "native_light"
NATIVE_DARK = "native_dark"
[docs]
@dataclass
class ColorScheme:
"""Color scheme configuration for themes."""
primary_bg: str = "#ffffff"
secondary_bg: str = "#f5f5f5"
accent_bg: str = "#e3f2fd"
primary_text: str = "#212121"
secondary_text: str = "#757575"
accent_text: str = "#1976d2"
border: str = "#e0e0e0"
separator: str = "#bdbdbd"
button_bg: str = "#2196f3"
button_fg: str = "#ffffff"
button_hover: str = "#1976d2"
button_active: str = "#0d47a1"
success: str = "#4caf50"
warning: str = "#ff9800"
error: str = "#f44336"
info: str = "#2196f3"
panel_header_bg: str = "#fafafa"
panel_header_fg: str = "#424242"
panel_content_bg: str = "#ffffff"
drag_indicator: str = "#2196f3"
drop_zone: str = "#e3f2fd"
[docs]
@dataclass
class Typography:
"""Typography configuration for themes."""
font_family: str = "Segoe UI"
font_family_fallback: str = "Arial"
font_size_small: int = 9
font_size_normal: int = 10
font_size_large: int = 12
font_size_title: int = 14
font_weight_light: str = "normal"
font_weight_normal: str = "normal"
font_weight_medium: str = "bold"
font_weight_bold: str = "bold"
[docs]
@dataclass
class Spacing:
"""Spacing configuration for themes."""
padding_small: int = 4
padding_normal: int = 8
padding_large: int = 16
margin_small: int = 2
margin_normal: int = 4
margin_large: int = 8
border_width: int = 1
separator_width: int = 1
[docs]
@dataclass
class Theme:
"""Complete theme configuration including colors, typography, and spacing."""
name: str
colors: ColorScheme = field(default_factory=ColorScheme)
typography: Typography = field(default_factory=Typography)
spacing: Spacing = field(default_factory=Spacing)
animation_duration: int = 200
enable_animations: bool = True
enable_shadows: bool = True
enable_gradients: bool = False
corner_radius: int = 4
[docs]
class ThemeManager:
"""Manages theme application and platform-specific theme detection."""
[docs]
def __init__(
self,
theme: Optional[Union[str, ThemeType]] = None,
custom_scheme: Optional[ColorScheme] = None,
) -> None:
"""Initialize theme manager with optional theme and custom color scheme."""
self._themes: Dict[str, Theme] = {}
self._current_theme: Optional[Theme] = None
self._style_cache: Dict[str, Dict[str, Any]] = {}
self._initialize_default_themes()
if theme == ThemeType.CUSTOM and custom_scheme:
custom_theme = Theme(name="custom", colors=custom_scheme)
self.register_theme(custom_theme)
self.set_theme("custom")
elif theme:
self.set_theme(theme)
# original
def _initialize_default_themes(self) -> None:
"""Initialize default themes."""
# Light Theme
light_colors = ColorScheme(
primary_bg="#ffffff",
secondary_bg="#f8f9fa",
accent_bg="#e3f2fd",
primary_text="#212121",
secondary_text="#6c757d",
accent_text="#0d6efd",
border="#dee2e6",
separator="#e9ecef",
button_bg="#6c757d", # "#0d6efd",
button_fg="#ffffff",
button_hover="#0b5ed7",
button_active="#0a58ca",
panel_header_bg="#f8f9fa",
panel_header_fg="#495057",
panel_content_bg="#ffffff",
drag_indicator="#0d6efd",
drop_zone="#e7f3ff",
)
self._themes["light"] = Theme(
name="Light",
colors=light_colors,
typography=Typography(font_family="Segoe UI"),
spacing=Spacing(),
)
# Dark Theme
dark_colors = ColorScheme(
primary_bg="#1e1e1e",
secondary_bg="#2d2d30",
accent_bg="#094771",
primary_text="#ffffff",
secondary_text="#cccccc",
accent_text="#4fc3f7",
border="#3e3e42",
separator="#464647",
button_bg="#0e639c",
button_fg="#ffffff",
button_hover="#1177bb",
button_active="#143d66",
panel_header_bg="#2d2d30",
panel_header_fg="#cccccc",
panel_content_bg="#1e1e1e",
drag_indicator="#4fc3f7",
drop_zone="#094771",
)
self._themes["dark"] = Theme(
name="Dark",
colors=dark_colors,
typography=Typography(font_family="Segoe UI"),
spacing=Spacing(),
)
# Blue Professional Theme
blue_colors = ColorScheme(
primary_bg="#fafbfc",
secondary_bg="#f1f3f4",
accent_bg="#e8f0fe",
primary_text="#202124",
secondary_text="#5f6368",
accent_text="#1a73e8",
border="#dadce0",
separator="#e8eaed",
button_bg="#1a73e8",
button_fg="#ffffff",
button_hover="#1557b0",
button_active="#1246a0",
panel_header_bg="#f1f3f4",
panel_header_fg="#3c4043",
panel_content_bg="#ffffff",
drag_indicator="#1a73e8",
drop_zone="#e8f0fe",
)
self._themes["blue"] = Theme(
name="Blue Professional",
colors=blue_colors,
typography=Typography(font_family="Segoe UI"),
spacing=Spacing(),
)
# Green Theme
green_colors = ColorScheme(
primary_bg="#ffffff",
secondary_bg="#f8f9fa",
accent_bg="#e8f5e8",
primary_text="#1b5e20",
secondary_text="#388e3c",
accent_text="#2e7d32",
border="#c8e6c9",
separator="#a5d6a7",
button_bg="#4caf50",
button_fg="#ffffff",
button_hover="#388e3c",
button_active="#2e7d32",
panel_header_bg="#e8f5e8",
panel_header_fg="#1b5e20",
panel_content_bg="#ffffff",
drag_indicator="#4caf50",
drop_zone="#e8f5e8",
)
self._themes["green"] = Theme(
name="Green Nature",
colors=green_colors,
typography=Typography(font_family="Segoe UI"),
spacing=Spacing(),
)
# Purple Theme
purple_colors = ColorScheme(
primary_bg="#ffffff",
secondary_bg="#f8f9fa",
accent_bg="#f3e5f5",
primary_text="#4a148c",
secondary_text="#7b1fa2",
accent_text="#8e24aa",
border="#ce93d8",
separator="#ba68c8",
button_bg="#9c27b0",
button_fg="#ffffff",
button_hover="#7b1fa2",
button_active="#6a1b9a",
panel_header_bg="#f3e5f5",
panel_header_fg="#4a148c",
panel_content_bg="#ffffff",
drag_indicator="#9c27b0",
drop_zone="#f3e5f5",
)
self._themes["purple"] = Theme(
name="Purple Elegance",
colors=purple_colors,
typography=Typography(font_family="Segoe UI"),
spacing=Spacing(),
)
# System Theme - dynamically follows OS theme
self._initialize_system_theme()
# Platform Native Themes - use platform-specific styling
self._initialize_native_themes()
# Set default theme
self._current_theme = self._themes["light"]
def _initialize_system_theme(self) -> None:
"""Initialize system theme that follows OS theme."""
try:
import darkdetect # type: ignore[import-untyped]
is_dark = darkdetect.isDark()
except ImportError:
# Fallback if darkdetect is not available
is_dark = False
# Use existing dark or light theme as base for system theme
base_theme = self._themes["dark"] if is_dark else self._themes["light"]
# Create system theme as a copy of the appropriate base theme
self._themes["system"] = Theme(
name="System",
colors=base_theme.colors,
typography=base_theme.typography,
spacing=base_theme.spacing,
animation_duration=base_theme.animation_duration,
enable_animations=base_theme.enable_animations,
enable_shadows=base_theme.enable_shadows,
enable_gradients=base_theme.enable_gradients,
corner_radius=base_theme.corner_radius,
)
def _update_system_theme(self) -> bool:
"""Update system theme to match current OS theme."""
try:
import darkdetect
is_dark = darkdetect.isDark()
except ImportError:
is_dark = False
# Update system theme to match OS
base_theme = self._themes["dark"] if is_dark else self._themes["light"]
self._themes["system"] = Theme(
name="System",
colors=base_theme.colors,
typography=base_theme.typography,
spacing=base_theme.spacing,
animation_duration=base_theme.animation_duration,
enable_animations=base_theme.enable_animations,
enable_shadows=base_theme.enable_shadows,
enable_gradients=base_theme.enable_gradients,
corner_radius=base_theme.corner_radius,
)
return bool(is_dark)
def _initialize_native_themes(self) -> None:
"""Initialize platform-native themes."""
try:
# Get platform-specific colors and typography
platform_colors = platform_handler.get_platform_native_colors()
platform_typography = platform_handler.get_platform_typography()
if platform_colors and platform_typography:
# Create native light theme
native_light_colors = self._create_native_color_scheme(
platform_handler.get_platform_native_colors(is_dark=False)
)
native_light_typography = self._create_native_typography(
platform_typography
)
self._themes["native_light"] = Theme(
name="Native Light",
colors=native_light_colors,
typography=native_light_typography,
spacing=Spacing(),
)
# Create native dark theme
native_dark_colors = self._create_native_color_scheme(
platform_handler.get_platform_native_colors(is_dark=True)
)
self._themes["native_dark"] = Theme(
name="Native Dark",
colors=native_dark_colors,
typography=native_light_typography, # Same typography for both
spacing=Spacing(),
)
# Create adaptive native theme that follows system
is_system_dark = platform_handler.is_dark_mode()
base_native_theme = (
self._themes["native_dark"]
if is_system_dark
else self._themes["native_light"]
)
self._themes["native"] = Theme(
name="Native",
colors=base_native_theme.colors,
typography=base_native_theme.typography,
spacing=base_native_theme.spacing,
animation_duration=base_native_theme.animation_duration,
enable_animations=base_native_theme.enable_animations,
enable_shadows=base_native_theme.enable_shadows,
enable_gradients=base_native_theme.enable_gradients,
corner_radius=base_native_theme.corner_radius,
)
except Exception as e:
logger.warning("Could not initialize native themes: %s", e)
# Create fallback native themes based on existing themes
self._create_fallback_native_themes()
def _create_native_color_scheme(self, platform_colors: dict) -> ColorScheme:
"""Create a ColorScheme from platform-specific colors."""
# Clean up selection_bg to remove alpha channel if present
selection_bg = platform_colors.get("selection_bg", "#e3f2fd")
if selection_bg and len(selection_bg) > 7: # Has alpha channel
selection_bg = selection_bg[:7] # Remove alpha part
# If selection_bg is still invalid, create a lighter version of accent color
accent_color = platform_colors.get("accent", "#0078d4")
if not selection_bg or selection_bg == accent_color + "40":
selection_bg = self._lighten_color(accent_color, 0.8)
return ColorScheme(
primary_bg=platform_colors.get("window_bg", "#ffffff"),
secondary_bg=platform_colors.get("sidebar_bg", "#f5f5f5"),
accent_bg=selection_bg,
primary_text=platform_colors.get("text_primary", "#000000"),
secondary_text=platform_colors.get("text_secondary", "#666666"),
accent_text=accent_color,
border=platform_colors.get("border", "#cccccc"),
separator=platform_colors.get("separator", "#cccccc"),
button_bg=platform_colors.get("button_bg", "#e6e6e6"),
button_fg=platform_colors.get("text_primary", "#000000"),
button_hover=platform_colors.get("button_hover", "#d9d9d9"),
button_active=accent_color,
success="#4caf50",
warning="#ff9800",
error="#f44336",
info=accent_color,
panel_header_bg=platform_colors.get("sidebar_bg", "#f5f5f5"),
panel_header_fg=platform_colors.get("text_primary", "#000000"),
panel_content_bg=platform_colors.get("content_bg", "#ffffff"),
drag_indicator=accent_color,
drop_zone=selection_bg,
)
def _create_native_typography(self, platform_typography: dict) -> Typography:
"""Create a Typography from platform-specific typography."""
return Typography(
font_family=platform_typography.get("font_family", "Segoe UI"),
font_family_fallback=platform_typography.get(
"font_family_fallback", "Arial"
),
font_size_small=platform_typography.get("font_size_small", 9),
font_size_normal=platform_typography.get("font_size_normal", 10),
font_size_large=platform_typography.get("font_size_large", 12),
font_size_title=platform_typography.get("font_size_title", 14),
font_weight_light=platform_typography.get("font_weight_light", "normal"),
font_weight_normal=platform_typography.get("font_weight_normal", "normal"),
font_weight_medium=platform_typography.get("font_weight_medium", "bold"),
font_weight_bold=platform_typography.get("font_weight_bold", "bold"),
)
def _create_fallback_native_themes(self) -> None:
"""Create fallback native themes when platform detection fails."""
# Use existing themes as base for native themes
self._themes["native_light"] = Theme(
name="Native Light",
colors=self._themes["light"].colors,
typography=self._themes["light"].typography,
spacing=self._themes["light"].spacing,
)
self._themes["native_dark"] = Theme(
name="Native Dark",
colors=self._themes["dark"].colors,
typography=self._themes["dark"].typography,
spacing=self._themes["dark"].spacing,
)
# Adaptive native theme
try:
is_system_dark = platform_handler.is_dark_mode()
base_theme = (
self._themes["native_dark"]
if is_system_dark
else self._themes["native_light"]
)
except (ImportError, AttributeError, KeyError):
base_theme = self._themes["native_light"]
self._themes["native"] = Theme(
name="Native",
colors=base_theme.colors,
typography=base_theme.typography,
spacing=base_theme.spacing,
)
[docs]
def set_theme(
self,
name: Union[str, ThemeType],
custom_scheme: Optional[ColorScheme] = None,
window: Optional[tk.Tk] = None,
) -> bool:
"""Set the active theme by name or type."""
if custom_scheme and (
name == ThemeType.CUSTOM
or (hasattr(name, "value") and name.value == "custom")
):
custom_theme = Theme(name="custom", colors=custom_scheme)
self.register_theme(custom_theme)
name = "custom"
# Handle native theme updates
theme_name = name.value if hasattr(name, "value") else str(name).lower()
if theme_name in ["native", "native_light", "native_dark"]:
self._update_native_themes()
theme = self.get_theme(name)
if theme:
self._current_theme = theme
self._style_cache.clear()
if window:
# Use platform-specific titlebar customization
platform_handler.apply_custom_titlebar(window, theme.colors)
# Apply platform-specific styling for native themes
if theme_name.startswith("native"):
self._apply_platform_specific_styling(window, theme)
# Apply the theme to the entire window
self.apply_theme_to_window(window)
return True
return False
def _update_native_themes(self) -> None:
"""Update native themes to reflect current system settings."""
try:
# Re-initialize native themes with current system settings
self._initialize_native_themes()
except Exception as e:
logger.warning("Could not update native themes: %s", e)
def _apply_platform_specific_styling(self, window: tk.Tk, theme: Theme) -> None:
"""Apply platform-specific styling enhancements."""
try:
# Apply macOS-specific styling if available
if hasattr(platform_handler, "apply_macos_native_styling"):
platform_handler.apply_macos_native_styling(window, theme.colors)
except Exception as e:
logger.warning("Could not apply platform-specific styling: %s", e)
[docs]
def refresh_system_theme(self) -> bool:
"""
Refresh system and native themes to match current OS settings.
Returns:
True if themes were updated successfully
"""
try:
# Update system theme
self._update_system_theme()
# Update native themes
self._update_native_themes()
# If current theme is system or native, refresh it
current_name = (
self._current_theme.name.lower() if self._current_theme else ""
)
if current_name in ["system", "native", "native_light", "native_dark"]:
# Re-apply the current theme to pick up changes
theme_type = None
for theme_enum in ThemeType:
if theme_enum.value == current_name:
theme_type = theme_enum
break
if theme_type:
return self.set_theme(theme_type)
return True
except Exception as e:
logger.warning("Could not refresh system theme: %s", e)
return False
[docs]
def get_theme(self, name: Union[str, ThemeType]) -> Optional[Theme]:
"""Get a theme by name or type."""
if hasattr(name, "value"):
name = name.value
return self._themes.get(str(name).lower())
[docs]
def get_current_theme(self) -> Theme:
"""Get the currently active theme."""
return self._current_theme or self._themes["light"]
@property
def current_theme(self) -> ThemeType:
"""Get the current theme as a ThemeType enum."""
current = self.get_current_theme()
# Find the theme name by comparing the theme object
for name, theme in self._themes.items():
if theme is current:
# Convert string name to ThemeType enum
try:
return ThemeType(name)
except ValueError:
# If it's a custom theme not in ThemeType enum
return ThemeType.CUSTOM
return ThemeType.LIGHT # fallback
@property
def current_scheme(self) -> ColorScheme:
"""Get the current color scheme (alias for current theme colors)."""
return self.get_current_theme().colors
[docs]
def get_color(self, color_name: str) -> Optional[str]:
"""Get a color value from the current theme."""
try:
color_value = getattr(self.get_current_theme().colors, color_name)
return str(color_value) if color_value is not None else None
except AttributeError:
return None
[docs]
def register_theme(self, theme: Theme) -> None:
"""Register a new theme."""
self._themes[theme.name.lower()] = theme
[docs]
def get_available_themes(self) -> List[str]:
"""Get list of all available theme names."""
return list(self._themes.keys())
[docs]
def get_available_theme_types(self) -> List[ThemeType]:
"""Get list of available theme types as enums."""
available_types = []
for name in self._themes.keys():
try:
theme_type = ThemeType(name)
available_types.append(theme_type)
except ValueError:
# Custom themes that don't have enum values
if name == "custom":
available_types.append(ThemeType.CUSTOM)
return available_types
[docs]
def is_native_theme_available(self) -> bool:
"""Check if native themes are available on this platform."""
return "native" in self._themes
[docs]
def get_style(self, component: str, state: str = "normal") -> Dict[str, Any]:
"""Get styling for a component in a specific state."""
cache_key = f"{component}_{state}"
if cache_key in self._style_cache:
return self._style_cache[cache_key]
theme = self.get_current_theme()
style = self._generate_style(component, state, theme)
self._style_cache[cache_key] = style
return style
def _generate_style(
self, component: str, state: str, theme: Theme
) -> Dict[str, Any]:
"""Generate style dictionary for a component."""
colors = theme.colors
typography = theme.typography
spacing = theme.spacing
# Create font tuple with fallback
font_family = self._get_available_font(
typography.font_family, typography.font_family_fallback
)
base_style = {
"font": (font_family, typography.font_size_normal),
"relief": "flat",
"borderwidth": 0,
}
if component == "panel_header":
return {
**base_style,
"bg": colors.panel_header_bg,
"fg": colors.panel_header_fg,
"font": (
font_family,
typography.font_size_normal,
typography.font_weight_bold,
),
"padx": spacing.padding_normal,
"pady": spacing.padding_small,
}
elif component == "panel_content":
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
}
elif component == "button":
if state == "hover":
bg = colors.button_hover
elif state == "active":
bg = colors.button_active
else:
bg = colors.button_bg
return {
**base_style,
"bg": bg,
"fg": colors.button_fg,
"activebackground": colors.button_active,
"activeforeground": colors.button_fg,
"padx": spacing.padding_normal,
"pady": spacing.padding_small,
"cursor": "hand2",
}
elif component == "drag_handle":
return {
**base_style,
"bg": colors.secondary_bg,
"fg": colors.secondary_text,
"cursor": "fleur",
"relief": "raised",
"borderwidth": 1,
}
elif component == "separator":
return {
"bg": colors.separator,
"width": spacing.separator_width,
"relief": "flat",
"borderwidth": 0,
}
return base_style
def _get_available_font(
self, primary_font: str, fallback_font: str = "Arial"
) -> str:
"""
Get an available font from the system, with fallback.
Args:
primary_font: Preferred font family
fallback_font: Fallback font family
Returns:
Available font family name
"""
try:
import tkinter.font as tkfont
available_fonts = tkfont.families()
# Check if primary font is available
if primary_font in available_fonts:
return primary_font
# Check if fallback font is available
if fallback_font in available_fonts:
return fallback_font
# Platform-specific fallbacks
system = platform.system().lower()
if system == "darwin": # macOS
macos_fonts = ["SF Pro Display", "Helvetica Neue", "Helvetica", "Arial"]
for font in macos_fonts:
if font in available_fonts:
return font
elif system == "windows":
windows_fonts = ["Segoe UI", "Tahoma", "Arial"]
for font in windows_fonts:
if font in available_fonts:
return font
else: # Linux and others
linux_fonts = [
"Ubuntu",
"Cantarell",
"DejaVu Sans",
"Liberation Sans",
"Arial",
]
for font in linux_fonts:
if font in available_fonts:
return font
# Final fallback
return "TkDefaultFont"
except Exception:
return fallback_font
def _lighten_color(self, hex_color: str, factor: float) -> str:
"""
Lighten a hex color by mixing it with white.
Args:
hex_color: Hex color string (e.g., '#ff0000')
factor: Lightening factor (0.0 = original, 1.0 = white)
Returns:
Lightened hex color string
"""
hex_color = hex_color.lstrip("#")
if len(hex_color) != 6:
return hex_color
try:
r, g, b = [int(hex_color[i : i + 2], 16) for i in (0, 2, 4)]
# Mix with white
r = int(r + (255 - r) * factor)
g = int(g + (255 - g) * factor)
b = int(b + (255 - b) * factor)
return f"#{r:02x}{g:02x}{b:02x}"
except ValueError:
return hex_color
def _apply_theme_to_single_widget(self, widget) -> None:
"""Apply theme to a single widget without recursion."""
widget_class = widget.winfo_class()
# Skip TTK widgets (handled by apply_ttk_theme)
if widget_class.startswith("T"):
return
# Get the appropriate theme handler
theme_handler = self._get_widget_theme_handler(widget_class)
if theme_handler:
theme_handler(widget)
def _get_widget_theme_handler(self, widget_class: str):
"""Get the appropriate theme handler for a widget class."""
handlers = {
"Text": self._theme_text_widget,
"Listbox": self._theme_listbox_widget,
"Canvas": self._theme_canvas_widget,
"Entry": self._theme_entry_widget,
"Label": self._theme_label_widget,
"Button": self._theme_button_widget,
"Frame": self._theme_frame_widget,
"Toplevel": self._theme_toplevel_widget,
"Tk": self._theme_tk_widget,
}
return handlers.get(widget_class)
def _apply_theme_to_children(self, widget) -> None:
"""Apply theme to all children of a widget."""
try:
for child in widget.winfo_children():
self.apply_theme_to_widget(child, recursive=True)
except Exception as e:
# Some widgets don't support winfo_children() or have other issues
logger.debug(f"Could not apply theme to child widgets of {widget}: {e}")
def _theme_text_widget(self, widget) -> None:
"""Apply theme to Text widget."""
style = self.get_tk_widget_style("text")
widget.configure(**style)
def _theme_listbox_widget(self, widget) -> None:
"""Apply theme to Listbox widget."""
style = self.get_tk_widget_style("listbox")
widget.configure(**style)
def _theme_canvas_widget(self, widget) -> None:
"""Apply theme to Canvas widget."""
style = self.get_tk_widget_style("canvas")
widget.configure(**style)
def _theme_entry_widget(self, widget) -> None:
"""Apply theme to Entry widget."""
style = self.get_tk_widget_style("entry")
widget.configure(**style)
def _theme_label_widget(self, widget) -> None:
"""Apply theme to Label widget."""
style = self.get_tk_widget_style("label")
widget.configure(**style)
def _theme_button_widget(self, widget) -> None:
"""Apply theme to Button widget, skipping scrollbar buttons."""
if not self._is_scrollbar_button(widget):
style = self.get_tk_widget_style("button")
widget.configure(**style)
def _theme_frame_widget(self, widget) -> None:
"""Apply theme to Frame widget, handling scrollbar components specially."""
if self._is_custom_scrollbar(widget):
# This is a custom scrollbar - apply its own theming
widget.apply_theme(self.get_current_theme().colors)
elif not self._is_scrollbar_component(widget):
# Regular frame - apply standard frame styling
style = self.get_tk_widget_style("frame")
widget.configure(**style)
def _theme_toplevel_widget(self, widget) -> None:
"""Apply theme to Toplevel widget."""
theme = self.get_current_theme()
widget.configure(bg=theme.colors.primary_bg)
def _theme_tk_widget(self, widget) -> None:
"""Apply theme to Tk root widget."""
theme = self.get_current_theme()
widget.configure(bg=theme.colors.primary_bg)
def _is_scrollbar_button(self, widget) -> bool:
"""Check if a button belongs to a scrollbar."""
parent = widget.master
return (
parent
and hasattr(parent, "apply_theme")
and "scrollbar" in str(type(parent)).lower()
)
def _is_custom_scrollbar(self, widget) -> bool:
"""Check if a widget is a custom scrollbar."""
return (
hasattr(widget, "apply_theme") and "scrollbar" in str(type(widget)).lower()
)
def _is_scrollbar_component(self, widget) -> bool:
"""Check if a widget is a component of a scrollbar."""
parent = widget.master
# Check if parent is a scrollbar
if (
parent
and hasattr(parent, "apply_theme")
and "scrollbar" in str(type(parent)).lower()
):
return True
# Check if grandparent is a scrollbar
if (
parent
and parent.master
and hasattr(parent.master, "apply_theme")
and "scrollbar" in str(type(parent.master)).lower()
):
return True
return False
[docs]
def apply_theme_to_window(self, window) -> None:
"""
Apply current theme to an entire window and all its widgets.
Args:
window: The root window or toplevel to theme
"""
try:
# Apply TTK theme first
style = ttk.Style(window)
self.apply_ttk_theme(style)
# Apply theme to the window and all its widgets
self.apply_theme_to_widget(window, recursive=True)
except Exception as e:
logger.warning("Could not fully apply theme to window: %s", e)
def _is_dark_theme(self, colors: ColorScheme) -> bool:
"""Determine if a theme is dark based on its background color."""
# Convert hex to RGB and check brightness
bg_color = colors.panel_content_bg.lstrip("#")
r, g, b = [int(bg_color[i : i + 2], 16) for i in (0, 2, 4)]
# Calculate luminance (perceived brightness)
luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance < 0.5 # Dark if luminance is less than 50%
def _get_widget_style_handler(self, widget_type: str):
"""Get the appropriate style handler for a widget type."""
handlers = {
"text": self._get_text_widget_style,
"listbox": self._get_listbox_widget_style,
"scrollbar": self._get_scrollbar_widget_style,
"canvas": self._get_canvas_widget_style,
"frame": self._get_frame_widget_style,
"toplevel": self._get_toplevel_widget_style,
"label": self._get_label_widget_style,
"button": self._get_button_widget_style,
"entry": self._get_entry_widget_style,
"checkbutton": self._get_checkbutton_widget_style,
"radiobutton": self._get_radiobutton_widget_style,
"scale": self._get_scale_widget_style,
"spinbox": self._get_spinbox_widget_style,
"menu": self._get_menu_widget_style,
"menubutton": self._get_menubutton_widget_style,
"message": self._get_message_widget_style,
}
return handlers.get(widget_type)
def _get_base_widget_style(self) -> Dict[str, Any]:
"""Get base styling that applies to all widgets."""
theme = self.get_current_theme()
typography = theme.typography
# Create font tuple with fallback
font_family = self._get_available_font(
typography.font_family, typography.font_family_fallback
)
return {
"font": (font_family, typography.font_size_normal),
"relief": "flat",
"borderwidth": 0,
}
def _get_text_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Text widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"insertbackground": colors.primary_text,
"selectbackground": colors.accent_bg,
"selectforeground": colors.primary_text,
"highlightcolor": colors.accent_text,
"highlightbackground": colors.border,
"highlightthickness": 1,
"borderwidth": 1,
"relief": "solid",
}
def _get_listbox_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Listbox widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"selectbackground": colors.accent_bg,
"selectforeground": colors.primary_text,
"highlightcolor": colors.accent_text,
"highlightbackground": colors.border,
"highlightthickness": 1,
"borderwidth": 1,
"relief": "solid",
"activestyle": "dotbox",
}
def _get_scrollbar_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Scrollbar widgets."""
theme = self.get_current_theme()
colors = theme.colors
# For dark themes, use specific dark colors
is_dark_theme = self._is_dark_theme(colors)
if is_dark_theme:
scrollbar_bg = colors.secondary_bg # #2d2d30 for dark theme
trough_color = colors.panel_content_bg # #1e1e1e for dark theme
border_color = colors.panel_content_bg # #1e1e1e for dark theme
else:
# For colored themes, use white bg and theme color for trough/border
scrollbar_bg = "#ffffff" # White background
trough_color = colors.accent_bg # Theme color for trough
border_color = colors.accent_bg # Theme color for border
return {
"bg": scrollbar_bg,
"troughcolor": trough_color,
"activebackground": colors.button_hover,
"highlightbackground": border_color,
"highlightcolor": colors.accent_text,
"relief": "flat",
"borderwidth": 1,
"highlightthickness": 0,
"elementborderwidth": 1,
"width": 16,
# Additional options that work on most platforms
"jump": 1,
"repeatdelay": 300,
"repeatinterval": 100,
}
def _get_canvas_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Canvas widgets."""
theme = self.get_current_theme()
colors = theme.colors
return {
"bg": colors.panel_content_bg,
"highlightcolor": colors.accent_text,
"highlightbackground": colors.border,
"highlightthickness": 1,
"relief": "flat",
"borderwidth": 0,
}
def _get_frame_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Frame widgets."""
theme = self.get_current_theme()
colors = theme.colors
return {
"bg": colors.panel_content_bg,
"relief": "flat",
"borderwidth": 0,
}
def _get_toplevel_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Toplevel widgets."""
theme = self.get_current_theme()
colors = theme.colors
return {
"bg": colors.panel_content_bg,
"relief": "flat",
"borderwidth": 0,
}
def _get_label_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Label widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
}
def _get_button_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Button widgets."""
theme = self.get_current_theme()
colors = theme.colors
spacing = theme.spacing
base_style = self._get_base_widget_style()
if state == "hover":
bg = colors.button_hover
elif state == "active":
bg = colors.button_active
else:
bg = colors.button_bg
return {
**base_style,
"bg": bg,
"fg": colors.button_fg,
"activebackground": colors.button_active,
"activeforeground": colors.button_fg,
"cursor": "hand2",
"padx": spacing.padding_normal,
"pady": spacing.padding_small,
}
def _get_entry_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Entry widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"insertbackground": colors.primary_text,
"selectbackground": colors.accent_bg,
"selectforeground": colors.primary_text,
"highlightcolor": colors.accent_text,
"highlightbackground": colors.border,
"highlightthickness": 1,
"borderwidth": 1,
"relief": "solid",
}
def _get_checkbutton_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Checkbutton widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"activebackground": colors.panel_content_bg,
"activeforeground": colors.primary_text,
"selectcolor": colors.panel_content_bg,
"cursor": "hand2",
}
def _get_radiobutton_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Radiobutton widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"activebackground": colors.panel_content_bg,
"activeforeground": colors.primary_text,
"selectcolor": colors.panel_content_bg,
"cursor": "hand2",
}
def _get_scale_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Scale widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"troughcolor": colors.secondary_bg,
"activebackground": colors.button_hover,
"highlightcolor": colors.accent_text,
"highlightbackground": colors.border,
}
def _get_spinbox_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Spinbox widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"insertbackground": colors.primary_text,
"selectbackground": colors.accent_bg,
"selectforeground": colors.primary_text,
"highlightcolor": colors.accent_text,
"highlightbackground": colors.border,
"highlightthickness": 1,
"borderwidth": 1,
"relief": "solid",
"buttonbackground": colors.button_bg,
}
def _get_menu_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Menu widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
"activebackground": colors.accent_bg,
"activeforeground": colors.primary_text,
"selectcolor": colors.accent_bg,
"borderwidth": 1,
"relief": "solid",
}
def _get_menubutton_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Menubutton widgets."""
theme = self.get_current_theme()
colors = theme.colors
spacing = theme.spacing
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.button_bg,
"fg": colors.button_fg,
"activebackground": colors.button_hover,
"activeforeground": colors.button_fg,
"cursor": "hand2",
"padx": spacing.padding_normal,
"pady": spacing.padding_small,
}
def _get_message_widget_style(self, state: str = "normal") -> Dict[str, Any]:
"""Get styling for Message widgets."""
theme = self.get_current_theme()
colors = theme.colors
base_style = self._get_base_widget_style()
return {
**base_style,
"bg": colors.panel_content_bg,
"fg": colors.primary_text,
}
[docs]
def apply_ttk_theme(self, style: ttk.Style) -> None:
"""Apply current theme to ttk widgets."""
theme = self.get_current_theme()
colors = theme.colors
typography = theme.typography
# Get available font with fallback
font_family = self._get_available_font(
typography.font_family, typography.font_family_fallback
)
# Configure ttk styles - choose base theme based on current theme
base_theme = "clam" # Default base theme
# Choose better base theme based on platform and theme
# Use clam for better theming control across all platforms
try:
available_themes = style.theme_names()
# Always use clam for consistent theming
if "clam" in available_themes:
base_theme = "clam"
else:
# Fallback to default if clam is not available
base_theme = available_themes[0] if available_themes else "default"
except Exception:
base_theme = "clam"
style.theme_use(base_theme)
# Configure default TTK widget styles
# These will apply to all TTK widgets unless overridden
# Default Label
style.configure(
"TLabel",
background=colors.panel_content_bg,
foreground=colors.primary_text,
font=(font_family, typography.font_size_normal),
)
# Default Button
style.configure(
"TButton",
background=colors.button_bg,
foreground=colors.button_fg,
font=(font_family, typography.font_size_normal),
borderwidth=1,
focuscolor="none",
relief="raised",
padding=(8, 4),
)
style.map(
"TButton",
background=[
("active", colors.button_hover),
("pressed", colors.button_active),
("disabled", colors.secondary_bg),
],
foreground=[
("active", colors.button_fg),
("pressed", colors.button_fg),
("disabled", colors.secondary_text),
],
relief=[
("pressed", "sunken"),
("active", "raised"),
],
)
# Default Entry
style.configure(
"TEntry",
fieldbackground=colors.panel_content_bg,
foreground=colors.primary_text,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
font=(font_family, typography.font_size_normal),
borderwidth=1,
)
style.map(
"TEntry",
focuscolor=[("focus", colors.accent_text)],
bordercolor=[("focus", colors.accent_text)],
)
# Default Combobox
style.configure(
"TCombobox",
fieldbackground=colors.panel_content_bg,
foreground=colors.primary_text,
background=colors.button_bg,
bordercolor=colors.border,
arrowcolor=colors.primary_text,
font=(font_family, typography.font_size_normal),
borderwidth=1,
)
style.map(
"TCombobox",
focuscolor=[("focus", colors.accent_text)],
bordercolor=[("focus", colors.accent_text)],
fieldbackground=[("readonly", colors.secondary_bg)],
)
# Default Checkbutton
style.configure(
"TCheckbutton",
background=colors.panel_content_bg,
foreground=colors.primary_text,
focuscolor="none",
font=(font_family, typography.font_size_normal),
)
style.map(
"TCheckbutton",
background=[("active", colors.panel_content_bg)],
indicatorcolor=[
("selected", colors.accent_text),
("pressed", colors.button_active),
],
)
# Default Radiobutton
style.configure(
"TRadiobutton",
background=colors.panel_content_bg,
foreground=colors.primary_text,
focuscolor="none",
font=(font_family, typography.font_size_normal),
)
style.map(
"TRadiobutton",
background=[("active", colors.panel_content_bg)],
indicatorcolor=[
("selected", colors.accent_text),
("pressed", colors.button_active),
],
)
# Default Frame
style.configure(
"TFrame",
background=colors.panel_content_bg,
borderwidth=0,
)
# Default LabelFrame
style.configure(
"TLabelframe",
background=colors.panel_content_bg,
foreground=colors.primary_text,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
font=(font_family, typography.font_size_normal),
borderwidth=1,
)
style.configure(
"TLabelframe.Label",
background=colors.panel_content_bg,
foreground=colors.primary_text,
font=(font_family, typography.font_size_normal),
)
# Default Notebook
style.configure(
"TNotebook",
background=colors.secondary_bg,
borderwidth=1,
tabmargins=[2, 5, 2, 0],
)
style.configure(
"TNotebook.Tab",
background=colors.secondary_bg,
foreground=colors.primary_text,
padding=[12, 8, 12, 8],
font=(font_family, typography.font_size_normal),
)
style.map(
"TNotebook.Tab",
background=[
("selected", colors.panel_content_bg),
("active", colors.accent_bg),
],
foreground=[
("selected", colors.primary_text),
("active", colors.primary_text),
],
)
# Default Progressbar
style.configure(
"TProgressbar",
background=colors.accent_text,
troughcolor=colors.secondary_bg,
borderwidth=1,
lightcolor=colors.border,
darkcolor=colors.border,
)
# Default Scale
style.configure(
"TScale",
background=colors.panel_content_bg,
troughcolor=colors.secondary_bg,
borderwidth=1,
lightcolor=colors.border,
darkcolor=colors.border,
)
# Default Scrollbar
style.configure(
"TScrollbar",
background=colors.secondary_bg,
troughcolor=colors.panel_content_bg,
bordercolor=colors.border,
arrowcolor=colors.primary_text,
darkcolor=colors.border,
lightcolor=colors.border,
)
style.map(
"TScrollbar",
background=[
("active", colors.accent_bg),
("pressed", colors.accent_text),
],
)
# Default Treeview
style.configure(
"Treeview",
background=colors.panel_content_bg,
foreground=colors.primary_text,
fieldbackground=colors.panel_content_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
font=(font_family, typography.font_size_normal),
)
style.configure(
"Treeview.Heading",
background=colors.panel_header_bg,
foreground=colors.panel_header_fg,
font=(
font_family,
typography.font_size_normal,
typography.font_weight_bold,
),
)
style.map(
"Treeview",
background=[("selected", colors.accent_bg)],
foreground=[("selected", colors.primary_text)],
)
style.map(
"Treeview.Heading",
background=[("active", colors.accent_bg)],
)
# Custom themed styles (for explicit theming)
# PanedWindow
style.configure(
"Themed.TPanedwindow",
background=colors.secondary_bg,
borderwidth=0,
relief="flat",
)
style.configure(
"Themed.TPanedwindow.Sash",
sashthickness=4,
gripcount=0,
background=colors.separator,
)
# Frame
style.configure(
"Themed.TFrame",
background=colors.panel_content_bg,
borderwidth=0,
relief="flat",
)
style.configure(
"Header.TFrame",
background=colors.panel_header_bg,
borderwidth=theme.spacing.border_width,
relief="solid",
)
# Label
style.configure(
"Themed.TLabel",
background=colors.panel_content_bg,
foreground=colors.primary_text,
font=(font_family, typography.font_size_normal),
)
style.configure(
"Header.TLabel",
background=colors.panel_header_bg,
foreground=colors.panel_header_fg,
font=(
font_family,
typography.font_size_normal,
typography.font_weight_bold,
),
)
# Button
style.configure(
"Themed.TButton",
background=colors.button_bg,
foreground=colors.button_fg,
borderwidth=0,
focuscolor="none",
font=(typography.font_family, typography.font_size_normal),
)
style.map(
"Themed.TButton",
background=[
("active", colors.button_hover),
("pressed", colors.button_active),
],
)
# Checkbutton
style.configure(
"Themed.TCheckbutton",
background=colors.panel_content_bg,
foreground=colors.primary_text,
focuscolor="none",
font=(typography.font_family, typography.font_size_normal),
)
style.map(
"Themed.TCheckbutton",
background=[
("active", colors.panel_content_bg),
("pressed", colors.panel_content_bg),
],
foreground=[
("active", colors.primary_text),
("pressed", colors.primary_text),
],
)
# Radiobutton
style.configure(
"Themed.TRadiobutton",
background=colors.panel_content_bg,
foreground=colors.primary_text,
focuscolor="none",
font=(typography.font_family, typography.font_size_normal),
)
style.map(
"Themed.TRadiobutton",
background=[
("active", colors.panel_content_bg),
("pressed", colors.panel_content_bg),
],
foreground=[
("active", colors.primary_text),
("pressed", colors.primary_text),
],
)
# Entry
style.configure(
"Themed.TEntry",
fieldbackground=colors.panel_content_bg,
background=colors.panel_content_bg,
foreground=colors.primary_text,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
insertcolor=colors.primary_text,
selectbackground=colors.accent_bg,
selectforeground=colors.primary_text,
font=(typography.font_family, typography.font_size_normal),
)
style.map(
"Themed.TEntry",
fieldbackground=[
("focus", colors.panel_content_bg),
("!focus", colors.panel_content_bg),
],
bordercolor=[
("focus", colors.accent_text),
("!focus", colors.border),
],
)
# Combobox
style.configure(
"Themed.TCombobox",
fieldbackground=colors.panel_content_bg,
background=colors.panel_content_bg,
foreground=colors.primary_text,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
insertcolor=colors.primary_text,
selectbackground=colors.accent_bg,
selectforeground=colors.primary_text,
font=(typography.font_family, typography.font_size_normal),
)
style.map(
"Themed.TCombobox",
fieldbackground=[
("focus", colors.panel_content_bg),
("!focus", colors.panel_content_bg),
],
bordercolor=[
("focus", colors.accent_text),
("!focus", colors.border),
],
)
# Spinbox
style.configure(
"Themed.TSpinbox",
fieldbackground=colors.panel_content_bg,
background=colors.panel_content_bg,
foreground=colors.primary_text,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
insertcolor=colors.primary_text,
selectbackground=colors.accent_bg,
selectforeground=colors.primary_text,
font=(typography.font_family, typography.font_size_normal),
)
style.map(
"Themed.TSpinbox",
fieldbackground=[
("focus", colors.panel_content_bg),
("!focus", colors.panel_content_bg),
],
bordercolor=[
("focus", colors.accent_text),
("!focus", colors.border),
],
)
# Scale - configure both orientations
style.configure(
"Themed.Horizontal.TScale",
background=colors.panel_content_bg,
troughcolor=colors.secondary_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
)
style.configure(
"Themed.Vertical.TScale",
background=colors.panel_content_bg,
troughcolor=colors.secondary_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
)
# Progressbar - configure both orientations
style.configure(
"Themed.Horizontal.TProgressbar",
background=colors.accent_bg,
troughcolor=colors.secondary_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
)
style.configure(
"Themed.Vertical.TProgressbar",
background=colors.accent_bg,
troughcolor=colors.secondary_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
)
# Scrollbar - Enhanced theming for better visibility
is_dark_theme = self._is_dark_theme(colors)
if is_dark_theme:
scrollbar_bg = colors.secondary_bg
scrollbar_trough = colors.panel_content_bg
scrollbar_arrow = colors.secondary_text
else:
scrollbar_bg = colors.panel_content_bg
scrollbar_trough = colors.accent_bg
scrollbar_arrow = colors.primary_text
style.configure(
"Themed.Vertical.TScrollbar",
background=scrollbar_bg,
troughcolor=scrollbar_trough,
bordercolor=colors.border,
arrowcolor=scrollbar_arrow,
darkcolor=colors.border,
lightcolor=colors.panel_content_bg,
relief="flat",
borderwidth=1,
)
style.configure(
"Themed.Horizontal.TScrollbar",
background=scrollbar_bg,
troughcolor=scrollbar_trough,
bordercolor=colors.border,
arrowcolor=scrollbar_arrow,
darkcolor=colors.border,
lightcolor=colors.panel_content_bg,
relief="flat",
borderwidth=1,
)
style.map(
"Themed.Vertical.TScrollbar",
background=[
("active", colors.button_hover),
("pressed", colors.button_active),
],
troughcolor=[
("active", scrollbar_trough),
("pressed", scrollbar_trough),
],
arrowcolor=[
("active", colors.accent_text),
("pressed", colors.accent_text),
],
)
style.map(
"Themed.Horizontal.TScrollbar",
background=[
("active", colors.button_hover),
("pressed", colors.button_active),
],
troughcolor=[
("active", scrollbar_trough),
("pressed", scrollbar_trough),
],
arrowcolor=[
("active", colors.accent_text),
("pressed", colors.accent_text),
],
)
# Listbox (via Treeview styling)
style.configure(
"Themed.Treeview",
background=colors.panel_content_bg,
foreground=colors.primary_text,
fieldbackground=colors.panel_content_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
font=(typography.font_family, typography.font_size_normal),
)
style.configure(
"Themed.Treeview.Heading",
background=colors.panel_header_bg,
foreground=colors.panel_header_fg,
font=(
typography.font_family,
typography.font_size_normal,
typography.font_weight_bold,
),
)
style.map(
"Themed.Treeview",
background=[
("selected", colors.accent_bg),
("focus", colors.accent_bg),
],
foreground=[
("selected", colors.primary_text),
("focus", colors.primary_text),
],
)
# Notebook
style.configure(
"Themed.TNotebook",
background=colors.secondary_bg,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
)
style.configure(
"Themed.TNotebook.Tab",
background=colors.secondary_bg,
foreground=colors.secondary_text,
bordercolor=colors.border,
lightcolor=colors.border,
darkcolor=colors.border,
font=(typography.font_family, typography.font_size_normal),
padding=[12, 8, 12, 8],
)
style.map(
"Themed.TNotebook.Tab",
background=[
("selected", colors.panel_content_bg),
("active", colors.accent_bg),
],
foreground=[
("selected", colors.primary_text),
("active", colors.primary_text),
],
)
# Global theme manager instance
_global_theme_manager: Optional[ThemeManager] = None
[docs]
def get_theme_manager() -> ThemeManager:
"""Get the global theme manager instance."""
global _global_theme_manager
if _global_theme_manager is None:
_global_theme_manager = ThemeManager()
return _global_theme_manager
[docs]
def set_global_theme(
theme: Union[str, ThemeType],
custom_scheme: Optional[ColorScheme] = None,
window: Optional[tk.Tk] = None,
) -> bool:
"""Set the global theme for all ThreePaneWindows components."""
return get_theme_manager().set_theme(theme, custom_scheme, window)
# Add missing methods to ThemeManager class
def _add_missing_methods():
"""Add missing methods to ThemeManager class."""
from typing import Any, Callable, Dict, List, Union
def get_available_themes(self) -> List[str]:
"""Get list of available theme names."""
return list(self._themes.keys())
def list_themes(self) -> Dict[str, str]:
"""Get dictionary of theme names and their display names."""
return {name: theme.name for name, theme in self._themes.items()}
def should_use_custom_scrollbars(self) -> bool:
"""
Determine whether to use custom scrollbars based on platform.
Returns:
bool: True if custom scrollbars should be used, False for native scrollbars
Platform-specific behavior:
- Windows: Custom scrollbars (better theming support)
- macOS/Linux: Native scrollbars (better system integration)
"""
return platform.system() == "Windows"
def get_platform_info(self) -> Dict[str, str]:
"""
Get platform information for display purposes.
Returns:
Dict containing platform name and recommended scrollbar type
"""
system = platform.system()
scrollbar_type = "custom" if self.should_use_custom_scrollbars() else "native"
return {
"platform": system,
"scrollbar_type": scrollbar_type,
"scrollbar_description": (
"Custom scrollbars (better theming)"
if scrollbar_type == "custom"
else "Native scrollbars (better integration)"
),
}
def create_themed_scrollbar_auto(
self,
parent: tk.Widget,
orient: str = "vertical",
command: Optional[Callable] = None,
**kwargs: Any,
) -> Union["ThemedScrollbar", ttk.Scrollbar]:
"""
Create a scrollbar with automatic platform-specific type selection.
Args:
parent: Parent widget
orient: Scrollbar orientation ("vertical" or "horizontal")
command: Scroll command callback
**kwargs: Additional arguments passed to scrollbar creation
Returns:
Scrollbar widget (custom or native based on platform)
"""
# Import here to avoid circular imports
from .custom_scrollbar import create_themed_scrollbar
use_custom = self.should_use_custom_scrollbars()
scrollbar = create_themed_scrollbar(
parent=parent,
orient=orient,
command=command,
use_custom=use_custom,
theme_manager=self,
**kwargs,
)
return scrollbar # type: ignore[no-any-return]
# Add methods to ThemeManager class
ThemeManager.get_available_themes = get_available_themes
ThemeManager.list_themes = list_themes
ThemeManager.should_use_custom_scrollbars = should_use_custom_scrollbars
ThemeManager.get_platform_info = get_platform_info
ThemeManager.create_themed_scrollbar_auto = create_themed_scrollbar_auto
# Call the function to add missing methods
_add_missing_methods()