Enhance theme management with combo box for selection; implement dark title bar theming across platforms
This commit is contained in:
@@ -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,46 +2195,49 @@ 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):
|
||||||
"""
|
"""
|
||||||
Initializes the main UI components and layouts.
|
Initializes the main UI components and layouts.
|
||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user