Refactor title bar theming logic; introduce helper methods for consistent application across windows and dialogs

This commit is contained in:
2025-07-31 16:05:51 +02:00
parent 21dd3e7ba4
commit cb79b5d6e1
+139 -63
View File
@@ -2468,6 +2468,13 @@ class MainWindow(QtWidgets.QMainWindow):
Apply dark mode to the window title bar. Apply dark mode to the window title bar.
Works on Windows 10/11, macOS, and some Linux desktop environments. Works on Windows 10/11, macOS, and some Linux desktop environments.
""" """
return self._apply_title_bar_theme_to_window(self, enable_dark)
def _apply_title_bar_theme_to_window(self, window, enable_dark=True):
"""
Apply title bar theme to a specific window (can be main window or dialog).
This is a helper method that can be used for any QWidget.
"""
try: try:
import platform import platform
system = platform.system().lower() system = platform.system().lower()
@@ -2479,7 +2486,7 @@ class MainWindow(QtWidgets.QMainWindow):
from ctypes import wintypes from ctypes import wintypes
# Get window handle # Get window handle
hwnd = int(self.winId()) hwnd = int(window.winId())
# Define constants for Windows API # Define constants for Windows API
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
@@ -2504,11 +2511,11 @@ class MainWindow(QtWidgets.QMainWindow):
) )
if result == 0: # S_OK if result == 0: # S_OK
logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar ({'newer' if use_newer_api else 'legacy'} API)") logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar to {window.__class__.__name__}")
# Force immediate window redraw # Force immediate window redraw
self.update() window.update()
self.repaint() window.repaint()
return True return True
else: else:
@@ -2527,9 +2534,9 @@ class MainWindow(QtWidgets.QMainWindow):
ctypes.sizeof(ctypes.c_int) ctypes.sizeof(ctypes.c_int)
) )
if result == 0: if result == 0:
logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar (fallback API)") logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar to {window.__class__.__name__} (fallback API)")
self.update() window.update()
self.repaint() window.repaint()
return True return True
except Exception as fallback_e: except Exception as fallback_e:
@@ -2590,33 +2597,24 @@ class MainWindow(QtWidgets.QMainWindow):
# Apply title bar theming immediately # Apply title bar theming immediately
self.apply_dark_title_bar(is_dark) self.apply_dark_title_bar(is_dark)
# Also apply to any open dialogs immediately # Apply to ALL widgets in the application (including dialogs that aren't currently visible)
for dialog in QtWidgets.QApplication.allWidgets(): for widget in QtWidgets.QApplication.allWidgets():
if isinstance(dialog, QtWidgets.QDialog) and dialog.isVisible(): if isinstance(widget, (QtWidgets.QDialog, QtWidgets.QMainWindow)):
try: try:
hwnd = int(dialog.winId()) # Apply title bar theme to each window/dialog
if hwnd: self._apply_title_bar_theme_to_window(widget, is_dark)
import ctypes
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
# Try both API versions # If it's a dialog, also force its stylesheet update
for attribute in [DWMWA_USE_IMMERSIVE_DARK_MODE, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1]: if isinstance(widget, QtWidgets.QDialog) and hasattr(widget, 'parent_window'):
try: if widget.parent_window == self: # Only update dialogs that belong to this main window
result = ctypes.windll.dwmapi.DwmSetWindowAttribute( widget.setStyleSheet(self._get_dialog_stylesheet())
hwnd, widget.update()
attribute, widget.repaint()
ctypes.byref(ctypes.c_int(1 if is_dark else 0)), except Exception as e:
ctypes.sizeof(ctypes.c_int) logger.debug(f"Failed to apply theme to {widget.__class__.__name__}: {e}")
)
if result == 0: # Store the current theme state globally so new windows can pick it up
dialog.update() QtWidgets.QApplication.instance().setProperty('dark_theme_enabled', is_dark)
dialog.repaint()
break
except:
continue
except:
pass
# Main window style # Main window style
main_style = f""" main_style = f"""
@@ -2732,6 +2730,91 @@ class MainWindow(QtWidgets.QMainWindow):
self.gpu_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') self.gpu_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};')
self.performance_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') self.performance_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};')
def _get_dialog_stylesheet(self):
"""
Get the stylesheet for dialogs to match the main window theme.
"""
colors = self.get_theme_styles()
return f"""
QDialog {{
background-color: {colors['background']};
color: {colors['foreground']};
}}
QGroupBox {{
font-size: 9pt;
font-weight: bold;
border: 1px solid {colors['border']};
border-radius: 5px;
margin-top: 10px;
padding-top: 5px;
color: {colors['foreground']};
}}
QGroupBox::title {{
subcontrol-origin: margin;
left: 10px;
padding: 0 5px 0 5px;
color: {colors['foreground']};
}}
QLabel {{
font-size: 9pt;
color: {colors['foreground']};
}}
QComboBox {{
font-size: 9pt;
min-height: 20px;
padding: 2px 6px;
min-width: 90px;
background-color: {colors['input_bg']};
border: 1px solid {colors['border']};
border-radius: 3px;
color: {colors['foreground']};
}}
QComboBox::drop-down {{
border: none;
width: 20px;
subcontrol-origin: padding;
subcontrol-position: center right;
}}
QComboBox::down-arrow {{
image: none;
width: 0px;
height: 0px;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid {colors['foreground']};
margin: 0px;
padding: 0px;
}}
QComboBox::down-arrow:hover {{
border-top-color: {colors['accent']};
}}
QComboBox QAbstractItemView {{
background-color: {colors['input_bg']};
border: 1px solid {colors['border']};
border-radius: 3px;
color: {colors['foreground']};
selection-background-color: {colors['accent']};
selection-color: white;
}}
QPushButton {{
font-size: 9pt;
min-height: 20px;
padding: 2px 4px;
min-width: 70px;
background-color: {colors['button_bg']};
border: 1px solid {colors['border']};
border-radius: 3px;
color: {colors['foreground']};
}}
QPushButton:hover {{
background-color: {colors['button_hover']};
}}
QPushButton:pressed {{
background-color: {colors['border']};
}}
"""
def on_theme_changed(self, theme_text): def on_theme_changed(self, theme_text):
""" """
Handle theme dropdown selection change. Handle theme dropdown selection change.
@@ -2845,30 +2928,9 @@ class MainWindow(QtWidgets.QMainWindow):
try: try:
dialog = SettingsDialog(self) dialog = SettingsDialog(self)
# Apply title bar theming to the dialog # Apply title bar theming to the dialog immediately
is_dark = self.get_effective_dark_mode() is_dark = self.get_effective_dark_mode()
try: self._apply_title_bar_theme_to_window(dialog, is_dark)
hwnd = int(dialog.winId())
if hwnd:
import ctypes
DWMWA_USE_IMMERSIVE_DARK_MODE = 20
DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19
# Try both API versions
for attribute in [DWMWA_USE_IMMERSIVE_DARK_MODE, DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1]:
try:
result = ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd,
attribute,
ctypes.byref(ctypes.c_int(1 if is_dark else 0)),
ctypes.sizeof(ctypes.c_int)
)
if result == 0: # S_OK
break
except:
continue
except:
pass # Ignore title bar theming errors
dialog.exec() dialog.exec()
@@ -2879,14 +2941,28 @@ class MainWindow(QtWidgets.QMainWindow):
""" """
Update the theme combo box text based on current theme mode and system state. Update the theme combo box text based on current theme mode and system state.
""" """
if self.theme_mode == 'system': # Temporarily disconnect the signal to prevent triggering theme changes
system_dark = self.is_system_dark_theme() self.theme_combo.currentTextChanged.disconnect()
display_text = f"System ({'Dark' if system_dark else 'Light'})"
# Update the first item's text to show current system theme try:
self.theme_combo.setItemText(0, display_text) if self.theme_mode == 'system':
else: system_dark = self.is_system_dark_theme()
# Reset system item text display_text = f"System ({'Dark' if system_dark else 'Light'})"
self.theme_combo.setItemText(0, 'System') # Update the first item's text to show current system theme
self.theme_combo.setItemText(0, display_text)
# Ensure the combo shows the correct selection
self.theme_combo.setCurrentIndex(0)
else:
# Reset system item text
self.theme_combo.setItemText(0, 'System')
# Set the correct selection based on theme mode
if self.theme_mode == 'light':
self.theme_combo.setCurrentIndex(1)
elif self.theme_mode == 'dark':
self.theme_combo.setCurrentIndex(2)
finally:
# Reconnect the signal
self.theme_combo.currentTextChanged.connect(self.on_theme_changed)
def init_ui(self): def init_ui(self):
""" """