Add settings dialog for application configuration; include theme, logging level, and update interval options

This commit is contained in:
2025-07-31 15:42:46 +02:00
parent 0ec8bee99f
commit 06f8061fc5
+361 -70
View File
@@ -812,6 +812,227 @@ def take_screenshot_with_tkinter():
return selection_rect 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): class MainWindow(QtWidgets.QMainWindow):
""" """
Main application window for Scenario Image Automation. Main application window for Scenario Image Automation.
@@ -1757,6 +1978,7 @@ class MainWindow(QtWidgets.QMainWindow):
existing_config['main_window'] = geometry_data existing_config['main_window'] = geometry_data
existing_config['update_interval'] = self.update_interval_combo.currentData() existing_config['update_interval'] = self.update_interval_combo.currentData()
existing_config['theme_mode'] = self.theme_mode existing_config['theme_mode'] = self.theme_mode
existing_config['logging_level'] = self.logging_combo.currentData()
with open(config_path, 'w') as f: with open(config_path, 'w') as f:
json.dump(existing_config, f, indent=2) json.dump(existing_config, f, indent=2)
@@ -1827,6 +2049,24 @@ class MainWindow(QtWidgets.QMainWindow):
self.theme_combo.setCurrentText(theme_display) self.theme_combo.setCurrentText(theme_display)
logger.debug(f"Restored theme mode: {saved_theme}") 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}") logger.debug(f"Restored window geometry: x={x}, y={y}, w={width}, h={height}")
except (json.JSONDecodeError, IOError, KeyError) as e: 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') 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): def _update_theme_combo_text(self):
""" """
Update the theme combo box text based on current theme mode and system state. Update the theme combo box text based on current theme mode and system state.
@@ -2516,38 +2822,6 @@ class MainWindow(QtWidgets.QMainWindow):
self.btn_start_stop.setMinimumWidth(80) self.btn_start_stop.setMinimumWidth(80)
self.btn_start_stop.clicked.connect(self._log_btn_start_stop) self.btn_start_stop.clicked.connect(self._log_btn_start_stop)
# Theme 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 # State label
self.state_label = QtWidgets.QLabel('State: Paused') self.state_label = QtWidgets.QLabel('State: Paused')
self.state_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter) 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: --') self.performance_label = QtWidgets.QLabel('Performance: --')
resource_layout.addWidget(self.performance_label, 1, 2) 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) self.resource_group.setLayout(resource_layout)
# Main layout # Main layout
@@ -2642,12 +2883,62 @@ class MainWindow(QtWidgets.QMainWindow):
start_state_layout.setSpacing(6) # Fixed gap between elements start_state_layout.setSpacing(6) # Fixed gap between elements
start_state_layout.addWidget(self.btn_start_stop) start_state_layout.addWidget(self.btn_start_stop)
start_state_layout.addWidget(self.state_label) 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 # Settings button on the right side
self.theme_label = QtWidgets.QLabel('Theme:') self.settings_btn = QtWidgets.QPushButton('Settings')
start_state_layout.addWidget(self.theme_label) self.settings_btn.setMinimumWidth(70)
start_state_layout.addWidget(self.theme_combo) 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) start_state_group.setLayout(start_state_layout)
layout.addWidget(start_state_group, 6, 0, 1, 5) layout.addWidget(start_state_group, 6, 0, 1, 5)