From 7aeaae8880aebb5a6f5a24043a215e6bccf902c8 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 14:50:21 +0200 Subject: [PATCH 01/17] Add theme support with dynamic monitoring and UI updates --- main.py | 398 ++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 360 insertions(+), 38 deletions(-) diff --git a/main.py b/main.py index ef11459..11fef7e 100644 --- a/main.py +++ b/main.py @@ -1283,6 +1283,9 @@ class MainWindow(QtWidgets.QMainWindow): self.selected_step_idx = None self.last_toggle_time = 0 # For debounce of start/stop + # Theme support + self.theme_mode = 'system' # 'light', 'dark', or 'system' + # Memory optimization attributes self._last_screenshot = None self._last_screenshot_time = 0 @@ -1314,6 +1317,40 @@ class MainWindow(QtWidgets.QMainWindow): # Initial resource display update (delayed to prevent startup hanging) QtCore.QTimer.singleShot(2000, self._enable_resource_monitoring) + + # Setup system theme monitoring timer (only when in system mode) + self.theme_monitor_timer = QtCore.QTimer() + self.theme_monitor_timer.timeout.connect(self._check_system_theme_change) + self._last_system_theme = None + + # Start theme monitoring if in system mode + if self.theme_mode == 'system': + self.theme_monitor_timer.start(5000) # Check every 5 seconds + + def _check_system_theme_change(self): + """ + Check if the system theme has changed and update accordingly. + Only runs when theme_mode is 'system'. + """ + if self.theme_mode != 'system': + self.theme_monitor_timer.stop() + return + + try: + current_system_theme = self.is_system_dark_theme() + + if self._last_system_theme is None: + self._last_system_theme = current_system_theme + return + + if current_system_theme != self._last_system_theme: + logger.info(f'System theme changed to: {"Dark" if current_system_theme else "Light"}') + self._last_system_theme = current_system_theme + self.apply_theme() + self._update_theme_button_text() + + except Exception as e: + logger.debug(f"Error checking system theme change: {e}") def _enable_resource_monitoring(self): """Enable resource monitoring after startup delay.""" @@ -1406,16 +1443,18 @@ class MainWindow(QtWidgets.QMainWindow): def _update_resource_display(self, memory_info): """ - Update the resource usage display in the UI. + Update the resource usage display in the UI with theme-aware colors. """ try: + colors = self.get_theme_styles() + # Memory usage if memory_info.get('has_psutil', False): memory_text = f"Memory: {memory_info['process_memory_mb']:.1f}MB ({memory_info['process_memory_percent']:.1f}%)" - memory_color = '#d9534f' if memory_info['process_memory_percent'] > 10 else '#5cb85c' + memory_color = colors['error'] if memory_info['process_memory_percent'] > 10 else colors['success'] else: memory_text = "Memory: N/A (psutil needed)" - memory_color = '#f0ad4e' + memory_color = colors['warning'] self.memory_label.setText(memory_text) self.memory_label.setStyleSheet(f'font-size: 9pt; color: {memory_color};') @@ -1428,10 +1467,10 @@ class MainWindow(QtWidgets.QMainWindow): # CPU usage if memory_info.get('has_psutil', False): cpu_text = f"CPU: {memory_info['cpu_percent']:.1f}%" - cpu_color = '#d9534f' if memory_info['cpu_percent'] > 50 else '#5cb85c' + cpu_color = colors['error'] if memory_info['cpu_percent'] > 50 else colors['success'] else: cpu_text = "CPU: N/A" - cpu_color = '#777' + cpu_color = colors['text_light'] self.cpu_label.setText(cpu_text) self.cpu_label.setStyleSheet(f'font-size: 9pt; color: {cpu_color};') @@ -1439,17 +1478,17 @@ class MainWindow(QtWidgets.QMainWindow): # System memory if memory_info.get('has_psutil', False): system_text = f"System: {memory_info['system_memory_percent']:.1f}% ({memory_info['system_memory_available_gb']:.1f}GB free)" - system_color = '#d9534f' if memory_info['system_memory_percent'] > 85 else '#5cb85c' + system_color = colors['error'] if memory_info['system_memory_percent'] > 85 else colors['success'] else: system_text = "System: N/A" - system_color = '#777' + system_color = colors['text_light'] self.system_memory_label.setText(system_text) self.system_memory_label.setStyleSheet(f'font-size: 9pt; color: {system_color};') # Cache info cache_text = f"Cache: {memory_info['template_cache_size']} templates, {memory_info['cooldown_entries']} cooldowns" - cache_color = '#f0ad4e' if memory_info['template_cache_size'] > 20 else '#5bc0de' + cache_color = colors['warning'] if memory_info['template_cache_size'] > 20 else colors['info'] self.cache_label.setText(cache_text) self.cache_label.setStyleSheet(f'font-size: 9pt; color: {cache_color};') @@ -1464,23 +1503,23 @@ class MainWindow(QtWidgets.QMainWindow): if gpu_total_mb > 0: gpu_text = f"GPU: {gpu_used_mb:.0f}/{gpu_total_mb:.0f}MB ({gpu_utilization:.1f}%)" - gpu_color = '#d9534f' if gpu_utilization > 80 else '#f0ad4e' if gpu_utilization > 60 else '#5cb85c' + gpu_color = colors['error'] if gpu_utilization > 80 else colors['warning'] if gpu_utilization > 60 else colors['success'] # Add GPU name as tooltip gpu_name = memory_info.get('gpu_name', 'Unknown GPU') self.gpu_label.setToolTip(f"GPU: {gpu_name} (detected via {gpu_method})") else: gpu_text = f"GPU: Detected ({gpu_method})" - gpu_color = '#5bc0de' + gpu_color = colors['info'] gpu_name = memory_info.get('gpu_name', 'Unknown GPU') self.gpu_label.setToolTip(f"GPU: {gpu_name} (limited info via {gpu_method})") elif gpu_method == 'loading': gpu_text = "GPU: Loading..." - gpu_color = '#f0ad4e' + gpu_color = colors['warning'] self.gpu_label.setToolTip("GPU detection in progress...") else: gpu_text = "GPU: Not detected" - gpu_color = '#777' + gpu_color = colors['text_light'] self.gpu_label.setToolTip( "No GPU detected or GPU monitoring libraries not available.\n\n" "For enhanced GPU monitoring:\n" @@ -1507,10 +1546,10 @@ class MainWindow(QtWidgets.QMainWindow): if hasattr(self, '_loop_times') and self._loop_times: avg_time = sum(self._loop_times) / len(self._loop_times) perf_text = f"Performance: {avg_time*1000:.0f}ms avg loop time" - perf_color = '#d9534f' if avg_time > 0.5 else '#5cb85c' + perf_color = colors['error'] if avg_time > 0.5 else colors['success'] else: perf_text = "Performance: Not running" - perf_color = '#777' + perf_color = colors['text_light'] self.performance_label.setText(perf_text) self.performance_label.setStyleSheet(f'font-size: 9pt; color: {perf_color};') @@ -1518,22 +1557,32 @@ class MainWindow(QtWidgets.QMainWindow): # Show error if any if 'error' in memory_info: self.performance_label.setText(f"Error: {memory_info['error'][:50]}...") - self.performance_label.setStyleSheet('font-size: 9pt; color: #d9534f;') + self.performance_label.setStyleSheet(f'font-size: 9pt; color: {colors["error"]};') except Exception as e: logger.debug(f"Error updating resource display: {e}") - # Show basic fallback info + colors = self.get_theme_styles() + # Show basic fallback info with theme-aware colors + error_color = colors['error'] self.memory_label.setText("Memory: Error") + self.memory_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') self.cpu_label.setText("CPU: Error") + self.cpu_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') self.system_memory_label.setText("System: Error") + self.system_memory_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') self.cache_label.setText("Cache: Error") + self.cache_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') self.gpu_label.setText("GPU: Error") + self.gpu_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') self.performance_label.setText(f"Display Error: {str(e)[:30]}...") + self.performance_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') def stop_monitoring(self): - """Stop performance monitoring timer.""" + """Stop performance monitoring and theme monitoring timers.""" if hasattr(self, 'monitor_timer') and self.monitor_timer: self.monitor_timer.stop() + if hasattr(self, 'theme_monitor_timer') and self.theme_monitor_timer: + self.theme_monitor_timer.stop() def _toggle_resource_display(self): """Toggle the visibility of resource usage widgets.""" @@ -1782,26 +1831,296 @@ class MainWindow(QtWidgets.QMainWindow): logger.info("Resources cleaned up successfully") + def is_system_dark_theme(self): + """ + Detect if the system is using dark theme. + 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 mode detection + try: + import winreg + registry = winreg.ConnectRegistry(None, winreg.HKEY_CURRENT_USER) + key = winreg.OpenKey(registry, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize") + value, _ = winreg.QueryValueEx(key, "AppsUseLightTheme") + winreg.CloseKey(key) + return value == 0 # 0 = dark theme, 1 = light theme + except (ImportError, FileNotFoundError, OSError): + # Fallback: check if PyQt6 can detect system dark theme + try: + palette = QtWidgets.QApplication.palette() + # Check if the window background is darker than text color + bg_color = palette.color(QtGui.QPalette.ColorRole.Window) + text_color = palette.color(QtGui.QPalette.ColorRole.WindowText) + # Calculate luminance to determine if background is dark + bg_luminance = (0.299 * bg_color.red() + 0.587 * bg_color.green() + 0.114 * bg_color.blue()) / 255 + return bg_luminance < 0.5 + except: + pass + + elif system == 'darwin': # macOS + try: + import subprocess + result = subprocess.run(['defaults', 'read', '-g', 'AppleInterfaceStyle'], + capture_output=True, text=True, timeout=2) + return result.stdout.strip().lower() == 'dark' + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass + + elif system == 'linux': + # Try to detect dark theme on Linux (GNOME, KDE, etc.) + try: + import subprocess + import os + + # Try GNOME gsettings + try: + result = subprocess.run(['gsettings', 'get', 'org.gnome.desktop.interface', 'gtk-theme'], + capture_output=True, text=True, timeout=2) + theme_name = result.stdout.strip().strip("'").lower() + return any(dark_indicator in theme_name for dark_indicator in ['dark', 'adwaita-dark', 'breeze-dark']) + except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError): + pass + + # Try KDE configuration + kde_config_paths = [ + os.path.expanduser('~/.config/kdeglobals'), + os.path.expanduser('~/.kde/share/config/kdeglobals'), + os.path.expanduser('~/.kde4/share/config/kdeglobals') + ] + + for config_path in kde_config_paths: + try: + if os.path.exists(config_path): + with open(config_path, 'r') as f: + content = f.read().lower() + if 'colorscheme=breezedark' in content or 'dark' in content: + return True + except: + continue + + # Fallback: check environment variables + desktop_env = os.environ.get('XDG_CURRENT_DESKTOP', '').lower() + if 'dark' in desktop_env: + return True + + except: + pass + + except Exception as e: + logger.debug(f"System theme detection failed: {e}") + + # Default fallback: assume light theme + return False + + def get_effective_dark_mode(self): + """ + Get the effective dark mode state based on current theme setting. + """ + if self.theme_mode == 'dark': + return True + elif self.theme_mode == 'light': + return False + else: # system + return self.is_system_dark_theme() + + def get_theme_styles(self): + """ + Get the current theme styles (light or dark mode). + """ + is_dark = self.get_effective_dark_mode() + + if is_dark: + return { + 'background': '#2b2b2b', + 'foreground': '#ffffff', + 'button_bg': '#404040', + 'button_hover': '#505050', + 'input_bg': '#3c3c3c', + 'border': '#555555', + 'accent': '#0078d4', + 'success': '#107c10', + 'warning': '#ff8c00', + 'error': '#d13438', + 'info': '#0078d4', + 'text_light': '#cccccc', + 'text_dark': '#ffffff' + } + else: + return { + 'background': '#ffffff', + 'foreground': '#000000', + 'button_bg': '#f0f0f0', + 'button_hover': '#e0e0e0', + 'input_bg': '#ffffff', + 'border': '#cccccc', + 'accent': '#0055aa', + 'success': '#107c10', + 'warning': '#ff8c00', + 'error': '#d9534f', + 'info': '#0055aa', + 'text_light': '#333333', + 'text_dark': '#000000' + } + + def apply_theme(self): + """ + Apply the current theme to all UI elements. + """ + colors = self.get_theme_styles() + + # Main window style + main_style = f""" + QMainWindow {{ + background-color: {colors['background']}; + color: {colors['foreground']}; + }} + QPushButton {{ + font-size: 9pt; + min-height: 20px; + padding: 2px 6px; + 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']}; + }} + 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; + }} + QComboBox::down-arrow {{ + image: none; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid {colors['foreground']}; + margin: 0px 4px 0px 4px; + }} + QListWidget {{ + font-size: 9pt; + min-width: 200px; + min-height: 120px; + background-color: {colors['input_bg']}; + border: 1px solid {colors['border']}; + border-radius: 3px; + color: {colors['foreground']}; + }} + QListWidget::item {{ + padding: 4px; + border-bottom: 1px solid {colors['border']}; + }} + QListWidget::item:selected {{ + background-color: {colors['accent']}; + color: white; + }} + QLabel {{ + font-size: 9pt; + 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']}; + }} + QWidget {{ + color: {colors['foreground']}; + background-color: {colors['background']}; + }} + """ + + self.setStyleSheet(main_style) + + # Update individual labels with theme-appropriate colors + self.state_label.setStyleSheet(f'font-weight: bold; font-size: 11pt; color: {colors["accent"]};') + + # Update resource labels + self.memory_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') + self.cpu_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') + self.system_memory_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') + self.cache_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.interval_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') + + def _update_theme_button_text(self): + """ + Update the theme button text based on current theme mode. + """ + if self.theme_mode == 'system': + system_dark = self.is_system_dark_theme() + theme_text = f"System ({'Dark' if system_dark else 'Light'})" + elif self.theme_mode == 'light': + theme_text = "Light" + 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: + if hasattr(self, 'theme_monitor_timer'): + self.theme_monitor_timer.stop() + + self.apply_theme() + self._update_theme_button_text() + + logger.info(f'Theme changed to: {self.theme_mode} mode') + def init_ui(self): """ Initializes the main UI components and layouts. Sets up scenario and window dropdowns, step list, and action buttons. """ - # Set compact font and style + # Set compact font font = QtGui.QFont() font.setPointSize(9) self.setFont(font) - style = """ - QPushButton, QComboBox, QListWidget, QLabel { - font-size: 9pt; - min-height: 20px; - padding: 2px 6px; - } - QComboBox { min-width: 90px; } - QPushButton { min-width: 70px; } - QListWidget { min-width: 200px; min-height: 120px; } - """ - self.setStyleSheet(style) # Scenario group (QGroupBox with title 'Scenario') scenario_group_box = QtWidgets.QGroupBox('Scenario') @@ -1898,9 +2217,14 @@ class MainWindow(QtWidgets.QMainWindow): self.btn_start_stop.setMinimumWidth(80) self.btn_start_stop.clicked.connect(self._log_btn_start_stop) + # Theme toggle button + self.btn_theme = QtWidgets.QPushButton('System') + self.btn_theme.setMinimumWidth(80) + self.btn_theme.setToolTip('Cycle through theme modes: System (follows OS) -> Light -> Dark -> System...') + self.btn_theme.clicked.connect(self.toggle_theme) + # State label self.state_label = QtWidgets.QLabel('State: Paused') - self.state_label.setStyleSheet('font-weight: bold; font-size: 11pt; color: #0055aa;') self.state_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter) # Resource usage display @@ -1912,32 +2236,26 @@ class MainWindow(QtWidgets.QMainWindow): # Memory usage self.memory_label = QtWidgets.QLabel('Memory: --') - self.memory_label.setStyleSheet('font-size: 9pt; color: #333;') resource_layout.addWidget(self.memory_label, 0, 0) # CPU usage self.cpu_label = QtWidgets.QLabel('CPU: --') - self.cpu_label.setStyleSheet('font-size: 9pt; color: #333;') resource_layout.addWidget(self.cpu_label, 0, 1) # System memory self.system_memory_label = QtWidgets.QLabel('System: --') - self.system_memory_label.setStyleSheet('font-size: 9pt; color: #333;') resource_layout.addWidget(self.system_memory_label, 1, 0) # Cache info self.cache_label = QtWidgets.QLabel('Cache: --') - self.cache_label.setStyleSheet('font-size: 9pt; color: #333;') resource_layout.addWidget(self.cache_label, 1, 1) # GPU memory info self.gpu_label = QtWidgets.QLabel('GPU: --') - self.gpu_label.setStyleSheet('font-size: 9pt; color: #333;') resource_layout.addWidget(self.gpu_label, 1, 2) # Performance info self.performance_label = QtWidgets.QLabel('Performance: --') - self.performance_label.setStyleSheet('font-size: 9pt; color: #333;') resource_layout.addWidget(self.performance_label, 2, 0, 1, 3) # Controls row (toggle button and update interval) - placed in a separate row @@ -1949,7 +2267,6 @@ class MainWindow(QtWidgets.QMainWindow): # Update interval controls self.interval_label = QtWidgets.QLabel('Update:') - self.interval_label.setStyleSheet('font-size: 9pt; color: #333;') self.update_interval_combo = QtWidgets.QComboBox() self.update_interval_combo.setMaximumWidth(80) self.update_interval_combo.setToolTip('Set resource monitoring update interval') @@ -1998,8 +2315,9 @@ class MainWindow(QtWidgets.QMainWindow): start_state_group.setTitle("") start_state_group.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) start_state_layout = QtWidgets.QHBoxLayout() - start_state_layout.setSpacing(6) # Fixed gap between button and label + start_state_layout.setSpacing(6) # Fixed gap between elements 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.addStretch(1) start_state_group.setLayout(start_state_layout) @@ -2010,6 +2328,10 @@ class MainWindow(QtWidgets.QMainWindow): central = QtWidgets.QWidget() central.setLayout(layout) self.setCentralWidget(central) + + # Apply initial theme and set button text + self.apply_theme() + self._update_theme_button_text() def _log_btn_refresh_windows(self): logger.info("UI: Refresh Windows button pressed") From 2a6940ea3ed3bef489a4acb178f87d035ba02d33 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 14:59:09 +0200 Subject: [PATCH 02/17] Enhance theme management with combo box for selection; implement dark title bar theming across platforms --- main.py | 233 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 186 insertions(+), 47 deletions(-) diff --git a/main.py b/main.py index 11fef7e..4cc2726 100644 --- a/main.py +++ b/main.py @@ -1347,7 +1347,7 @@ class MainWindow(QtWidgets.QMainWindow): logger.info(f'System theme changed to: {"Dark" if current_system_theme else "Light"}') self._last_system_theme = current_system_theme self.apply_theme() - self._update_theme_button_text() + self._update_theme_combo_text() except Exception as 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 + 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): """Handle window state changes (maximize/minimize).""" super().changeEvent(event) @@ -1967,11 +1973,128 @@ class MainWindow(QtWidgets.QMainWindow): '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): """ - Apply the current theme to all UI elements. + Apply the current theme to all UI elements including title bar. """ 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_style = f""" @@ -2072,46 +2195,49 @@ class MainWindow(QtWidgets.QMainWindow): self.performance_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': system_dark = self.is_system_dark_theme() - theme_text = f"System ({'Dark' if system_dark else 'Light'})" - elif self.theme_mode == 'light': - theme_text = "Light" - 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 + display_text = f"System ({'Dark' if system_dark else 'Light'})" + # Update the first item's text to show current system theme + self.theme_combo.setItemText(0, display_text) else: - if hasattr(self, 'theme_monitor_timer'): - self.theme_monitor_timer.stop() - - self.apply_theme() - self._update_theme_button_text() - - logger.info(f'Theme changed to: {self.theme_mode} mode') - + # Reset system item text + self.theme_combo.setItemText(0, 'System') + def init_ui(self): """ 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.clicked.connect(self._log_btn_start_stop) - # Theme toggle button - self.btn_theme = QtWidgets.QPushButton('System') - self.btn_theme.setMinimumWidth(80) - self.btn_theme.setToolTip('Cycle through theme modes: System (follows OS) -> Light -> Dark -> System...') - self.btn_theme.clicked.connect(self.toggle_theme) + # Theme dropdown + self.theme_combo = QtWidgets.QComboBox() + self.theme_combo.setMinimumWidth(100) + self.theme_combo.setMaximumWidth(120) + 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 self.state_label = QtWidgets.QLabel('State: Paused') @@ -2258,7 +2389,7 @@ class MainWindow(QtWidgets.QMainWindow): self.performance_label = QtWidgets.QLabel('Performance: --') 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.setMaximumWidth(100) 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.setSpacing(6) # Fixed gap between elements 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.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) layout.addWidget(start_state_group, 6, 0, 1, 5) layout.setRowStretch(6, 0) # Prevent vertical stretch @@ -2329,9 +2465,12 @@ class MainWindow(QtWidgets.QMainWindow): central.setLayout(layout) self.setCentralWidget(central) - # Apply initial theme and set button text + # Apply initial theme and set combo box text 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): logger.info("UI: Refresh Windows button pressed") From 6309edaeeaf3af761abe4a21ca23b87dff5fcfd3 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:07:11 +0200 Subject: [PATCH 03/17] Implement resource usage tracking and statistics display; enhance theme application timing and UI controls --- main.py | 212 +++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 158 insertions(+), 54 deletions(-) diff --git a/main.py b/main.py index 4cc2726..55c90b6 100644 --- a/main.py +++ b/main.py @@ -1299,12 +1299,26 @@ class MainWindow(QtWidgets.QMainWindow): self._last_warning_messages = {} # Track last warning messages to prevent duplicates self._warning_cooldown = 30 # Seconds between duplicate warnings + # Resource usage tracking for max/average values + self._resource_history = { + 'memory_mb': [], + 'memory_percent': [], + 'cpu_percent': [], + 'system_memory_percent': [], + 'gpu_utilization': [], + 'loop_times': [] + } + self._max_resource_samples = 50 # Keep last 50 samples for statistics + self.init_ui() self.load_scenarios() # Restore window geometry (size and position) self._restore_window_geometry() + # Apply theme after restoration (delayed to ensure window is ready) + QtCore.QTimer.singleShot(100, self._apply_initial_theme) + # Setup cleanup on close self.setAttribute(QtCore.Qt.WidgetAttribute.WA_DeleteOnClose) @@ -1318,14 +1332,10 @@ class MainWindow(QtWidgets.QMainWindow): # Initial resource display update (delayed to prevent startup hanging) QtCore.QTimer.singleShot(2000, self._enable_resource_monitoring) - # Setup system theme monitoring timer (only when in system mode) + # Setup system theme monitoring timer (will be started by _apply_initial_theme if needed) self.theme_monitor_timer = QtCore.QTimer() self.theme_monitor_timer.timeout.connect(self._check_system_theme_change) self._last_system_theme = None - - # Start theme monitoring if in system mode - if self.theme_mode == 'system': - self.theme_monitor_timer.start(5000) # Check every 5 seconds def _check_system_theme_change(self): """ @@ -1352,6 +1362,63 @@ class MainWindow(QtWidgets.QMainWindow): except Exception as e: logger.debug(f"Error checking system theme change: {e}") + def _update_resource_history(self, memory_info): + """ + Update resource usage history for calculating max/average values. + """ + try: + # Add current values to history + if memory_info.get('has_psutil', False): + self._resource_history['memory_mb'].append(memory_info['process_memory_mb']) + self._resource_history['memory_percent'].append(memory_info['process_memory_percent']) + self._resource_history['cpu_percent'].append(memory_info['cpu_percent']) + self._resource_history['system_memory_percent'].append(memory_info['system_memory_percent']) + + if memory_info.get('gpu_has_gpu', False): + self._resource_history['gpu_utilization'].append(memory_info['gpu_utilization_percent']) + + # Add loop time if available + if hasattr(self, '_loop_times') and self._loop_times: + avg_loop_time = sum(self._loop_times) / len(self._loop_times) + self._resource_history['loop_times'].append(avg_loop_time * 1000) # Convert to ms + + # Keep only the last N samples for each metric + for key in self._resource_history: + if len(self._resource_history[key]) > self._max_resource_samples: + self._resource_history[key] = self._resource_history[key][-self._max_resource_samples:] + + except Exception as e: + logger.debug(f"Error updating resource history: {e}") + + def _get_resource_stats(self, values_list): + """ + Calculate current, max, and average values from a list of values. + Returns (current, max, avg) or (0, 0, 0) if list is empty. + """ + if not values_list: + return 0, 0, 0 + + current = values_list[-1] if values_list else 0 + max_val = max(values_list) + avg_val = sum(values_list) / len(values_list) + + return current, max_val, avg_val + + def _apply_initial_theme(self): + """ + Apply the initial theme and start theme monitoring after UI is ready. + """ + # Apply the theme (includes title bar theming) + self.apply_theme() + self._update_theme_combo_text() + + # Start theme monitoring if in system mode + if self.theme_mode == 'system': + self._last_system_theme = self.is_system_dark_theme() + self.theme_monitor_timer.start(5000) # Check every 5 seconds + + logger.debug(f"Applied initial theme: {self.theme_mode}") + def _enable_resource_monitoring(self): """Enable resource monitoring after startup delay.""" if not hasattr(self, 'resource_monitoring_enabled') or not self.resource_monitoring_enabled: @@ -1443,17 +1510,23 @@ class MainWindow(QtWidgets.QMainWindow): def _update_resource_display(self, memory_info): """ - Update the resource usage display in the UI with theme-aware colors. + Update the resource usage display in the UI with theme-aware colors and max/average statistics. """ try: colors = self.get_theme_styles() - # Memory usage + # Update resource history for statistics + self._update_resource_history(memory_info) + + # Memory usage with statistics if memory_info.get('has_psutil', False): - memory_text = f"Memory: {memory_info['process_memory_mb']:.1f}MB ({memory_info['process_memory_percent']:.1f}%)" - memory_color = colors['error'] if memory_info['process_memory_percent'] > 10 else colors['success'] + current_mb, max_mb, avg_mb = self._get_resource_stats(self._resource_history['memory_mb']) + current_pct, max_pct, avg_pct = self._get_resource_stats(self._resource_history['memory_percent']) + + memory_text = f"Memory: {current_mb:.1f}MB ({current_pct:.1f}%)\nMax: {max_mb:.1f}MB ({max_pct:.1f}%) | Avg: {avg_mb:.1f}MB ({avg_pct:.1f}%)" + memory_color = colors['error'] if current_pct > 10 else colors['success'] else: - memory_text = "Memory: N/A (psutil needed)" + memory_text = "Memory: N/A (psutil needed)\nInstall psutil for detailed monitoring" memory_color = colors['warning'] self.memory_label.setText(memory_text) @@ -1464,36 +1537,40 @@ class MainWindow(QtWidgets.QMainWindow): self.memory_label.mousePressEvent = lambda event: self._show_psutil_info() self.memory_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.PointingHandCursor)) - # CPU usage + # CPU usage with statistics if memory_info.get('has_psutil', False): - cpu_text = f"CPU: {memory_info['cpu_percent']:.1f}%" - cpu_color = colors['error'] if memory_info['cpu_percent'] > 50 else colors['success'] + current_cpu, max_cpu, avg_cpu = self._get_resource_stats(self._resource_history['cpu_percent']) + + cpu_text = f"CPU: {current_cpu:.1f}%\nMax: {max_cpu:.1f}% | Avg: {avg_cpu:.1f}%" + cpu_color = colors['error'] if current_cpu > 50 else colors['success'] else: - cpu_text = "CPU: N/A" + cpu_text = "CPU: N/A\nInstall psutil for monitoring" cpu_color = colors['text_light'] self.cpu_label.setText(cpu_text) self.cpu_label.setStyleSheet(f'font-size: 9pt; color: {cpu_color};') - # System memory + # System memory with statistics if memory_info.get('has_psutil', False): - system_text = f"System: {memory_info['system_memory_percent']:.1f}% ({memory_info['system_memory_available_gb']:.1f}GB free)" - system_color = colors['error'] if memory_info['system_memory_percent'] > 85 else colors['success'] + current_sys, max_sys, avg_sys = self._get_resource_stats(self._resource_history['system_memory_percent']) + + system_text = f"System: {current_sys:.1f}% ({memory_info['system_memory_available_gb']:.1f}GB free)\nMax: {max_sys:.1f}% | Avg: {avg_sys:.1f}%" + system_color = colors['error'] if current_sys > 85 else colors['success'] else: - system_text = "System: N/A" + system_text = "System: N/A\nInstall psutil for monitoring" system_color = colors['text_light'] self.system_memory_label.setText(system_text) self.system_memory_label.setStyleSheet(f'font-size: 9pt; color: {system_color};') - # Cache info - cache_text = f"Cache: {memory_info['template_cache_size']} templates, {memory_info['cooldown_entries']} cooldowns" + # Cache info (enhanced display) + cache_text = f"Cache: {memory_info['template_cache_size']} templates\n{memory_info['cooldown_entries']} cooldowns" cache_color = colors['warning'] if memory_info['template_cache_size'] > 20 else colors['info'] self.cache_label.setText(cache_text) self.cache_label.setStyleSheet(f'font-size: 9pt; color: {cache_color};') - # GPU memory info + # GPU memory info with statistics gpu_method = memory_info.get('gpu_method', 'unknown') if memory_info.get('gpu_has_gpu', False): @@ -1501,24 +1578,27 @@ class MainWindow(QtWidgets.QMainWindow): gpu_total_mb = memory_info.get('gpu_total_mb', 0) gpu_utilization = memory_info.get('gpu_utilization_percent', 0) + # Get GPU statistics + current_gpu, max_gpu, avg_gpu = self._get_resource_stats(self._resource_history['gpu_utilization']) + if gpu_total_mb > 0: - gpu_text = f"GPU: {gpu_used_mb:.0f}/{gpu_total_mb:.0f}MB ({gpu_utilization:.1f}%)" - gpu_color = colors['error'] if gpu_utilization > 80 else colors['warning'] if gpu_utilization > 60 else colors['success'] + gpu_text = f"GPU: {gpu_used_mb:.0f}/{gpu_total_mb:.0f}MB ({current_gpu:.1f}%)\nMax: {max_gpu:.1f}% | Avg: {avg_gpu:.1f}%" + gpu_color = colors['error'] if current_gpu > 80 else colors['warning'] if current_gpu > 60 else colors['success'] # Add GPU name as tooltip gpu_name = memory_info.get('gpu_name', 'Unknown GPU') self.gpu_label.setToolTip(f"GPU: {gpu_name} (detected via {gpu_method})") else: - gpu_text = f"GPU: Detected ({gpu_method})" + gpu_text = f"GPU: Detected ({gpu_method})\nUtilization: {current_gpu:.1f}%" gpu_color = colors['info'] gpu_name = memory_info.get('gpu_name', 'Unknown GPU') self.gpu_label.setToolTip(f"GPU: {gpu_name} (limited info via {gpu_method})") elif gpu_method == 'loading': - gpu_text = "GPU: Loading..." + gpu_text = "GPU: Loading...\nDetection in progress" gpu_color = colors['warning'] self.gpu_label.setToolTip("GPU detection in progress...") else: - gpu_text = "GPU: Not detected" + gpu_text = "GPU: Not detected\nClick for setup info" gpu_color = colors['text_light'] self.gpu_label.setToolTip( "No GPU detected or GPU monitoring libraries not available.\n\n" @@ -1542,13 +1622,14 @@ class MainWindow(QtWidgets.QMainWindow): self.gpu_label.mousePressEvent = None self.gpu_label.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor)) - # Performance info + # Performance info with statistics if hasattr(self, '_loop_times') and self._loop_times: - avg_time = sum(self._loop_times) / len(self._loop_times) - perf_text = f"Performance: {avg_time*1000:.0f}ms avg loop time" - perf_color = colors['error'] if avg_time > 0.5 else colors['success'] + current_loop, max_loop, avg_loop = self._get_resource_stats(self._resource_history['loop_times']) + + perf_text = f"Performance: {current_loop:.0f}ms loop time\nMax: {max_loop:.0f}ms | Avg: {avg_loop:.0f}ms" + perf_color = colors['error'] if current_loop > 500 else colors['success'] else: - perf_text = "Performance: Not running" + perf_text = "Performance: Not running\nStart automation to see metrics" perf_color = colors['text_light'] self.performance_label.setText(perf_text) @@ -1556,7 +1637,7 @@ class MainWindow(QtWidgets.QMainWindow): # Show error if any if 'error' in memory_info: - self.performance_label.setText(f"Error: {memory_info['error'][:50]}...") + self.performance_label.setText(f"Error: {memory_info['error'][:30]}...\nCheck logs for details") self.performance_label.setStyleSheet(f'font-size: 9pt; color: {colors["error"]};') except Exception as e: @@ -1564,17 +1645,17 @@ class MainWindow(QtWidgets.QMainWindow): colors = self.get_theme_styles() # Show basic fallback info with theme-aware colors error_color = colors['error'] - self.memory_label.setText("Memory: Error") + self.memory_label.setText("Memory: Error\nCheck logs for details") self.memory_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') - self.cpu_label.setText("CPU: Error") + self.cpu_label.setText("CPU: Error\nCheck logs for details") self.cpu_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') - self.system_memory_label.setText("System: Error") + self.system_memory_label.setText("System: Error\nCheck logs for details") self.system_memory_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') - self.cache_label.setText("Cache: Error") + self.cache_label.setText("Cache: Error\nCheck logs for details") self.cache_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') - self.gpu_label.setText("GPU: Error") + self.gpu_label.setText("GPU: Error\nCheck logs for details") self.gpu_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') - self.performance_label.setText(f"Display Error: {str(e)[:30]}...") + self.performance_label.setText(f"Display Error: {str(e)[:20]}...\nResource monitoring failed") self.performance_label.setStyleSheet(f'font-size: 9pt; color: {error_color};') def stop_monitoring(self): @@ -1594,7 +1675,6 @@ class MainWindow(QtWidgets.QMainWindow): self.cache_label.hide() self.gpu_label.hide() self.performance_label.hide() - self.interval_label.hide() self.update_interval_combo.hide() self.toggle_resources_btn.setText('Show Resources') self.resource_widgets_visible = False @@ -1606,7 +1686,6 @@ class MainWindow(QtWidgets.QMainWindow): self.cache_label.show() self.gpu_label.show() self.performance_label.show() - self.interval_label.show() self.update_interval_combo.show() self.toggle_resources_btn.setText('Hide Resources') self.resource_widgets_visible = True @@ -1698,6 +1777,7 @@ class MainWindow(QtWidgets.QMainWindow): # Update with window geometry and settings existing_config['main_window'] = geometry_data existing_config['update_interval'] = self.update_interval_combo.currentData() + existing_config['theme_mode'] = self.theme_mode with open(config_path, 'w') as f: json.dump(existing_config, f, indent=2) @@ -1758,6 +1838,16 @@ class MainWindow(QtWidgets.QMainWindow): break logger.debug(f"Restored update interval: {saved_interval}ms") + # Restore theme mode if saved + saved_theme = config.get('theme_mode') + if saved_theme and saved_theme in ['system', 'light', 'dark']: + self.theme_mode = saved_theme + # Update the combo box to match the restored theme + theme_display_map = {'system': 'System', 'light': 'Light', 'dark': 'Dark'} + theme_display = theme_display_map.get(saved_theme, 'System') + self.theme_combo.setCurrentText(theme_display) + logger.debug(f"Restored theme mode: {saved_theme}") + logger.debug(f"Restored window geometry: x={x}, y={y}, w={width}, h={height}") except (json.JSONDecodeError, IOError, KeyError) as e: @@ -1831,6 +1921,9 @@ class MainWindow(QtWidgets.QMainWindow): self._loop_times.clear() if hasattr(self, '_step_cooldown'): self._step_cooldown.clear() + if hasattr(self, '_resource_history'): + for key in self._resource_history: + self._resource_history[key].clear() # Force garbage collection gc.collect() @@ -2093,8 +2186,8 @@ class MainWindow(QtWidgets.QMainWindow): 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) + # Apply dark title bar if in dark mode (with small delay to ensure window is ready) + QtCore.QTimer.singleShot(10, lambda: self.apply_dark_title_bar(is_dark)) # Main window style main_style = f""" @@ -2193,7 +2286,6 @@ class MainWindow(QtWidgets.QMainWindow): self.cache_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.interval_label.setStyleSheet(f'font-size: 9pt; color: {colors["text_light"]};') def on_theme_changed(self, theme_text): """ @@ -2223,6 +2315,9 @@ class MainWindow(QtWidgets.QMainWindow): self.apply_theme() self._update_theme_combo_text() + # Save the theme preference immediately + self._save_window_geometry() + logger.info(f'Theme changed to: {self.theme_mode} mode') def _update_theme_combo_text(self): @@ -2389,15 +2484,13 @@ class MainWindow(QtWidgets.QMainWindow): self.performance_label = QtWidgets.QLabel('Performance: --') resource_layout.addWidget(self.performance_label, 2, 0, 1, 3) - # Controls row (toggle button, update interval, and theme selector) - self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources') - 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.clicked.connect(self._toggle_resource_display) - resource_layout.addWidget(self.toggle_resources_btn, 3, 0) - - # Update interval controls - self.interval_label = QtWidgets.QLabel('Update:') + # Controls row - Update interval group + interval_group = QtWidgets.QGroupBox('Update Interval') + interval_group.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) + interval_layout = QtWidgets.QHBoxLayout() + interval_layout.setContentsMargins(4, 4, 4, 4) + interval_layout.setSpacing(4) + self.update_interval_combo = QtWidgets.QComboBox() self.update_interval_combo.setMaximumWidth(80) self.update_interval_combo.setToolTip('Set resource monitoring update interval') @@ -2419,8 +2512,19 @@ class MainWindow(QtWidgets.QMainWindow): self.update_interval_combo.setCurrentIndex(2) self.update_interval_combo.currentIndexChanged.connect(self._on_update_interval_changed) - resource_layout.addWidget(self.interval_label, 3, 1) - resource_layout.addWidget(self.update_interval_combo, 3, 2) + interval_layout.addWidget(self.update_interval_combo) + interval_layout.addStretch(1) + interval_group.setLayout(interval_layout) + + # Toggle button + self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources') + 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.clicked.connect(self._toggle_resource_display) + + # Add controls to layout + resource_layout.addWidget(interval_group, 3, 0) + resource_layout.addWidget(self.toggle_resources_btn, 3, 1) self.resource_group.setLayout(resource_layout) self.resource_widgets_visible = True From 517ab1bac827ac76969f0aeb885142a360e4a141 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:12:32 +0200 Subject: [PATCH 04/17] Enhance title bar theming with multiple application attempts; streamline resource display toggle functionality --- main.py | 90 ++++++++++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/main.py b/main.py index 55c90b6..0d7035b 100644 --- a/main.py +++ b/main.py @@ -1412,6 +1412,13 @@ class MainWindow(QtWidgets.QMainWindow): self.apply_theme() self._update_theme_combo_text() + # Apply title bar theming with multiple attempts to ensure it works + is_dark = self.get_effective_dark_mode() + self.apply_dark_title_bar(is_dark) + QtCore.QTimer.singleShot(100, lambda: self.apply_dark_title_bar(is_dark)) + QtCore.QTimer.singleShot(300, lambda: self.apply_dark_title_bar(is_dark)) + QtCore.QTimer.singleShot(1000, lambda: self.apply_dark_title_bar(is_dark)) + # Start theme monitoring if in system mode if self.theme_mode == 'system': self._last_system_theme = self.is_system_dark_theme() @@ -1665,31 +1672,6 @@ class MainWindow(QtWidgets.QMainWindow): if hasattr(self, 'theme_monitor_timer') and self.theme_monitor_timer: self.theme_monitor_timer.stop() - def _toggle_resource_display(self): - """Toggle the visibility of resource usage widgets.""" - if self.resource_widgets_visible: - # Hide resource widgets - self.memory_label.hide() - self.cpu_label.hide() - self.system_memory_label.hide() - self.cache_label.hide() - self.gpu_label.hide() - self.performance_label.hide() - self.update_interval_combo.hide() - self.toggle_resources_btn.setText('Show Resources') - self.resource_widgets_visible = False - else: - # Show resource widgets - self.memory_label.show() - self.cpu_label.show() - self.system_memory_label.show() - self.cache_label.show() - self.gpu_label.show() - self.performance_label.show() - self.update_interval_combo.show() - self.toggle_resources_btn.setText('Hide Resources') - self.resource_widgets_visible = True - def _show_psutil_info(self): """Show information about installing psutil for enhanced monitoring.""" msg = QtWidgets.QMessageBox(self) @@ -1884,8 +1866,11 @@ class MainWindow(QtWidgets.QMainWindow): 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())) + # Apply title bar theming when window is shown with multiple attempts + is_dark = self.get_effective_dark_mode() + QtCore.QTimer.singleShot(50, lambda: self.apply_dark_title_bar(is_dark)) + QtCore.QTimer.singleShot(150, lambda: self.apply_dark_title_bar(is_dark)) + QtCore.QTimer.singleShot(300, lambda: self.apply_dark_title_bar(is_dark)) def changeEvent(self, event): """Handle window state changes (maximize/minimize).""" @@ -2107,7 +2092,7 @@ class MainWindow(QtWidgets.QMainWindow): ) if result == 0: # S_OK - logger.debug(f"Applied Windows dark title bar ({'newer' if use_newer_api else 'legacy'} API)") + logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar ({'newer' if use_newer_api else 'legacy'} API)") # Force window to redraw self.update() @@ -2116,7 +2101,7 @@ class MainWindow(QtWidgets.QMainWindow): 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}") + logger.debug(f"Windows {'dark' if enable_dark else 'light'} 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 @@ -2128,7 +2113,7 @@ class MainWindow(QtWidgets.QMainWindow): ctypes.sizeof(ctypes.c_int) ) if result == 0: - logger.debug("Applied Windows dark title bar (fallback API)") + logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar (fallback API)") self.update() return True except Exception as fallback_e: @@ -2186,8 +2171,17 @@ class MainWindow(QtWidgets.QMainWindow): colors = self.get_theme_styles() is_dark = self.get_effective_dark_mode() - # Apply dark title bar if in dark mode (with small delay to ensure window is ready) - QtCore.QTimer.singleShot(10, lambda: self.apply_dark_title_bar(is_dark)) + # Apply title bar theming with multiple attempts to ensure it takes effect + def apply_title_bar_theming(): + success = self.apply_dark_title_bar(is_dark) + if not success: + # Retry after a longer delay if first attempt failed + QtCore.QTimer.singleShot(100, lambda: self.apply_dark_title_bar(is_dark)) + + # Apply immediately and with delays to handle different timing scenarios + apply_title_bar_theming() + QtCore.QTimer.singleShot(50, apply_title_bar_theming) + QtCore.QTimer.singleShot(200, apply_title_bar_theming) # Main window style main_style = f""" @@ -2196,6 +2190,22 @@ class MainWindow(QtWidgets.QMainWindow): color: {colors['foreground']}; }} QPushButton {{ + font-size: 9pt; + min-height: 20px; + padding: 2px 6px; + min-width: 90px; + 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']}; + }} + QComboBox {{ font-size: 9pt; min-height: 20px; padding: 2px 6px; @@ -2315,6 +2325,10 @@ class MainWindow(QtWidgets.QMainWindow): self.apply_theme() self._update_theme_combo_text() + # Apply title bar theming immediately for theme changes + is_dark = self.get_effective_dark_mode() + self.apply_dark_title_bar(is_dark) + # Save the theme preference immediately self._save_window_geometry() @@ -2516,18 +2530,10 @@ class MainWindow(QtWidgets.QMainWindow): interval_layout.addStretch(1) interval_group.setLayout(interval_layout) - # Toggle button - self.toggle_resources_btn = QtWidgets.QPushButton('Hide Resources') - 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.clicked.connect(self._toggle_resource_display) - - # Add controls to layout - resource_layout.addWidget(interval_group, 3, 0) - resource_layout.addWidget(self.toggle_resources_btn, 3, 1) + # Add controls to layout - update interval group spans full width + resource_layout.addWidget(interval_group, 3, 0, 1, 2) self.resource_group.setLayout(resource_layout) - self.resource_widgets_visible = True # Main layout layout = QtWidgets.QGridLayout() From 7ceae34fba653dabe9376ab80ed3f5a246729359 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:14:42 +0200 Subject: [PATCH 05/17] Adjust MainWindow size to accommodate UI elements; increase minimum height to 800px --- main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 0d7035b..24bb4fa 100644 --- a/main.py +++ b/main.py @@ -1270,9 +1270,9 @@ class MainWindow(QtWidgets.QMainWindow): """ super().__init__() self.setWindowTitle('Scenario Image Automation') - # Make window smaller and more compact + # Make window larger to accommodate all UI elements min_width = 640 - min_height = 640 + min_height = 800 self.setMinimumSize(min_width, min_height) self.setGeometry(100, 100, min_width, min_height) self.running = False @@ -1801,7 +1801,7 @@ class MainWindow(QtWidgets.QMainWindow): x = max(0, min(geometry_data['x'], screen_width - 100)) # Ensure at least 100px visible y = max(0, min(geometry_data['y'], screen_height - 100)) width = max(640, min(geometry_data['width'], screen_width)) # Minimum 640px wide - height = max(480, min(geometry_data['height'], screen_height)) # Minimum 480px tall + height = max(800, min(geometry_data['height'], screen_height)) # Minimum 800px tall # Apply geometry self.setGeometry(x, y, width, height) From 9f38776a5ad549c438c1a8fe9233b16647abee9b Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:19:07 +0200 Subject: [PATCH 06/17] Reorganize resource display layout; move GPU memory info up and adjust update interval controls --- main.py | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/main.py b/main.py index 24bb4fa..da246ee 100644 --- a/main.py +++ b/main.py @@ -2482,6 +2482,10 @@ class MainWindow(QtWidgets.QMainWindow): self.cpu_label = QtWidgets.QLabel('CPU: --') resource_layout.addWidget(self.cpu_label, 0, 1) + # GPU memory info (moved up) + self.gpu_label = QtWidgets.QLabel('GPU: --') + resource_layout.addWidget(self.gpu_label, 0, 2) + # System memory self.system_memory_label = QtWidgets.QLabel('System: --') resource_layout.addWidget(self.system_memory_label, 1, 0) @@ -2490,21 +2494,15 @@ class MainWindow(QtWidgets.QMainWindow): self.cache_label = QtWidgets.QLabel('Cache: --') resource_layout.addWidget(self.cache_label, 1, 1) - # GPU memory info - self.gpu_label = QtWidgets.QLabel('GPU: --') - resource_layout.addWidget(self.gpu_label, 1, 2) - - # Performance info + # Performance info (moved down) self.performance_label = QtWidgets.QLabel('Performance: --') - resource_layout.addWidget(self.performance_label, 2, 0, 1, 3) + resource_layout.addWidget(self.performance_label, 1, 2) - # Controls row - Update interval group - interval_group = QtWidgets.QGroupBox('Update Interval') - interval_group.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) - interval_layout = QtWidgets.QHBoxLayout() - interval_layout.setContentsMargins(4, 4, 4, 4) - interval_layout.setSpacing(4) + # Controls row - Update interval controls (no border, like theme controls) + update_interval_layout = QtWidgets.QHBoxLayout() + update_interval_layout.setSpacing(6) + self.update_interval_label = QtWidgets.QLabel('Update Interval:') self.update_interval_combo = QtWidgets.QComboBox() self.update_interval_combo.setMaximumWidth(80) self.update_interval_combo.setToolTip('Set resource monitoring update interval') @@ -2526,12 +2524,12 @@ class MainWindow(QtWidgets.QMainWindow): self.update_interval_combo.setCurrentIndex(2) self.update_interval_combo.currentIndexChanged.connect(self._on_update_interval_changed) - interval_layout.addWidget(self.update_interval_combo) - interval_layout.addStretch(1) - interval_group.setLayout(interval_layout) + update_interval_layout.addWidget(self.update_interval_label) + update_interval_layout.addWidget(self.update_interval_combo) + update_interval_layout.addStretch(1) - # Add controls to layout - update interval group spans full width - resource_layout.addWidget(interval_group, 3, 0, 1, 2) + # Add controls to layout - update interval controls span full width + resource_layout.addLayout(update_interval_layout, 2, 0, 1, 3) self.resource_group.setLayout(resource_layout) From a9965493e8fae662af12ab29bb89a29d2fc45eee Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:19:49 +0200 Subject: [PATCH 07/17] Adjust minimum height of MainWindow to 720px for better UI element accommodation --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index da246ee..8870561 100644 --- a/main.py +++ b/main.py @@ -1272,7 +1272,7 @@ class MainWindow(QtWidgets.QMainWindow): self.setWindowTitle('Scenario Image Automation') # Make window larger to accommodate all UI elements min_width = 640 - min_height = 800 + min_height = 720 self.setMinimumSize(min_width, min_height) self.setGeometry(100, 100, min_width, min_height) self.running = False From 2abc71347ffbf20282765ffea145ed09c21cbc3c Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:27:49 +0200 Subject: [PATCH 08/17] Enhance title bar theming and refresh logic; ensure immediate updates for main and dialog windows --- main.py | 129 +++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 114 insertions(+), 15 deletions(-) diff --git a/main.py b/main.py index 8870561..5175f46 100644 --- a/main.py +++ b/main.py @@ -2094,8 +2094,19 @@ class MainWindow(QtWidgets.QMainWindow): 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)") - # Force window to redraw + # Force immediate window redraw using multiple methods self.update() + self.repaint() + + # Force a window refresh by briefly changing and restoring window flags + try: + original_flags = self.windowFlags() + self.setWindowFlags(original_flags | QtCore.Qt.WindowType.WindowStaysOnTopHint) + self.setWindowFlags(original_flags) + self.show() + except: + pass + return True else: logger.debug(f"Windows title bar API returned error code: {result}") @@ -2115,6 +2126,17 @@ class MainWindow(QtWidgets.QMainWindow): if result == 0: logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar (fallback API)") self.update() + self.repaint() + + # Force window refresh + try: + original_flags = self.windowFlags() + self.setWindowFlags(original_flags | QtCore.Qt.WindowType.WindowStaysOnTopHint) + self.setWindowFlags(original_flags) + self.show() + except: + pass + return True except Exception as fallback_e: logger.debug(f"Windows dark title bar fallback failed: {fallback_e}") @@ -2171,17 +2193,36 @@ class MainWindow(QtWidgets.QMainWindow): colors = self.get_theme_styles() is_dark = self.get_effective_dark_mode() - # Apply title bar theming with multiple attempts to ensure it takes effect - def apply_title_bar_theming(): - success = self.apply_dark_title_bar(is_dark) - if not success: - # Retry after a longer delay if first attempt failed - QtCore.QTimer.singleShot(100, lambda: self.apply_dark_title_bar(is_dark)) + # Apply title bar theming immediately + self.apply_dark_title_bar(is_dark) - # Apply immediately and with delays to handle different timing scenarios - apply_title_bar_theming() - QtCore.QTimer.singleShot(50, apply_title_bar_theming) - QtCore.QTimer.singleShot(200, apply_title_bar_theming) + # Also apply to any open dialogs immediately + for dialog in QtWidgets.QApplication.allWidgets(): + if isinstance(dialog, QtWidgets.QDialog) and dialog.isVisible(): + try: + 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: + dialog.update() + dialog.repaint() + break + except: + continue + except: + pass # Main window style main_style = f""" @@ -2234,13 +2275,17 @@ class MainWindow(QtWidgets.QMainWindow): QComboBox::drop-down {{ border: none; width: 20px; + subcontrol-origin: padding; + subcontrol-position: center right; }} QComboBox::down-arrow {{ image: none; - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid {colors['foreground']}; - margin: 0px 4px 0px 4px; + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid {colors['foreground']}; + margin: 0px; }} QListWidget {{ font-size: 9pt; @@ -2329,6 +2374,39 @@ class MainWindow(QtWidgets.QMainWindow): is_dark = self.get_effective_dark_mode() self.apply_dark_title_bar(is_dark) + # Also apply to any open dialogs immediately + for dialog in QtWidgets.QApplication.allWidgets(): + if isinstance(dialog, QtWidgets.QDialog) and dialog.isVisible(): + try: + 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: + dialog.update() + dialog.repaint() + break + except: + continue + except: + pass + + # Force an immediate application repaint for instant visual feedback + QtWidgets.QApplication.processEvents() + self.update() + self.repaint() + # Save the theme preference immediately self._save_window_geometry() @@ -2457,11 +2535,32 @@ class MainWindow(QtWidgets.QMainWindow): self.theme_combo.setMinimumWidth(100) self.theme_combo.setMaximumWidth(120) self.theme_combo.setToolTip('Select theme: System (follows OS), Light, or Dark') + + # Make the theme dropdown open upwards by setting a custom style + # This will be handled in the theme application + 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) + + # Make theme dropdown open upwards + def showPopupUpwards(): + # Get the combo box position and size + combo_rect = self.theme_combo.geometry() + popup = self.theme_combo.view() + popup_height = popup.sizeHint().height() + + # Calculate position above the combo box + global_pos = self.theme_combo.mapToGlobal(QtCore.QPoint(0, -popup_height)) + + # Show the popup at the calculated position + popup.setGeometry(global_pos.x(), global_pos.y(), combo_rect.width(), popup_height) + popup.show() + + # Override the showPopup method to make it open upwards + self.theme_combo.showPopup = showPopupUpwards # State label self.state_label = QtWidgets.QLabel('State: Paused') From be5f538a692455b89422f681b326918e93ee891d Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:29:28 +0200 Subject: [PATCH 09/17] Refactor title bar theming logic; reduce multiple attempts to single application for improved efficiency --- main.py | 29 +++-------------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/main.py b/main.py index 5175f46..fd2b665 100644 --- a/main.py +++ b/main.py @@ -1412,12 +1412,9 @@ class MainWindow(QtWidgets.QMainWindow): self.apply_theme() self._update_theme_combo_text() - # Apply title bar theming with multiple attempts to ensure it works + # Apply title bar theming once more to ensure it takes effect is_dark = self.get_effective_dark_mode() self.apply_dark_title_bar(is_dark) - QtCore.QTimer.singleShot(100, lambda: self.apply_dark_title_bar(is_dark)) - QtCore.QTimer.singleShot(300, lambda: self.apply_dark_title_bar(is_dark)) - QtCore.QTimer.singleShot(1000, lambda: self.apply_dark_title_bar(is_dark)) # Start theme monitoring if in system mode if self.theme_mode == 'system': @@ -1866,11 +1863,9 @@ class MainWindow(QtWidgets.QMainWindow): 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 with multiple attempts + # Apply title bar theming when window is shown (once only) is_dark = self.get_effective_dark_mode() QtCore.QTimer.singleShot(50, lambda: self.apply_dark_title_bar(is_dark)) - QtCore.QTimer.singleShot(150, lambda: self.apply_dark_title_bar(is_dark)) - QtCore.QTimer.singleShot(300, lambda: self.apply_dark_title_bar(is_dark)) def changeEvent(self, event): """Handle window state changes (maximize/minimize).""" @@ -2094,19 +2089,10 @@ class MainWindow(QtWidgets.QMainWindow): 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)") - # Force immediate window redraw using multiple methods + # Force immediate window redraw self.update() self.repaint() - # Force a window refresh by briefly changing and restoring window flags - try: - original_flags = self.windowFlags() - self.setWindowFlags(original_flags | QtCore.Qt.WindowType.WindowStaysOnTopHint) - self.setWindowFlags(original_flags) - self.show() - except: - pass - return True else: logger.debug(f"Windows title bar API returned error code: {result}") @@ -2128,15 +2114,6 @@ class MainWindow(QtWidgets.QMainWindow): self.update() self.repaint() - # Force window refresh - try: - original_flags = self.windowFlags() - self.setWindowFlags(original_flags | QtCore.Qt.WindowType.WindowStaysOnTopHint) - self.setWindowFlags(original_flags) - self.show() - except: - pass - return True except Exception as fallback_e: logger.debug(f"Windows dark title bar fallback failed: {fallback_e}") From 0ec8bee99f8bde2a8d206f934cc50ea5214ad50d Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:32:59 +0200 Subject: [PATCH 10/17] Refactor button layout in MainWindow; reduce padding, minimum width, and spacing for improved UI consistency --- main.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/main.py b/main.py index fd2b665..76b6a6f 100644 --- a/main.py +++ b/main.py @@ -2210,8 +2210,8 @@ class MainWindow(QtWidgets.QMainWindow): QPushButton {{ font-size: 9pt; min-height: 20px; - padding: 2px 6px; - min-width: 90px; + padding: 2px 4px; + min-width: 35px; background-color: {colors['button_bg']}; border: 1px solid {colors['border']}; border-radius: 3px; @@ -2490,9 +2490,18 @@ class MainWindow(QtWidgets.QMainWindow): self.btn_move_down_step = QtWidgets.QPushButton('Move Down') self.btn_move_down_step.setToolTip('Move step down (lower priority)') self.btn_move_down_step.clicked.connect(self._log_btn_move_down_step) + + # Set all step buttons to have flexible sizing + step_buttons = [self.btn_add_step, self.btn_edit_step, self.btn_del_step, + self.btn_rename_step, self.btn_move_up_step, self.btn_move_down_step] + for btn in step_buttons: + btn.setSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + btn.setMinimumWidth(35) # Further reduced minimum width for full text + btn.setMaximumHeight(24) # Keep height consistent + step_btn_group_layout = QtWidgets.QHBoxLayout() - step_btn_group_layout.setSpacing(4) - step_btn_group_layout.setContentsMargins(4, 4, 4, 4) + step_btn_group_layout.setSpacing(2) # Reduced spacing + step_btn_group_layout.setContentsMargins(2, 2, 2, 2) # Reduced margins step_btn_group_layout.addWidget(self.btn_add_step) step_btn_group_layout.addWidget(self.btn_edit_step) step_btn_group_layout.addWidget(self.btn_del_step) From 06f8061fc546bb92f4d2c5a7d24111f7020f347e Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:42:46 +0200 Subject: [PATCH 11/17] Add settings dialog for application configuration; include theme, logging level, and update interval options --- main.py | 431 +++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 361 insertions(+), 70 deletions(-) diff --git a/main.py b/main.py index 76b6a6f..aea089e 100644 --- a/main.py +++ b/main.py @@ -812,6 +812,227 @@ def take_screenshot_with_tkinter(): return selection_rect + +class SettingsDialog(QtWidgets.QDialog): + """ + Settings dialog for application configuration. + """ + def __init__(self, parent=None): + super().__init__(parent) + self.parent_window = parent + self.setWindowTitle('Settings') + self.setModal(True) + self.setFixedSize(400, 300) + + # Apply the same theme as parent window + if parent: + colors = parent.get_theme_styles() + self.setStyleSheet(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: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid {colors['foreground']}; + margin: 0px; + }} + 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']}; + }} + """) + + self.init_ui() + + def init_ui(self): + """Initialize the settings dialog UI.""" + layout = QtWidgets.QVBoxLayout() + layout.setSpacing(10) + layout.setContentsMargins(15, 15, 15, 15) + + # Appearance settings group + appearance_group = QtWidgets.QGroupBox('Appearance') + appearance_layout = QtWidgets.QFormLayout() + appearance_layout.setSpacing(8) + + # Theme setting + self.theme_combo = QtWidgets.QComboBox() + self.theme_combo.setMinimumWidth(120) + 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') + + # Set current theme + if self.parent_window: + theme_display_map = {'system': 'System', 'light': 'Light', 'dark': 'Dark'} + current_theme_display = theme_display_map.get(self.parent_window.theme_mode, 'System') + self.theme_combo.setCurrentText(current_theme_display) + + self.theme_combo.currentTextChanged.connect(self.on_theme_changed) + appearance_layout.addRow('Theme:', self.theme_combo) + + appearance_group.setLayout(appearance_layout) + layout.addWidget(appearance_group) + + # Logging settings group + logging_group = QtWidgets.QGroupBox('Logging') + logging_layout = QtWidgets.QFormLayout() + logging_layout.setSpacing(8) + + # Logging level setting + self.logging_combo = QtWidgets.QComboBox() + self.logging_combo.setMinimumWidth(120) + self.logging_combo.setToolTip('Set logging level: DEBUG shows all messages, ERROR shows only critical errors') + + logging_levels = [ + ('DEBUG', logging.DEBUG), + ('INFO', logging.INFO), + ('WARNING', logging.WARNING), + ('ERROR', logging.ERROR), + ('CRITICAL', logging.CRITICAL) + ] + + for text, level in logging_levels: + self.logging_combo.addItem(text, level) + + # Set current logging level + if self.parent_window and hasattr(self.parent_window, 'logging_combo'): + current_index = self.parent_window.logging_combo.currentIndex() + self.logging_combo.setCurrentIndex(current_index) + else: + self.logging_combo.setCurrentIndex(1) # Default to INFO + + self.logging_combo.currentTextChanged.connect(self.on_logging_level_changed) + logging_layout.addRow('Log Level:', self.logging_combo) + + logging_group.setLayout(logging_layout) + layout.addWidget(logging_group) + + # Resource monitoring settings group + monitoring_group = QtWidgets.QGroupBox('Resource Monitoring') + monitoring_layout = QtWidgets.QFormLayout() + monitoring_layout.setSpacing(8) + + # Update interval setting + self.update_interval_combo = QtWidgets.QComboBox() + self.update_interval_combo.setMinimumWidth(120) + self.update_interval_combo.setToolTip('Set resource monitoring update interval') + + intervals = [ + ('0.5s', 500), + ('1s', 1000), + ('2s', 2000), + ('3s', 3000), + ('5s', 5000), + ('10s', 10000) + ] + + for text, value in intervals: + self.update_interval_combo.addItem(text, value) + + # Set current update interval + if self.parent_window and hasattr(self.parent_window, 'update_interval_combo'): + current_index = self.parent_window.update_interval_combo.currentIndex() + self.update_interval_combo.setCurrentIndex(current_index) + else: + self.update_interval_combo.setCurrentIndex(2) # Default to 2s + + self.update_interval_combo.currentIndexChanged.connect(self.on_update_interval_changed) + monitoring_layout.addRow('Update Interval:', self.update_interval_combo) + + monitoring_group.setLayout(monitoring_layout) + layout.addWidget(monitoring_group) + + # Add stretch to push everything to the top + layout.addStretch() + + # Button layout + button_layout = QtWidgets.QHBoxLayout() + button_layout.addStretch() + + # Close button + self.close_btn = QtWidgets.QPushButton('Close') + self.close_btn.clicked.connect(self.accept) + button_layout.addWidget(self.close_btn) + + layout.addLayout(button_layout) + self.setLayout(layout) + + def on_theme_changed(self, theme_text): + """Handle theme change from settings dialog.""" + if self.parent_window: + self.parent_window.on_theme_changed(theme_text) + # Update the dialog's theme immediately + colors = self.parent_window.get_theme_styles() + self.setStyleSheet(self.styleSheet()) # Re-apply stylesheet with new colors + + def on_logging_level_changed(self, level_text): + """Handle logging level change from settings dialog.""" + if self.parent_window: + self.parent_window.on_logging_level_changed(level_text) + + def on_update_interval_changed(self): + """Handle update interval change from settings dialog.""" + if self.parent_window: + # Sync the parent's combo box + parent_combo = self.parent_window.update_interval_combo + parent_combo.setCurrentIndex(self.update_interval_combo.currentIndex()) + self.parent_window._on_update_interval_changed() + + class MainWindow(QtWidgets.QMainWindow): """ Main application window for Scenario Image Automation. @@ -1757,6 +1978,7 @@ class MainWindow(QtWidgets.QMainWindow): existing_config['main_window'] = geometry_data existing_config['update_interval'] = self.update_interval_combo.currentData() existing_config['theme_mode'] = self.theme_mode + existing_config['logging_level'] = self.logging_combo.currentData() with open(config_path, 'w') as f: json.dump(existing_config, f, indent=2) @@ -1827,6 +2049,24 @@ class MainWindow(QtWidgets.QMainWindow): self.theme_combo.setCurrentText(theme_display) logger.debug(f"Restored theme mode: {saved_theme}") + # Restore logging level if saved + saved_logging_level = config.get('logging_level') + if saved_logging_level is not None: + # Find the index of the saved logging level in the combo box + for i in range(self.logging_combo.count()): + if self.logging_combo.itemData(i) == saved_logging_level: + self.logging_combo.setCurrentIndex(i) + # Apply the logging level immediately + logger.setLevel(saved_logging_level) + root_logger = logging.getLogger() + root_logger.setLevel(saved_logging_level) + for handler in logger.handlers: + handler.setLevel(saved_logging_level) + for handler in root_logger.handlers: + handler.setLevel(saved_logging_level) + break + logger.debug(f"Restored logging level: {saved_logging_level}") + logger.debug(f"Restored window geometry: x={x}, y={y}, w={width}, h={height}") except (json.JSONDecodeError, IOError, KeyError) as e: @@ -2389,6 +2629,72 @@ class MainWindow(QtWidgets.QMainWindow): logger.info(f'Theme changed to: {self.theme_mode} mode') + def on_logging_level_changed(self, level_text): + """ + Handle logging level dropdown selection change. + """ + # Get the logging level value from the combo box + level_value = self.logging_combo.currentData() + + if level_value is not None: + # Apply the new logging level to the logger + logger.setLevel(level_value) + + # Also update the root logger level if it exists + root_logger = logging.getLogger() + root_logger.setLevel(level_value) + + # Update all handlers to use the new level + for handler in logger.handlers: + handler.setLevel(level_value) + + for handler in root_logger.handlers: + handler.setLevel(level_value) + + # Save the logging level preference + self._save_window_geometry() + + # Log the change (this will only show if the level allows it) + logger.info(f'Logging level changed to: {level_text} ({level_value})') + logger.debug(f'Debug logging is now {"enabled" if level_value <= logging.DEBUG else "disabled"}') + + def show_settings_dialog(self): + """ + Show the settings dialog window. + """ + try: + dialog = SettingsDialog(self) + + # Apply title bar theming to the dialog + is_dark = self.get_effective_dark_mode() + try: + 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() + + except Exception as e: + logger.error(f'Error showing settings dialog: {e}') + def _update_theme_combo_text(self): """ Update the theme combo box text based on current theme mode and system state. @@ -2516,38 +2822,6 @@ class MainWindow(QtWidgets.QMainWindow): self.btn_start_stop.setMinimumWidth(80) self.btn_start_stop.clicked.connect(self._log_btn_start_stop) - # Theme dropdown - self.theme_combo = QtWidgets.QComboBox() - self.theme_combo.setMinimumWidth(100) - self.theme_combo.setMaximumWidth(120) - self.theme_combo.setToolTip('Select theme: System (follows OS), Light, or Dark') - - # Make the theme dropdown open upwards by setting a custom style - # This will be handled in the theme application - - 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) - - # Make theme dropdown open upwards - def showPopupUpwards(): - # Get the combo box position and size - combo_rect = self.theme_combo.geometry() - popup = self.theme_combo.view() - popup_height = popup.sizeHint().height() - - # Calculate position above the combo box - global_pos = self.theme_combo.mapToGlobal(QtCore.QPoint(0, -popup_height)) - - # Show the popup at the calculated position - popup.setGeometry(global_pos.x(), global_pos.y(), combo_rect.width(), popup_height) - popup.show() - - # Override the showPopup method to make it open upwards - self.theme_combo.showPopup = showPopupUpwards - # State label self.state_label = QtWidgets.QLabel('State: Paused') self.state_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter) @@ -2583,39 +2857,6 @@ class MainWindow(QtWidgets.QMainWindow): self.performance_label = QtWidgets.QLabel('Performance: --') resource_layout.addWidget(self.performance_label, 1, 2) - # Controls row - Update interval controls (no border, like theme controls) - update_interval_layout = QtWidgets.QHBoxLayout() - update_interval_layout.setSpacing(6) - - self.update_interval_label = QtWidgets.QLabel('Update Interval:') - self.update_interval_combo = QtWidgets.QComboBox() - self.update_interval_combo.setMaximumWidth(80) - self.update_interval_combo.setToolTip('Set resource monitoring update interval') - - # Add interval options (in milliseconds) - intervals = [ - ('0.5s', 500), - ('1s', 1000), - ('2s', 2000), - ('3s', 3000), - ('5s', 5000), - ('10s', 10000) - ] - - for text, value in intervals: - self.update_interval_combo.addItem(text, value) - - # Set default to 2 seconds (index 2) - self.update_interval_combo.setCurrentIndex(2) - self.update_interval_combo.currentIndexChanged.connect(self._on_update_interval_changed) - - update_interval_layout.addWidget(self.update_interval_label) - update_interval_layout.addWidget(self.update_interval_combo) - update_interval_layout.addStretch(1) - - # Add controls to layout - update interval controls span full width - resource_layout.addLayout(update_interval_layout, 2, 0, 1, 3) - self.resource_group.setLayout(resource_layout) # Main layout @@ -2642,12 +2883,62 @@ class MainWindow(QtWidgets.QMainWindow): start_state_layout.setSpacing(6) # Fixed gap between elements start_state_layout.addWidget(self.btn_start_stop) start_state_layout.addWidget(self.state_label) - start_state_layout.addStretch(1) # This pushes theme controls to the right + start_state_layout.addStretch(1) # This pushes settings button 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) + # Settings button on the right side + self.settings_btn = QtWidgets.QPushButton('Settings') + self.settings_btn.setMinimumWidth(70) + self.settings_btn.setMaximumWidth(90) + self.settings_btn.setToolTip('Open settings window to configure theme, logging, and monitoring options') + self.settings_btn.clicked.connect(self.show_settings_dialog) + start_state_layout.addWidget(self.settings_btn) + + # Initialize settings that were previously in the UI but now handled by dialog + self.theme_mode = 'system' # Default theme mode + self.logging_level = logging.INFO # Default logging level + + # Create theme and logging combos for settings dialog to reference + # These are not added to the UI layout but used by the settings dialog + self.theme_combo = QtWidgets.QComboBox() + 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) + + self.logging_combo = QtWidgets.QComboBox() + logging_levels = [ + ('DEBUG', logging.DEBUG), + ('INFO', logging.INFO), + ('WARNING', logging.WARNING), + ('ERROR', logging.ERROR), + ('CRITICAL', logging.CRITICAL) + ] + + for text, level in logging_levels: + self.logging_combo.addItem(text, level) + + # Set default to INFO level (index 1) + self.logging_combo.setCurrentIndex(1) + self.logging_combo.currentTextChanged.connect(self.on_logging_level_changed) + + # Create update interval combo for settings dialog to reference + self.update_interval_combo = QtWidgets.QComboBox() + intervals = [ + ('0.5s', 500), + ('1s', 1000), + ('2s', 2000), + ('3s', 3000), + ('5s', 5000), + ('10s', 10000) + ] + + for text, value in intervals: + self.update_interval_combo.addItem(text, value) + + # Set default to 2 seconds (index 2) + self.update_interval_combo.setCurrentIndex(2) + self.update_interval_combo.currentIndexChanged.connect(self._on_update_interval_changed) start_state_group.setLayout(start_state_layout) layout.addWidget(start_state_group, 6, 0, 1, 5) From dda575f043a002029b50095717c64540db40a28b Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:49:59 +0200 Subject: [PATCH 12/17] Enhance SettingsDialog theme application; ensure immediate updates and consistency across child widgets --- main.py | 135 +++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 26 deletions(-) diff --git a/main.py b/main.py index aea089e..e016f34 100644 --- a/main.py +++ b/main.py @@ -893,8 +893,16 @@ class SettingsDialog(QtWidgets.QDialog): background-color: {colors['border']}; }} """) + + # Force immediate visual update for initial theme + self.update() + self.repaint() self.init_ui() + + # Apply theme to all child widgets after UI initialization + if parent: + QtCore.QTimer.singleShot(0, self.apply_theme_to_all_widgets) def init_ui(self): """Initialize the settings dialog UI.""" @@ -1011,13 +1019,104 @@ class SettingsDialog(QtWidgets.QDialog): layout.addLayout(button_layout) self.setLayout(layout) + def apply_theme_to_all_widgets(self): + """Apply theme to all child widgets to ensure consistency.""" + if not self.parent_window: + return + + # Force update all child widgets + for widget in self.findChildren(QtWidgets.QWidget): + widget.update() + + # Force layout updates + if self.layout(): + self.layout().update() + + # Process all events to ensure immediate visual update + QtWidgets.QApplication.processEvents() + def on_theme_changed(self, theme_text): """Handle theme change from settings dialog.""" if self.parent_window: self.parent_window.on_theme_changed(theme_text) - # Update the dialog's theme immediately + # Update the dialog's theme immediately with new colors colors = self.parent_window.get_theme_styles() - self.setStyleSheet(self.styleSheet()) # Re-apply stylesheet with new colors + self.setStyleSheet(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: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 6px solid {colors['foreground']}; + margin: 0px; + }} + 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']}; + }} + """) + + # Force immediate visual update + self.update() + self.repaint() + + # Process all pending events to ensure immediate application + QtWidgets.QApplication.processEvents() + + # Apply theme to all child widgets + self.apply_theme_to_all_widgets() def on_logging_level_changed(self, level_text): """Handle logging level change from settings dialog.""" @@ -1748,7 +1847,7 @@ class MainWindow(QtWidgets.QMainWindow): current_mb, max_mb, avg_mb = self._get_resource_stats(self._resource_history['memory_mb']) current_pct, max_pct, avg_pct = self._get_resource_stats(self._resource_history['memory_percent']) - memory_text = f"Memory: {current_mb:.1f}MB ({current_pct:.1f}%)\nMax: {max_mb:.1f}MB ({max_pct:.1f}%) | Avg: {avg_mb:.1f}MB ({avg_pct:.1f}%)" + memory_text = f"Memory: {current_mb:.1f}MB ({current_pct:.1f}%)\nAvg: {avg_mb:.1f}MB ({avg_pct:.1f}%) | Max: {max_mb:.1f}MB ({max_pct:.1f}%)" memory_color = colors['error'] if current_pct > 10 else colors['success'] else: memory_text = "Memory: N/A (psutil needed)\nInstall psutil for detailed monitoring" @@ -1766,7 +1865,7 @@ class MainWindow(QtWidgets.QMainWindow): if memory_info.get('has_psutil', False): current_cpu, max_cpu, avg_cpu = self._get_resource_stats(self._resource_history['cpu_percent']) - cpu_text = f"CPU: {current_cpu:.1f}%\nMax: {max_cpu:.1f}% | Avg: {avg_cpu:.1f}%" + cpu_text = f"CPU: {current_cpu:.1f}%\nAvg: {avg_cpu:.1f}% | Max: {max_cpu:.1f}%" cpu_color = colors['error'] if current_cpu > 50 else colors['success'] else: cpu_text = "CPU: N/A\nInstall psutil for monitoring" @@ -1779,7 +1878,7 @@ class MainWindow(QtWidgets.QMainWindow): if memory_info.get('has_psutil', False): current_sys, max_sys, avg_sys = self._get_resource_stats(self._resource_history['system_memory_percent']) - system_text = f"System: {current_sys:.1f}% ({memory_info['system_memory_available_gb']:.1f}GB free)\nMax: {max_sys:.1f}% | Avg: {avg_sys:.1f}%" + system_text = f"System: {current_sys:.1f}% ({memory_info['system_memory_available_gb']:.1f}GB free)\nAvg: {avg_sys:.1f}% | Max: {max_sys:.1f}%" system_color = colors['error'] if current_sys > 85 else colors['success'] else: system_text = "System: N/A\nInstall psutil for monitoring" @@ -1807,7 +1906,7 @@ class MainWindow(QtWidgets.QMainWindow): current_gpu, max_gpu, avg_gpu = self._get_resource_stats(self._resource_history['gpu_utilization']) if gpu_total_mb > 0: - gpu_text = f"GPU: {gpu_used_mb:.0f}/{gpu_total_mb:.0f}MB ({current_gpu:.1f}%)\nMax: {max_gpu:.1f}% | Avg: {avg_gpu:.1f}%" + gpu_text = f"GPU: {gpu_used_mb:.0f}/{gpu_total_mb:.0f}MB ({current_gpu:.1f}%)\nAvg: {avg_gpu:.1f}% | Max: {max_gpu:.1f}%" gpu_color = colors['error'] if current_gpu > 80 else colors['warning'] if current_gpu > 60 else colors['success'] # Add GPU name as tooltip @@ -1851,7 +1950,7 @@ class MainWindow(QtWidgets.QMainWindow): if hasattr(self, '_loop_times') and self._loop_times: current_loop, max_loop, avg_loop = self._get_resource_stats(self._resource_history['loop_times']) - perf_text = f"Performance: {current_loop:.0f}ms loop time\nMax: {max_loop:.0f}ms | Avg: {avg_loop:.0f}ms" + perf_text = f"Performance: {current_loop:.0f}ms loop time\nAvg: {avg_loop:.0f}ms | Max: {max_loop:.0f}ms" perf_color = colors['error'] if current_loop > 500 else colors['success'] else: perf_text = "Performance: Not running\nStart automation to see metrics" @@ -2463,22 +2562,6 @@ class MainWindow(QtWidgets.QMainWindow): QPushButton:pressed {{ background-color: {colors['border']}; }} - QComboBox {{ - font-size: 9pt; - min-height: 20px; - padding: 2px 6px; - 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']}; - }} QComboBox {{ font-size: 9pt; min-height: 20px; @@ -2763,10 +2846,10 @@ class MainWindow(QtWidgets.QMainWindow): scenario_btn_group_layout.setSpacing(4) scenario_btn_group_layout.setContentsMargins(4, 4, 4, 4) scenario_btn_group_layout.addWidget(self.btn_new) - scenario_btn_group_layout.addWidget(self.btn_import) - scenario_btn_group_layout.addWidget(self.btn_export) scenario_btn_group_layout.addWidget(self.btn_rename_scenario) scenario_btn_group_layout.addWidget(self.btn_delete_scenario) + scenario_btn_group_layout.addWidget(self.btn_import) + scenario_btn_group_layout.addWidget(self.btn_export) scenario_group_layout.addLayout(scenario_btn_group_layout) scenario_group_box.setLayout(scenario_group_layout) @@ -2810,8 +2893,8 @@ class MainWindow(QtWidgets.QMainWindow): step_btn_group_layout.setContentsMargins(2, 2, 2, 2) # Reduced margins step_btn_group_layout.addWidget(self.btn_add_step) step_btn_group_layout.addWidget(self.btn_edit_step) - step_btn_group_layout.addWidget(self.btn_del_step) step_btn_group_layout.addWidget(self.btn_rename_step) + step_btn_group_layout.addWidget(self.btn_del_step) step_btn_group_layout.addWidget(self.btn_move_up_step) step_btn_group_layout.addWidget(self.btn_move_down_step) steps_group_layout.addLayout(step_btn_group_layout) From eb824ff7b286e9943f33436c0f7dc45f7f977a38 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:52:41 +0200 Subject: [PATCH 13/17] Enhance window geometry saving logic; add validation and prevent saving when minimized or not visible --- main.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/main.py b/main.py index e016f34..5182fad 100644 --- a/main.py +++ b/main.py @@ -1656,6 +1656,9 @@ class MainWindow(QtWidgets.QMainWindow): self.theme_monitor_timer = QtCore.QTimer() self.theme_monitor_timer.timeout.connect(self._check_system_theme_change) self._last_system_theme = None + + # Save initial geometry after window is fully shown (delayed to ensure proper initialization) + QtCore.QTimer.singleShot(1000, self._save_window_geometry) def _check_system_theme_change(self): """ @@ -2055,7 +2058,13 @@ class MainWindow(QtWidgets.QMainWindow): def _save_window_geometry(self): """Save the current window size and position to a config file.""" try: + # Don't save if window is minimized or not visible + if self.isMinimized() or not self.isVisible(): + return + config_path = os.path.join(CONFIG_DIR, 'window_config.json') + + # Get current geometry geometry_data = { 'x': self.x(), 'y': self.y(), @@ -2064,6 +2073,12 @@ class MainWindow(QtWidgets.QMainWindow): 'maximized': self.isMaximized() } + # Validate geometry data before saving + if (geometry_data['width'] < 400 or geometry_data['height'] < 300 or + geometry_data['x'] < -100 or geometry_data['y'] < -100): + logger.debug(f"Invalid geometry data, skipping save: {geometry_data}") + return + # Load existing config if it exists existing_config = {} if os.path.exists(config_path): @@ -2079,6 +2094,10 @@ class MainWindow(QtWidgets.QMainWindow): existing_config['theme_mode'] = self.theme_mode existing_config['logging_level'] = self.logging_combo.currentData() + # Create directory if it doesn't exist + os.makedirs(CONFIG_DIR, exist_ok=True) + + # Write config file with open(config_path, 'w') as f: json.dump(existing_config, f, indent=2) @@ -2110,6 +2129,12 @@ class MainWindow(QtWidgets.QMainWindow): logger.debug("Incomplete geometry data, using defaults") return + # Additional validation for reasonable values + if (geometry_data['width'] < 400 or geometry_data['height'] < 300 or + geometry_data['width'] > 5000 or geometry_data['height'] > 5000): + logger.debug("Invalid geometry dimensions, using defaults") + return + # Get screen dimensions to validate position screen = QtWidgets.QApplication.primaryScreen().geometry() screen_width = screen.width() @@ -2119,7 +2144,7 @@ class MainWindow(QtWidgets.QMainWindow): x = max(0, min(geometry_data['x'], screen_width - 100)) # Ensure at least 100px visible y = max(0, min(geometry_data['y'], screen_height - 100)) width = max(640, min(geometry_data['width'], screen_width)) # Minimum 640px wide - height = max(800, min(geometry_data['height'], screen_height)) # Minimum 800px tall + height = max(720, min(geometry_data['height'], screen_height)) # Minimum 720px tall (matching initial height) # Apply geometry self.setGeometry(x, y, width, height) From fda126e6117bba5229d9e4243fe70d6c754aa317 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:54:18 +0200 Subject: [PATCH 14/17] Refactor window position validation logic; allow negative positions for multi-monitor setups and ensure minimal visibility --- main.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 5182fad..bdecf20 100644 --- a/main.py +++ b/main.py @@ -2140,12 +2140,23 @@ class MainWindow(QtWidgets.QMainWindow): screen_width = screen.width() screen_height = screen.height() - # Validate and adjust position if necessary - x = max(0, min(geometry_data['x'], screen_width - 100)) # Ensure at least 100px visible - y = max(0, min(geometry_data['y'], screen_height - 100)) + # Use exact saved position with minimal validation (only prevent completely offscreen) + x = geometry_data['x'] + y = geometry_data['y'] width = max(640, min(geometry_data['width'], screen_width)) # Minimum 640px wide height = max(720, min(geometry_data['height'], screen_height)) # Minimum 720px tall (matching initial height) + # Only adjust position if window would be completely invisible + # Allow negative positions for multi-monitor setups + if x + width < 50: # Less than 50px visible horizontally + x = 50 - width + if y + height < 50: # Less than 50px visible vertically + y = 50 - height + if x > screen_width - 50: # Too far right + x = screen_width - 50 + if y > screen_height - 50: # Too far down + y = screen_height - 50 + # Apply geometry self.setGeometry(x, y, width, height) From 879141a0807998167d5531200f2d1b390e136816 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 15:56:13 +0200 Subject: [PATCH 15/17] Refactor window geometry adjustment logic; improve visibility checks and add logging for applied values --- main.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/main.py b/main.py index bdecf20..2d17b14 100644 --- a/main.py +++ b/main.py @@ -2146,23 +2146,34 @@ class MainWindow(QtWidgets.QMainWindow): width = max(640, min(geometry_data['width'], screen_width)) # Minimum 640px wide height = max(720, min(geometry_data['height'], screen_height)) # Minimum 720px tall (matching initial height) - # Only adjust position if window would be completely invisible + # Only adjust position if window would be completely invisible (very permissive) # Allow negative positions for multi-monitor setups - if x + width < 50: # Less than 50px visible horizontally - x = 50 - width - if y + height < 50: # Less than 50px visible vertically - y = 50 - height - if x > screen_width - 50: # Too far right - x = screen_width - 50 - if y > screen_height - 50: # Too far down - y = screen_height - 50 + if x + width < 10: # Less than 10px visible horizontally + x = 10 - width + if y + height < 10: # Less than 10px visible vertically + y = 10 - height + if x > screen_width - 10: # Too far right (only if less than 10px visible) + x = screen_width - 10 + if y > screen_height - 10: # Too far down (only if less than 10px visible) + y = screen_height - 10 - # Apply geometry + # Apply geometry immediately self.setGeometry(x, y, width, height) + # Log the exact values being applied for debugging + logger.debug(f"Applying geometry: x={x}, y={y}, width={width}, height={height}") + logger.debug(f"Original saved position: x={geometry_data['x']}, y={geometry_data['y']}") + logger.debug(f"Screen dimensions: {screen_width}x{screen_height}") + + # Apply geometry again with a small delay to override any system adjustments + QtCore.QTimer.singleShot(50, lambda: self.setGeometry(x, y, width, height)) + # Restore maximized state if applicable if geometry_data.get('maximized', False): self.showMaximized() + else: + # Ensure we're not maximized and apply the geometry one more time + QtCore.QTimer.singleShot(100, lambda: self.setGeometry(x, y, width, height)) # Restore update interval if saved saved_interval = config.get('update_interval') From 21dd3e7ba4b5cc877e967f1a6721ac34e9791140 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 16:02:17 +0200 Subject: [PATCH 16/17] Enhance SettingsDialog and MainWindow styling; improve QComboBox appearance and validation logic for geometry data --- main.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/main.py b/main.py index 2d17b14..316b804 100644 --- a/main.py +++ b/main.py @@ -869,12 +869,24 @@ class SettingsDialog(QtWidgets.QDialog): }} QComboBox::down-arrow {{ image: none; - width: 0; - height: 0; + 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; @@ -1083,12 +1095,24 @@ class SettingsDialog(QtWidgets.QDialog): }} QComboBox::down-arrow {{ image: none; - width: 0; - height: 0; + 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; @@ -2073,10 +2097,10 @@ class MainWindow(QtWidgets.QMainWindow): 'maximized': self.isMaximized() } - # Validate geometry data before saving - if (geometry_data['width'] < 400 or geometry_data['height'] < 300 or - geometry_data['x'] < -100 or geometry_data['y'] < -100): - logger.debug(f"Invalid geometry data, skipping save: {geometry_data}") + # Validate geometry data before saving - only prevent extremely invalid values + if (geometry_data['width'] < 300 or geometry_data['height'] < 200 or + geometry_data['width'] > 10000 or geometry_data['height'] > 10000): + logger.debug(f"Invalid geometry dimensions, skipping save: {geometry_data}") return # Load existing config if it exists @@ -2140,11 +2164,18 @@ class MainWindow(QtWidgets.QMainWindow): screen_width = screen.width() screen_height = screen.height() - # Use exact saved position with minimal validation (only prevent completely offscreen) + # Use the exact saved position and dimensions - no adjustments x = geometry_data['x'] - y = geometry_data['y'] - width = max(640, min(geometry_data['width'], screen_width)) # Minimum 640px wide - height = max(720, min(geometry_data['height'], screen_height)) # Minimum 720px tall (matching initial height) + y = geometry_data['y'] + width = geometry_data['width'] + height = geometry_data['height'] + + # Only ensure the saved dimensions meet actual minimum requirements + # but preserve the exact position + if width < 640: + width = 640 + if height < 720: + height = 720 # Only adjust position if window would be completely invisible (very permissive) # Allow negative positions for multi-monitor setups @@ -2627,12 +2658,24 @@ class MainWindow(QtWidgets.QMainWindow): }} QComboBox::down-arrow {{ image: none; - width: 0; - height: 0; + 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; }} QListWidget {{ font-size: 9pt; @@ -2721,6 +2764,10 @@ class MainWindow(QtWidgets.QMainWindow): is_dark = self.get_effective_dark_mode() self.apply_dark_title_bar(is_dark) + # Force immediate update and repaint + self.update() + self.repaint() + # Also apply to any open dialogs immediately for dialog in QtWidgets.QApplication.allWidgets(): if isinstance(dialog, QtWidgets.QDialog) and dialog.isVisible(): @@ -2746,6 +2793,9 @@ class MainWindow(QtWidgets.QMainWindow): break except: continue + # Force dialog to update its theme styling + if hasattr(dialog, 'apply_theme_to_all_widgets'): + dialog.apply_theme_to_all_widgets() except: pass From cb79b5d6e161d6052db3b8b2dd38dc3fc8fec892 Mon Sep 17 00:00:00 2001 From: Gabriel20xx Date: Thu, 31 Jul 2025 16:05:51 +0200 Subject: [PATCH 17/17] Refactor title bar theming logic; introduce helper methods for consistent application across windows and dialogs --- main.py | 204 ++++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 140 insertions(+), 64 deletions(-) diff --git a/main.py b/main.py index 316b804..bae5b98 100644 --- a/main.py +++ b/main.py @@ -2468,6 +2468,13 @@ class MainWindow(QtWidgets.QMainWindow): Apply dark mode to the window title bar. 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: import platform system = platform.system().lower() @@ -2479,7 +2486,7 @@ class MainWindow(QtWidgets.QMainWindow): from ctypes import wintypes # Get window handle - hwnd = int(self.winId()) + hwnd = int(window.winId()) # Define constants for Windows API DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19 @@ -2504,11 +2511,11 @@ class MainWindow(QtWidgets.QMainWindow): ) 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 - self.update() - self.repaint() + window.update() + window.repaint() return True else: @@ -2527,9 +2534,9 @@ class MainWindow(QtWidgets.QMainWindow): ctypes.sizeof(ctypes.c_int) ) if result == 0: - logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar (fallback API)") - self.update() - self.repaint() + logger.debug(f"Applied Windows {'dark' if enable_dark else 'light'} title bar to {window.__class__.__name__} (fallback API)") + window.update() + window.repaint() return True except Exception as fallback_e: @@ -2590,33 +2597,24 @@ class MainWindow(QtWidgets.QMainWindow): # Apply title bar theming immediately self.apply_dark_title_bar(is_dark) - # Also apply to any open dialogs immediately - for dialog in QtWidgets.QApplication.allWidgets(): - if isinstance(dialog, QtWidgets.QDialog) and dialog.isVisible(): + # Apply to ALL widgets in the application (including dialogs that aren't currently visible) + for widget in QtWidgets.QApplication.allWidgets(): + if isinstance(widget, (QtWidgets.QDialog, QtWidgets.QMainWindow)): try: - 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: - dialog.update() - dialog.repaint() - break - except: - continue - except: - pass + # Apply title bar theme to each window/dialog + self._apply_title_bar_theme_to_window(widget, is_dark) + + # If it's a dialog, also force its stylesheet update + if isinstance(widget, QtWidgets.QDialog) and hasattr(widget, 'parent_window'): + if widget.parent_window == self: # Only update dialogs that belong to this main window + widget.setStyleSheet(self._get_dialog_stylesheet()) + widget.update() + widget.repaint() + except Exception as e: + logger.debug(f"Failed to apply theme to {widget.__class__.__name__}: {e}") + + # Store the current theme state globally so new windows can pick it up + QtWidgets.QApplication.instance().setProperty('dark_theme_enabled', is_dark) # Main window style main_style = f""" @@ -2732,6 +2730,91 @@ class MainWindow(QtWidgets.QMainWindow): self.gpu_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): """ Handle theme dropdown selection change. @@ -2845,30 +2928,9 @@ class MainWindow(QtWidgets.QMainWindow): try: 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() - try: - 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 + self._apply_title_bar_theme_to_window(dialog, is_dark) dialog.exec() @@ -2879,14 +2941,28 @@ class MainWindow(QtWidgets.QMainWindow): """ Update the theme combo box text based on current theme mode and system state. """ - if self.theme_mode == 'system': - system_dark = self.is_system_dark_theme() - display_text = f"System ({'Dark' if system_dark else 'Light'})" - # Update the first item's text to show current system theme - self.theme_combo.setItemText(0, display_text) - else: - # Reset system item text - self.theme_combo.setItemText(0, 'System') + # Temporarily disconnect the signal to prevent triggering theme changes + self.theme_combo.currentTextChanged.disconnect() + + try: + if self.theme_mode == 'system': + system_dark = self.is_system_dark_theme() + display_text = f"System ({'Dark' if system_dark else 'Light'})" + # 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): """