dark mode toggle + enhancements
This commit is contained in:
@@ -19,6 +19,7 @@ from queue import Empty
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from functools import partial
|
||||
from multiprocessing import Process, current_process, freeze_support, Queue, set_start_method
|
||||
|
||||
# External library imports
|
||||
@@ -28,8 +29,8 @@ from PySide6.QtWidgets import (
|
||||
QApplication, QWidget, QMessageBox, QVBoxLayout, QHBoxLayout, QTextEdit, QScrollArea, QComboBox, QGridLayout, QSplitter, QDialogButtonBox, QHeaderView,
|
||||
QPushButton, QMainWindow, QLabel, QLineEdit, QGroupBox, QDialog, QMenu, QSpinBox, QTableWidget, QTableWidgetItem
|
||||
)
|
||||
from PySide6.QtCore import QEvent, Signal, Qt, QTimer
|
||||
from PySide6.QtGui import QAction, QFontMetrics, QKeySequence, QIcon
|
||||
from PySide6.QtCore import QEvent, QObject, Signal, Qt, QTimer
|
||||
from PySide6.QtGui import QAction, QActionGroup, QFontMetrics, QKeySequence, QIcon
|
||||
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
||||
|
||||
from file_ext_registration import register_file_association, ELEVATION_FLAG
|
||||
@@ -64,6 +65,7 @@ show_welcome_dialog = false
|
||||
first_startup = true
|
||||
|
||||
[Preferences]
|
||||
theme = auto
|
||||
2d_data_bypass = false
|
||||
incompatible_save_bypass = false
|
||||
missing_events_bypass = false
|
||||
@@ -321,7 +323,7 @@ SECTIONS = [
|
||||
BIDS_FIELD_MAP = {
|
||||
"BIDS - Age": "AGE",
|
||||
"BIDS - Sex": "SEX",
|
||||
"BIDS - Hand": "HAND",
|
||||
"BIDS - Handedness": "HAND",
|
||||
}
|
||||
|
||||
|
||||
@@ -509,6 +511,27 @@ class CustomApplication(QApplication):
|
||||
return super().event(e)
|
||||
|
||||
|
||||
class ThemeChangeWatcher(QObject):
|
||||
def __init__(self, main_window):
|
||||
super().__init__()
|
||||
self.main_window = main_window
|
||||
self._theme_timer = QTimer(self)
|
||||
self._theme_timer.setSingleShot(True)
|
||||
self._theme_timer.setInterval(100)
|
||||
self._theme_timer.timeout.connect(self._apply_theme)
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
if event.type() == QEvent.Type.ApplicationPaletteChange:
|
||||
# Restart the timer instead of updating immediately.
|
||||
# Multiple palette-change events collapse into one update.
|
||||
self._theme_timer.start()
|
||||
|
||||
return False
|
||||
|
||||
def _apply_theme(self):
|
||||
print("OS theme changed")
|
||||
self.main_window.update_theme()
|
||||
|
||||
|
||||
class MainApplication(QMainWindow):
|
||||
"""
|
||||
@@ -643,10 +666,10 @@ class MainApplication(QMainWindow):
|
||||
self.left_v_splitter.setChildrenCollapsible(False)
|
||||
self.left_v_splitter.setMinimumWidth(460)
|
||||
|
||||
top_left_container = QGroupBox("File Information")
|
||||
top_left_container.setStyleSheet("QGroupBox { font-weight: bold; }")
|
||||
top_left_container.setMinimumHeight(240)
|
||||
top_left_layout = QHBoxLayout(top_left_container)
|
||||
self.top_left_container = QGroupBox("File Information")
|
||||
self.top_left_container.setStyleSheet("QGroupBox { font-weight: bold; }")
|
||||
self.top_left_container.setMinimumHeight(240)
|
||||
top_left_layout = QHBoxLayout(self.top_left_container)
|
||||
|
||||
self.top_left_widget = QTextEdit()
|
||||
self.top_left_widget.setReadOnly(True)
|
||||
@@ -659,22 +682,25 @@ class MainApplication(QMainWindow):
|
||||
font_metrics = QFontMetrics(self.font())
|
||||
label_width = max(font_metrics.horizontalAdvance(key.capitalize()) for key in self.meta_fields) + 10
|
||||
|
||||
self.meta_labels = {}
|
||||
|
||||
for key, field in self.meta_fields.items():
|
||||
row_layout = QHBoxLayout()
|
||||
row_layout.setContentsMargins(0, 0, 0, 0)
|
||||
row_layout.setSpacing(0)
|
||||
|
||||
label = QLabel(key.capitalize() + ":")
|
||||
self.meta_labels[key] = label
|
||||
label.setFixedWidth(label_width)
|
||||
row_layout.addWidget(label)
|
||||
row_layout.addWidget(field)
|
||||
right_column_layout.addLayout(row_layout)
|
||||
field.textChanged.connect(self.sync_bubble_data)
|
||||
|
||||
label_desc = QLabel('<a href="#">Why are these useful?</a>')
|
||||
label_desc.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
label_desc.linkActivated.connect(lambda: QMessageBox.information(None, f"Info - {APP_NAME.upper()} ", "Age: Used in determing the participants PPF. Also used to assist in creating groups.\nGender: Used to assist in creating groups.\nHand: Used to assist in creating groups.\nGroup: Used to split participants into groups for comparisons between them."))
|
||||
right_column_layout.addWidget(label_desc)
|
||||
self.label_desc = QLabel('<a href="#">Why are these useful?</a>')
|
||||
self.label_desc.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.label_desc.linkActivated.connect(lambda: QMessageBox.information(None, f"Info - {APP_NAME.upper()} ", "Age: Used in determing the participants PPF. Also used to assist in creating groups.\nGender: Used to assist in creating groups.\nHand: Used to assist in creating groups.\nGroup: Used to split participants into groups for comparisons between them."))
|
||||
right_column_layout.addWidget(self.label_desc)
|
||||
right_column_layout.addStretch()
|
||||
self.right_column_widget.hide()
|
||||
top_left_layout.addWidget(self.right_column_widget, stretch=1)
|
||||
@@ -688,7 +714,7 @@ class MainApplication(QMainWindow):
|
||||
self.scroll_area.setWidget(self.bubble_container)
|
||||
self.scroll_area.setMinimumHeight(200)
|
||||
|
||||
self.left_v_splitter.addWidget(top_left_container)
|
||||
self.left_v_splitter.addWidget(self.top_left_container)
|
||||
self.left_v_splitter.addWidget(self.scroll_area)
|
||||
|
||||
self.right_container = QWidget()
|
||||
@@ -845,6 +871,31 @@ class MainApplication(QMainWindow):
|
||||
self.pref_actions = {}
|
||||
|
||||
preferences_menu = menu_bar.addMenu("Preferences")
|
||||
theme_menu = preferences_menu.addMenu("Theme")
|
||||
|
||||
theme_group = QActionGroup(self)
|
||||
theme_group.setExclusive(True)
|
||||
|
||||
# 4. Define actions for the submenu
|
||||
theme_actions = [
|
||||
("Auto", "", "auto", resource_path("icons/warning_off_24dp_1F1F1F.svg"), "theme_auto"),
|
||||
("Light", "", "light", resource_path("icons/warning_off_24dp_1F1F1F.svg"), "theme_light"),
|
||||
("Dark", "", "dark", resource_path("icons/warning_off_24dp_1F1F1F.svg"), "theme_dark"),
|
||||
]
|
||||
|
||||
for name, shortcut, mode, icon, config_key in theme_actions:
|
||||
# Use partial to pass 'mode' to self.theme_change_func on click
|
||||
slot = partial(self.theme_change_func, mode)
|
||||
|
||||
action = make_action(name, shortcut, slot, icon=icon, checkable=True)
|
||||
theme_menu.addAction(action)
|
||||
theme_group.addAction(action)
|
||||
|
||||
self.pref_actions[config_key] = action
|
||||
|
||||
# Set default selection (e.g., Auto)
|
||||
self.pref_actions["theme_auto"].setChecked(True)
|
||||
|
||||
preferences_actions = [
|
||||
("2D Data Bypass", "", self.is_2d_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "2d_data_bypass"),
|
||||
("Incompatible Save Bypass", "", self.incompatable_save_bypass_func, resource_path("icons/warning_off_24dp_1F1F1F.svg"), "incompatible_save_bypass"),
|
||||
@@ -871,6 +922,31 @@ class MainApplication(QMainWindow):
|
||||
self.statusbar.showMessage("Ready")
|
||||
|
||||
|
||||
def update_theme(self):
|
||||
text = self.label_desc.text()
|
||||
self.label_desc.setText("")
|
||||
self.label_desc.setText(text)
|
||||
|
||||
widgets = [
|
||||
self.label_desc,
|
||||
self.top_left_widget,
|
||||
self.right_column_widget,
|
||||
self.top_left_container,
|
||||
]
|
||||
|
||||
widgets.extend(self.meta_fields.values())
|
||||
widgets.extend(self.meta_labels.values())
|
||||
|
||||
for widget in widgets:
|
||||
widget.style().unpolish(widget)
|
||||
widget.style().polish(widget)
|
||||
widget.update()
|
||||
|
||||
for section in self.param_sections:
|
||||
print("hi")
|
||||
section.update_theme_colors()
|
||||
|
||||
|
||||
def update_sections(self, index):
|
||||
self.current_section_index = index
|
||||
|
||||
@@ -1157,6 +1233,19 @@ class MainApplication(QMainWindow):
|
||||
self.top_left_widget.paste() # Trigger paste
|
||||
self.statusbar.showMessage("Pasted from clipboard") # Show status message
|
||||
|
||||
def theme_change_func(self, mode):
|
||||
app = QApplication.instance()
|
||||
style_hints = app.styleHints()
|
||||
|
||||
if mode == "auto":
|
||||
style_hints.setColorScheme(Qt.ColorScheme.Unknown)
|
||||
|
||||
elif mode == "light":
|
||||
style_hints.setColorScheme(Qt.ColorScheme.Light)
|
||||
|
||||
elif mode == "dark":
|
||||
style_hints.setColorScheme(Qt.ColorScheme.Dark)
|
||||
|
||||
def _update_config_setting(self, group, key, value):
|
||||
"""Helper to update memory configuration and save to disk."""
|
||||
# configparser expects string values
|
||||
@@ -1855,7 +1944,7 @@ class MainApplication(QMainWindow):
|
||||
if self.button3.isVisible():
|
||||
msg = QMessageBox(self)
|
||||
msg.setWindowTitle("Confirm - FLARES")
|
||||
msg.setText("Processing new data will clear the current analysis. Continue? (If you do not want this dialog box to appear, toggle 'Analysis Clearing Bypass' from the Preferences menu.)")
|
||||
msg.setText("Processing new data will clear the current analysis and close all other windows. Continue? (If you do not want this dialog box to appear, toggle 'Analysis Clearing Bypass' from the Preferences menu.)")
|
||||
|
||||
# Add the OK and Cancel buttons
|
||||
msg.setStandardButtons(QMessageBox.StandardButton.Ok | QMessageBox.StandardButton.Cancel)
|
||||
@@ -1871,11 +1960,18 @@ class MainApplication(QMainWindow):
|
||||
else:
|
||||
return
|
||||
|
||||
self.button3.setVisible(False)
|
||||
self.button3.setVisible(False)
|
||||
|
||||
for item in DATA_SCHEMA:
|
||||
setattr(self, item["key"], {})
|
||||
for item in DATA_SCHEMA:
|
||||
setattr(self, item["key"], {})
|
||||
|
||||
for bubble in self.bubble_widgets.values():
|
||||
bubble.reset()
|
||||
|
||||
for widget in QApplication.topLevelWidgets():
|
||||
if widget is not self and widget.isVisible():
|
||||
widget.close()
|
||||
|
||||
self.button1.clicked.disconnect(self.on_run_task)
|
||||
self.button1.setText("Cancel")
|
||||
self.button1.clicked.connect(self.cancel_task)
|
||||
@@ -2673,6 +2769,8 @@ if __name__ == "__main__":
|
||||
icon_ext = "icns" if PLATFORM_NAME == "darwin" else "ico"
|
||||
app.setWindowIcon(QIcon(resource_path(f"icons/main.{icon_ext}")))
|
||||
window = MainApplication(file_to_open=startup_args.initial_file)
|
||||
app.theme_watcher = ThemeChangeWatcher(window)
|
||||
app.installEventFilter(app.theme_watcher)
|
||||
app.file_open_requested.connect(window.project_manager.load_project)
|
||||
window.setWindowIcon(QIcon(resource_path(f"icons/main.{icon_ext}")))
|
||||
window.show()
|
||||
|
||||
Reference in New Issue
Block a user