Refactor title bar theming logic; introduce helper methods for consistent application across windows and dialogs
This commit is contained in:
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
# Temporarily disconnect the signal to prevent triggering theme changes
|
||||||
|
self.theme_combo.currentTextChanged.disconnect()
|
||||||
|
|
||||||
|
try:
|
||||||
if self.theme_mode == 'system':
|
if self.theme_mode == 'system':
|
||||||
system_dark = self.is_system_dark_theme()
|
system_dark = self.is_system_dark_theme()
|
||||||
display_text = f"System ({'Dark' if system_dark else 'Light'})"
|
display_text = f"System ({'Dark' if system_dark else 'Light'})"
|
||||||
# Update the first item's text to show current system theme
|
# Update the first item's text to show current system theme
|
||||||
self.theme_combo.setItemText(0, display_text)
|
self.theme_combo.setItemText(0, display_text)
|
||||||
|
# Ensure the combo shows the correct selection
|
||||||
|
self.theme_combo.setCurrentIndex(0)
|
||||||
else:
|
else:
|
||||||
# Reset system item text
|
# Reset system item text
|
||||||
self.theme_combo.setItemText(0, 'System')
|
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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user