Enhance theme management with combo box for selection; implement dark title bar theming across platforms

This commit is contained in:
2025-07-31 14:59:09 +02:00
parent 7aeaae8880
commit 2a6940ea3e
+185 -46
View File
@@ -1347,7 +1347,7 @@ class MainWindow(QtWidgets.QMainWindow):
logger.info(f'System theme changed to: {"Dark" if current_system_theme else "Light"}') logger.info(f'System theme changed to: {"Dark" if current_system_theme else "Light"}')
self._last_system_theme = current_system_theme self._last_system_theme = current_system_theme
self.apply_theme() self.apply_theme()
self._update_theme_button_text() self._update_theme_combo_text()
except Exception as e: except Exception as e:
logger.debug(f"Error checking system theme change: {e}") logger.debug(f"Error checking system theme change: {e}")
@@ -1791,6 +1791,12 @@ class MainWindow(QtWidgets.QMainWindow):
self._geometry_save_timer.start(500) # Save after 500ms of no resizing self._geometry_save_timer.start(500) # Save after 500ms of no resizing
def showEvent(self, event):
"""Handle window show event to ensure title bar theming is applied."""
super().showEvent(event)
# Apply title bar theming when window is shown
QtCore.QTimer.singleShot(50, lambda: self.apply_dark_title_bar(self.get_effective_dark_mode()))
def changeEvent(self, event): def changeEvent(self, event):
"""Handle window state changes (maximize/minimize).""" """Handle window state changes (maximize/minimize)."""
super().changeEvent(event) super().changeEvent(event)
@@ -1967,11 +1973,128 @@ class MainWindow(QtWidgets.QMainWindow):
'text_dark': '#000000' 'text_dark': '#000000'
} }
def apply_dark_title_bar(self, enable_dark=True):
"""
Apply dark mode to the window title bar.
Works on Windows 10/11, macOS, and some Linux desktop environments.
"""
try:
import platform
system = platform.system().lower()
if system == 'windows':
# Windows 10/11 dark title bar
try:
import ctypes
from ctypes import wintypes
# Get window handle
hwnd = int(self.winId())
# Define constants for Windows API
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
# Check Windows version to determine which attribute to use
import sys
windows_version = sys.getwindowsversion()
# Windows 11 or Windows 10 version 2004 and later use the newer attribute
use_newer_api = (windows_version.major > 10 or
(windows_version.major == 10 and windows_version.build >= 19041))
attribute = DWMWA_USE_IMMERSIVE_DARK_MODE if use_newer_api else DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1
try:
result = ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd,
attribute,
ctypes.byref(ctypes.c_int(1 if enable_dark else 0)),
ctypes.sizeof(ctypes.c_int)
)
if result == 0: # S_OK
logger.debug(f"Applied Windows dark title bar ({'newer' if use_newer_api else 'legacy'} API)")
# Force window to redraw
self.update()
return True
else:
logger.debug(f"Windows title bar API returned error code: {result}")
except Exception as e:
logger.debug(f"Windows dark title bar API call failed: {e}")
# Try the other API as fallback
fallback_attribute = DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 if use_newer_api else DWMWA_USE_IMMERSIVE_DARK_MODE
try:
result = ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd,
fallback_attribute,
ctypes.byref(ctypes.c_int(1 if enable_dark else 0)),
ctypes.sizeof(ctypes.c_int)
)
if result == 0:
logger.debug("Applied Windows dark title bar (fallback API)")
self.update()
return True
except Exception as fallback_e:
logger.debug(f"Windows dark title bar fallback failed: {fallback_e}")
except ImportError:
logger.debug("ctypes not available for Windows title bar theming")
except Exception as e:
logger.debug(f"Windows title bar theming error: {e}")
elif system == 'darwin': # macOS
try:
# For macOS, we could use PyObjC bindings if available
# This is a simplified approach - full implementation would require PyObjC
logger.debug("macOS title bar theming requires PyObjC bindings (not implemented)")
# Alternative: Set window flags that might influence appearance
# This is limited but might work in some cases
if enable_dark:
# Try to hint to the system that we prefer dark appearance
pass
except Exception as e:
logger.debug(f"macOS title bar theming failed: {e}")
elif system == 'linux':
# Linux title bar theming depends on the window manager/desktop environment
try:
desktop_env = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
# For X11 systems, we might be able to set window properties
if os.environ.get('DISPLAY'):
try:
# This would require X11 libraries and is highly dependent on WM
logger.debug(f"Linux X11 desktop environment: {desktop_env} - title bar theming limited")
except Exception as e:
logger.debug(f"Linux X11 title bar theming failed: {e}")
# For Wayland, title bar theming is even more limited
elif os.environ.get('WAYLAND_DISPLAY'):
logger.debug("Linux Wayland - title bar theming not supported")
except Exception as e:
logger.debug(f"Linux title bar theming failed: {e}")
except Exception as e:
logger.debug(f"Title bar theming failed: {e}")
return False
def apply_theme(self): def apply_theme(self):
""" """
Apply the current theme to all UI elements. Apply the current theme to all UI elements including title bar.
""" """
colors = self.get_theme_styles() colors = self.get_theme_styles()
is_dark = self.get_effective_dark_mode()
# Apply dark title bar if in dark mode
self.apply_dark_title_bar(is_dark)
# Main window style # Main window style
main_style = f""" main_style = f"""
@@ -2072,45 +2195,48 @@ class MainWindow(QtWidgets.QMainWindow):
self.performance_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') self.performance_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};')
self.interval_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') self.interval_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};')
def _update_theme_button_text(self): def on_theme_changed(self, theme_text):
""" """
Update the theme button text based on current theme mode. Handle theme dropdown selection change.
"""
# Map display text to internal theme mode
theme_map = {
'System': 'system',
'Light': 'light',
'Dark': 'dark'
}
new_theme = theme_map.get(theme_text, 'system')
if new_theme != self.theme_mode:
self.theme_mode = new_theme
# Start/stop theme monitoring based on mode
if self.theme_mode == 'system':
self._last_system_theme = self.is_system_dark_theme()
if hasattr(self, 'theme_monitor_timer'):
self.theme_monitor_timer.start(5000) # Check every 5 seconds
else:
if hasattr(self, 'theme_monitor_timer'):
self.theme_monitor_timer.stop()
self.apply_theme()
self._update_theme_combo_text()
logger.info(f'Theme changed to: {self.theme_mode} mode')
def _update_theme_combo_text(self):
"""
Update the theme combo box text based on current theme mode and system state.
""" """
if self.theme_mode == 'system': if self.theme_mode == 'system':
system_dark = self.is_system_dark_theme() system_dark = self.is_system_dark_theme()
theme_text = f"System ({'Dark' if system_dark else 'Light'})" display_text = f"System ({'Dark' if system_dark else 'Light'})"
elif self.theme_mode == 'light': # Update the first item's text to show current system theme
theme_text = "Light" self.theme_combo.setItemText(0, display_text)
else: # dark
theme_text = "Dark"
if hasattr(self, 'btn_theme'):
self.btn_theme.setText(theme_text)
def toggle_theme(self):
"""
Cycle through theme modes: System -> Light -> Dark -> System...
"""
if self.theme_mode == 'system':
self.theme_mode = 'light'
elif self.theme_mode == 'light':
self.theme_mode = 'dark'
else: # dark
self.theme_mode = 'system'
# Start/stop theme monitoring based on mode
if self.theme_mode == 'system':
self._last_system_theme = self.is_system_dark_theme()
if hasattr(self, 'theme_monitor_timer'):
self.theme_monitor_timer.start(5000) # Check every 5 seconds
else: else:
if hasattr(self, 'theme_monitor_timer'): # Reset system item text
self.theme_monitor_timer.stop() self.theme_combo.setItemText(0, 'System')
self.apply_theme()
self._update_theme_button_text()
logger.info(f'Theme changed to: {self.theme_mode} mode')
def init_ui(self): def init_ui(self):
""" """
@@ -2217,11 +2343,16 @@ class MainWindow(QtWidgets.QMainWindow):
self.btn_start_stop.setMinimumWidth(80) self.btn_start_stop.setMinimumWidth(80)
self.btn_start_stop.clicked.connect(self._log_btn_start_stop) self.btn_start_stop.clicked.connect(self._log_btn_start_stop)
# Theme toggle button # Theme dropdown
self.btn_theme = QtWidgets.QPushButton('System') self.theme_combo = QtWidgets.QComboBox()
self.btn_theme.setMinimumWidth(80) self.theme_combo.setMinimumWidth(100)
self.btn_theme.setToolTip('Cycle through theme modes: System (follows OS) -> Light -> Dark -> System...') self.theme_combo.setMaximumWidth(120)
self.btn_theme.clicked.connect(self.toggle_theme) self.theme_combo.setToolTip('Select theme: System (follows OS), Light, or Dark')
self.theme_combo.addItem('System', 'system')
self.theme_combo.addItem('Light', 'light')
self.theme_combo.addItem('Dark', 'dark')
self.theme_combo.setCurrentText('System') # Default to system
self.theme_combo.currentTextChanged.connect(self.on_theme_changed)
# State label # State label
self.state_label = QtWidgets.QLabel('State: Paused') self.state_label = QtWidgets.QLabel('State: Paused')
@@ -2258,7 +2389,7 @@ class MainWindow(QtWidgets.QMainWindow):
self.performance_label = QtWidgets.QLabel('Performance: --') self.performance_label = QtWidgets.QLabel('Performance: --')
resource_layout.addWidget(self.performance_label, 2, 0, 1, 3) resource_layout.addWidget(self.performance_label, 2, 0, 1, 3)
# Controls row (toggle button and update interval) - placed in a separate row # Controls row (toggle button, update interval, and theme selector)
self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources') self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources')
self.toggle_resources_btn.setMaximumWidth(100) self.toggle_resources_btn.setMaximumWidth(100)
self.toggle_resources_btn.setToolTip('Toggle resource usage display. Install psutil for detailed system metrics.') self.toggle_resources_btn.setToolTip('Toggle resource usage display. Install psutil for detailed system metrics.')
@@ -2317,9 +2448,14 @@ class MainWindow(QtWidgets.QMainWindow):
start_state_layout = QtWidgets.QHBoxLayout() start_state_layout = QtWidgets.QHBoxLayout()
start_state_layout.setSpacing(6) # Fixed gap between elements start_state_layout.setSpacing(6) # Fixed gap between elements
start_state_layout.addWidget(self.btn_start_stop) start_state_layout.addWidget(self.btn_start_stop)
start_state_layout.addWidget(self.btn_theme)
start_state_layout.addWidget(self.state_label) start_state_layout.addWidget(self.state_label)
start_state_layout.addStretch(1) start_state_layout.addStretch(1) # This pushes theme controls to the right
# Theme controls on the right side
self.theme_label = QtWidgets.QLabel('Theme:')
start_state_layout.addWidget(self.theme_label)
start_state_layout.addWidget(self.theme_combo)
start_state_group.setLayout(start_state_layout) start_state_group.setLayout(start_state_layout)
layout.addWidget(start_state_group, 6, 0, 1, 5) layout.addWidget(start_state_group, 6, 0, 1, 5)
layout.setRowStretch(6, 0) # Prevent vertical stretch layout.setRowStretch(6, 0) # Prevent vertical stretch
@@ -2329,9 +2465,12 @@ class MainWindow(QtWidgets.QMainWindow):
central.setLayout(layout) central.setLayout(layout)
self.setCentralWidget(central) self.setCentralWidget(central)
# Apply initial theme and set button text # Apply initial theme and set combo box text
self.apply_theme() self.apply_theme()
self._update_theme_button_text() self._update_theme_combo_text()
# Apply title bar theming after window is fully initialized
QtCore.QTimer.singleShot(100, lambda: self.apply_dark_title_bar(self.get_effective_dark_mode()))
def _log_btn_refresh_windows(self): def _log_btn_refresh_windows(self):
logger.info("UI: Refresh Windows button pressed") logger.info("UI: Refresh Windows button pressed")