diff --git a/pyproject.toml b/pyproject.toml index 4a2b7ffa..cd993b79 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,9 +48,11 @@ mark-parentheses = false extend-ignore-names = ['allKeys', 'addItem', 'addItems', + 'changeEvent', 'closeEvent', 'columnCount', 'createEditor', + 'drawComplexControl', 'expandingDirections', 'eventFilter', 'hasHeightForWidth', diff --git a/rascal2/app.py b/rascal2/app.py index 7fddba1e..68836ae7 100644 --- a/rascal2/app.py +++ b/rascal2/app.py @@ -1,12 +1,11 @@ import logging import multiprocessing -import re -from contextlib import suppress from PyQt6 import QtWidgets from rascal2.config import MatlabHelper, handle_scaling, setup_logging -from rascal2.paths import IMAGES_PATH, STATIC_PATH +from rascal2.settings import get_colour_scheme +from rascal2.theme import THEMES, set_stylesheet from rascal2.ui.view import MainWindowView @@ -19,25 +18,19 @@ def ui_execute(splash): QApplication exit code """ handle_scaling() - QtWidgets.QApplication.setStyle("Fusion") app = QtWidgets.QApplication.instance() - with suppress(FileNotFoundError), open(STATIC_PATH / "style.css") as stylesheet: - palette = app.palette() - replacements = { - "@Path": IMAGES_PATH.as_posix(), - "@Window": palette.window().color().name(), - "@Highlight": palette.highlight().color().name(), - "@Midlight": palette.midlight().color().name(), - "@Text": palette.text().color().name(), - } - style = re.sub("|".join(replacements), lambda x: replacements[x.group(0)], stylesheet.read()) - app.setStyleSheet(style) + app.setStyle("Fusion") + app.installEventFilter(THEMES) + app.styleHints().setColorScheme(get_colour_scheme()) + set_stylesheet(app) window = MainWindowView() window.show() splash.finish(window) - return app.exec() + exit_code = app.exec() + app.removeEventFilter(THEMES) + return exit_code def start_app(splash): diff --git a/rascal2/dialogs/settings_dialog.py b/rascal2/dialogs/settings_dialog.py index b946e8e4..ddd3ac7a 100644 --- a/rascal2/dialogs/settings_dialog.py +++ b/rascal2/dialogs/settings_dialog.py @@ -7,7 +7,7 @@ from rascal2.config import LOGGER, SETTINGS, MatlabHelper from rascal2.paths import MATLAB_ARCH_FILE -from rascal2.settings import SettingsGroups +from rascal2.settings import SettingsGroups, change_ui_style from rascal2.widgets.inputs import get_validated_input @@ -69,6 +69,7 @@ def update_settings(self) -> None: def reset_default_settings(self) -> None: """Reset the settings to the global defaults.""" SETTINGS.reset_global_settings() + change_ui_style() self.accept() @@ -122,6 +123,10 @@ def modify_setting(self, setting: str): """ setattr(self.settings, setting, self.widgets[setting].get_data()) + match setting: + case "style": + change_ui_style(self.widgets[setting].get_data()) + class MatlabSetupTab(QtWidgets.QWidget): """Dialog to adjust Matlab location settings.""" diff --git a/rascal2/settings.py b/rascal2/settings.py index 37619b2b..5456a581 100644 --- a/rascal2/settings.py +++ b/rascal2/settings.py @@ -13,7 +13,7 @@ from typing import Any, TypeAlias from pydantic import BaseModel, Field -from PyQt6 import QtCore +from PyQt6 import QtCore, QtWidgets # we do this statically rather than making it an attribute of Settings because all fields in a Pydantic model @@ -52,7 +52,9 @@ class SettingsGroups(StrEnum): class Styles(StrEnum): """Visual styles for RasCAL-2.""" + System = "system" Light = "light" + Dark = "dark" class BackgroundColour(StrEnum): @@ -117,7 +119,7 @@ class Settings(BaseModel, validate_assignment=True, arbitrary_types_allowed=True # The Settings object's own model fields contain the within-project settings. # The global settings are read and written via this object using `set_global_settings`. - style: Styles = Field(default=Styles.Light, title=SettingsGroups.General, description="Style") + style: Styles = Field(default=Styles.System, title=SettingsGroups.General, description="Style") editor_fontsize: int = Field(default=12, title=SettingsGroups.General, description="Editor Font Size", gt=0) matlab_as_default_editor: bool = Field( default=False, @@ -159,7 +161,7 @@ def save(self, path: str | PathLike): def set_global_settings(self): """Set manually-set local settings as global settings.""" global_settings = get_global_settings() - for setting in self.model_fields_set: + for setting in self.model_fields: global_settings.setValue(global_name(setting), getattr(self, setting)) global_settings.sync() @@ -225,3 +227,29 @@ def update_recent_projects(path: str | None = None) -> list[str]: settings.setValue("recent_projects", new_recent_projects) settings.sync() return new_recent_projects + + +def get_colour_scheme(): + """Get the currently selected colour scheme and converts it to relevant Qt colour scheme flag.""" + colour_scheme = get_global_settings().value("General/style", "system") + match colour_scheme: + case "system": + colour_scheme_default = QtCore.Qt.ColorScheme.Unknown + case "light": + colour_scheme_default = QtCore.Qt.ColorScheme.Light + case "dark": + colour_scheme_default = QtCore.Qt.ColorScheme.Dark + return colour_scheme_default + + +def change_ui_style(style: Styles = Styles.System) -> None: + """Change the style of the app GUI to the given style.""" + app = QtWidgets.QApplication.instance() + match style: + case "system": + colour_scheme = QtCore.Qt.ColorScheme.Unknown + case "light": + colour_scheme = QtCore.Qt.ColorScheme.Light + case "dark": + colour_scheme = QtCore.Qt.ColorScheme.Dark + app.styleHints().setColorScheme(colour_scheme) diff --git a/rascal2/static/images/banner-dark.png b/rascal2/static/images/banner-dark.png new file mode 100644 index 00000000..08e4379b Binary files /dev/null and b/rascal2/static/images/banner-dark.png differ diff --git a/rascal2/static/images/banner.png b/rascal2/static/images/banner-light.png similarity index 100% rename from rascal2/static/images/banner.png rename to rascal2/static/images/banner-light.png diff --git a/rascal2/static/images/browse-dark.png b/rascal2/static/images/browse-dark.png index a89ceba0..4cf6e694 100644 Binary files a/rascal2/static/images/browse-dark.png and b/rascal2/static/images/browse-dark.png differ diff --git a/rascal2/static/images/browse-light.png b/rascal2/static/images/browse-light.png index 4cf6e694..a89ceba0 100644 Binary files a/rascal2/static/images/browse-light.png and b/rascal2/static/images/browse-light.png differ diff --git a/rascal2/static/images/cancel-dark.png b/rascal2/static/images/cancel-dark.png index 71041a03..71a9b88c 100644 Binary files a/rascal2/static/images/cancel-dark.png and b/rascal2/static/images/cancel-dark.png differ diff --git a/rascal2/static/images/cancel-light.png b/rascal2/static/images/cancel-light.png index 71a9b88c..71041a03 100644 Binary files a/rascal2/static/images/cancel-light.png and b/rascal2/static/images/cancel-light.png differ diff --git a/rascal2/static/images/create-dark.png b/rascal2/static/images/create-dark.png index 99544ce8..4dd1a10a 100644 Binary files a/rascal2/static/images/create-dark.png and b/rascal2/static/images/create-dark.png differ diff --git a/rascal2/static/images/create-light.png b/rascal2/static/images/create-light.png index 4dd1a10a..99544ce8 100644 Binary files a/rascal2/static/images/create-light.png and b/rascal2/static/images/create-light.png differ diff --git a/rascal2/static/images/delete-dark.png b/rascal2/static/images/delete-dark.png index eaef9b00..17c58783 100644 Binary files a/rascal2/static/images/delete-dark.png and b/rascal2/static/images/delete-dark.png differ diff --git a/rascal2/static/images/delete-light.png b/rascal2/static/images/delete-light.png new file mode 100644 index 00000000..eaef9b00 Binary files /dev/null and b/rascal2/static/images/delete-light.png differ diff --git a/rascal2/static/images/edit-dark.png b/rascal2/static/images/edit-dark.png new file mode 100644 index 00000000..6be929cf Binary files /dev/null and b/rascal2/static/images/edit-dark.png differ diff --git a/rascal2/static/images/edit.png b/rascal2/static/images/edit-light.png similarity index 100% rename from rascal2/static/images/edit.png rename to rascal2/static/images/edit-light.png diff --git a/rascal2/static/images/load-dark.png b/rascal2/static/images/footer-dark.png similarity index 56% rename from rascal2/static/images/load-dark.png rename to rascal2/static/images/footer-dark.png index c95a405a..f1debb61 100644 Binary files a/rascal2/static/images/load-dark.png and b/rascal2/static/images/footer-dark.png differ diff --git a/rascal2/static/images/footer.png b/rascal2/static/images/footer-light.png similarity index 100% rename from rascal2/static/images/footer.png rename to rascal2/static/images/footer-light.png diff --git a/rascal2/static/images/help-dark.png b/rascal2/static/images/help-dark.png new file mode 100644 index 00000000..d512a035 Binary files /dev/null and b/rascal2/static/images/help-dark.png differ diff --git a/rascal2/static/images/help.png b/rascal2/static/images/help-light.png similarity index 100% rename from rascal2/static/images/help.png rename to rascal2/static/images/help-light.png diff --git a/rascal2/static/images/hide-settings-dark.png b/rascal2/static/images/hide-settings-dark.png new file mode 100644 index 00000000..eeeba442 Binary files /dev/null and b/rascal2/static/images/hide-settings-dark.png differ diff --git a/rascal2/static/images/hide-settings.png b/rascal2/static/images/hide-settings-light.png similarity index 100% rename from rascal2/static/images/hide-settings.png rename to rascal2/static/images/hide-settings-light.png diff --git a/rascal2/static/images/import-r1-light.png b/rascal2/static/images/import-r1.png similarity index 100% rename from rascal2/static/images/import-r1-light.png rename to rascal2/static/images/import-r1.png diff --git a/rascal2/static/images/new-project-dark.png b/rascal2/static/images/new-project-dark.png new file mode 100644 index 00000000..2d98164f Binary files /dev/null and b/rascal2/static/images/new-project-dark.png differ diff --git a/rascal2/static/images/new-project.png b/rascal2/static/images/new-project-light.png similarity index 100% rename from rascal2/static/images/new-project.png rename to rascal2/static/images/new-project-light.png diff --git a/rascal2/static/images/play-dark.png b/rascal2/static/images/play-dark.png deleted file mode 100644 index 832a3759..00000000 Binary files a/rascal2/static/images/play-dark.png and /dev/null differ diff --git a/rascal2/static/images/redo-dark.png b/rascal2/static/images/redo-dark.png new file mode 100644 index 00000000..85812186 Binary files /dev/null and b/rascal2/static/images/redo-dark.png differ diff --git a/rascal2/static/images/redo.png b/rascal2/static/images/redo-light.png similarity index 100% rename from rascal2/static/images/redo.png rename to rascal2/static/images/redo-light.png diff --git a/rascal2/static/images/refresh-dark.png b/rascal2/static/images/refresh-dark.png new file mode 100644 index 00000000..24c5d1ae Binary files /dev/null and b/rascal2/static/images/refresh-dark.png differ diff --git a/rascal2/static/images/refresh.png b/rascal2/static/images/refresh-light.png similarity index 100% rename from rascal2/static/images/refresh.png rename to rascal2/static/images/refresh-light.png diff --git a/rascal2/static/images/save-project-dark.png b/rascal2/static/images/save-project-dark.png new file mode 100644 index 00000000..8ec24818 Binary files /dev/null and b/rascal2/static/images/save-project-dark.png differ diff --git a/rascal2/static/images/save-project.png b/rascal2/static/images/save-project-light.png similarity index 100% rename from rascal2/static/images/save-project.png rename to rascal2/static/images/save-project-light.png diff --git a/rascal2/static/images/settings-dark.png b/rascal2/static/images/settings-dark.png new file mode 100644 index 00000000..9c19273d Binary files /dev/null and b/rascal2/static/images/settings-dark.png differ diff --git a/rascal2/static/images/settings.png b/rascal2/static/images/settings-light.png similarity index 100% rename from rascal2/static/images/settings.png rename to rascal2/static/images/settings-light.png diff --git a/rascal2/static/images/stop-dark.png b/rascal2/static/images/stop-dark.png deleted file mode 100644 index a5922e6c..00000000 Binary files a/rascal2/static/images/stop-dark.png and /dev/null differ diff --git a/rascal2/static/images/tile-dark.png b/rascal2/static/images/tile-dark.png new file mode 100644 index 00000000..1067306e Binary files /dev/null and b/rascal2/static/images/tile-dark.png differ diff --git a/rascal2/static/images/tile.png b/rascal2/static/images/tile-light.png similarity index 100% rename from rascal2/static/images/tile.png rename to rascal2/static/images/tile-light.png diff --git a/rascal2/static/images/undo-dark.png b/rascal2/static/images/undo-dark.png new file mode 100644 index 00000000..50172f76 Binary files /dev/null and b/rascal2/static/images/undo-dark.png differ diff --git a/rascal2/static/images/undo.png b/rascal2/static/images/undo-light.png similarity index 100% rename from rascal2/static/images/undo.png rename to rascal2/static/images/undo-light.png diff --git a/rascal2/static/style.css b/rascal2/static/style.css index ed6d7259..37f492b9 100644 --- a/rascal2/static/style.css +++ b/rascal2/static/style.css @@ -5,7 +5,7 @@ } QDialog{ - border: 1px solid #ddd; + border: 1px solid darkgray; } QToolBar { @@ -23,11 +23,11 @@ QSpinBox, QDoubleSpinBox, QAbstractSpinBox, QLineEdit, QComboBox { QStatusBar { min-height: 20px; - border-top: 1px solid #ddd; + border-top: 1px solid @Midlight; } QPushButton { - background-color: white; + background-color: @Button; text-align: center; border: 1px solid #828282; padding: 5px 12px 5px 12px; @@ -37,14 +37,16 @@ QPushButton { min-height: 14px; } +QPushButton:focus, QPushButton:hover, -QPushButton:focus { - color: black; - border-color: #3874f2; +QPushButton:pressed { + color: @Text; + border-color: @Highlight; } -QPushButton:hover{ - background-color: #e0eef9; +QPushButton:hover, +QPushButton:pressed { + background-color: @Highlight; } QPushButton:disabled, @@ -54,26 +56,22 @@ QPushButton:disabled:checked { border-color: #b6b6b6; } -QPushButton:pressed { - background-color: #3874f2; -} - QPushButton:checked { background-color: #5e90fa; border-color: #3874f2; } QLineEdit { - color: black; - background-color: white; - selection-color: black; + color: @Text; + background-color: @Base; + selection-color: @Text; selection-background-color: @Highlight; - border: 1px solid #aaa; + border: 1px solid darkgray; border-radius: 3px; } QLineEdit:read-only { - selection-color: black; + selection-color: @Text; selection-background-color: @Highlight; background-color: transparent; } @@ -133,15 +131,15 @@ StartUpWidget QLabel { } StartUpWidget QToolButton#NewProjectButton{ - image: url(@Path/create-light.png); + image: url(@Path/create-dark.png); } StartUpWidget QToolButton#ImportProjectButton{ - image: url(@Path/browse-light.png); + image: url(@Path/browse-dark.png); } StartUpWidget QToolButton#ImportR1Button{ - image: url(@Path/import-r1-light.png); + image: url(@Path/import-r1.png); } /***************************** @@ -153,15 +151,15 @@ StartupDialog #CancelButton { } StartupDialog #BrowseButton { - icon: url(@Path/browse-dark.png); + icon: url(@Path/browse-light.png); } StartupDialog #CreateButton { - icon: url(@Path/create-dark.png); + icon: url(@Path/create-light.png); } StartupDialog #LoadButton { - icon: url(@Path/load-dark.png); + icon: url(@Path/load-light.png); } StartupDialog > QLabel { @@ -204,7 +202,7 @@ DisplayWidget #title{ } DisplayWidget #desc{ - color: #555 + color: @text } /***************************** @@ -228,17 +226,17 @@ ProjectWidget QPushButton{ } #InteractButton { - background-color: #dadbde; + background-color: @Button; } #InteractButton:hover, #InteractButton:focus { - color: black; - border-color: #3874f2; + color: @Text; + border-color: darkgray; } #InteractButton:hover{ - background-color: #e0eef9; + background-color: @Button; } #InteractButton:pressed { @@ -247,7 +245,7 @@ ProjectWidget QPushButton{ #InteractButton:checked { background-color: @Highlight; - border-color: #3874f2; + border-color: darkgray; } /***************************** @@ -255,12 +253,12 @@ ProjectWidget QPushButton{ *****************************/ TerminalWidget QPlainTextEdit{ - background-color: white; + background-color: @Base; } TerminalWidget QProgressBar{ border-radius: 0px; - border: 1px solid grey; + border: 1px solid @Midlight; text-align: center; } @@ -269,7 +267,7 @@ TerminalWidget QProgressBar{ *****************************/ ControlsWidget #FitSettings{ - background-color: white; + background-color: @Base; } ControlsWidget QPushButton { @@ -331,14 +329,14 @@ QTabWidget:focus { } QTabWidget::pane { - border: 1px solid lightgray; + border: 1px solid darkgray; top:-1px; - background: #f5f5f5;; + background: @Window;; } QTabBar::tab { - background: #e6e6e6; - border: 1px solid lightgray; + background: @Base; + border: 1px solid darkgray; padding: 10px; } @@ -357,12 +355,12 @@ QTabBar::tab:selected { QTableView { - background-color: white; + background-color: @Base; } QTableView::disabled { - background-color: @Window; + background-color: @Midlight; } QTableView item @@ -389,7 +387,7 @@ QTableView QPushButton { *****************************/ QMenuBar { - background-color: white; + background-color: @Window; } QMenuBar:disabled, @@ -425,7 +423,7 @@ AbstractProjectListWidget QPushButton { AbstractProjectListWidget QTableView { border: 1px solid @Midlight; - background-color: white; + background-color: @Window; } #CountHeader::section { diff --git a/rascal2/theme.py b/rascal2/theme.py new file mode 100644 index 00000000..8d803497 --- /dev/null +++ b/rascal2/theme.py @@ -0,0 +1,91 @@ +import re +from contextlib import suppress + +from PyQt6 import QtCore, QtGui, QtWidgets + +from rascal2.paths import IMAGES_PATH, STATIC_PATH, path_for + + +def set_stylesheet(app): + """Set the stylesheet of the app according to the given style.css file if available.""" + with suppress(FileNotFoundError), open(STATIC_PATH / "style.css") as stylesheet: + palette = app.palette() + replacements = { + "@Path": IMAGES_PATH.as_posix(), + "@Window": palette.window().color().name(), + "@Highlight": palette.highlight().color().name(), + "@Midlight": palette.midlight().color().name(), + "@Text": palette.text().color().name(), + } + style = re.sub("|".join(replacements), lambda x: replacements[x.group(0)], stylesheet.read()) + app.setStyleSheet(style) + + +class ThemeManager(QtCore.QObject): + """Class to manage the Theme of the UI.""" + + def __init__(self): + super().__init__() + scheme = QtWidgets.QApplication.styleHints().colorScheme() + self.cur_style = "light" if scheme == QtCore.Qt.ColorScheme.Light else "dark" + + def eventFilter(self, obj, event): + """Catch close event for overlay widget.""" + if isinstance(obj, QtWidgets.QApplication) and event.type() == QtCore.QEvent.Type.ApplicationPaletteChange: + scheme = QtWidgets.QApplication.styleHints().colorScheme() + style = "light" if scheme == QtCore.Qt.ColorScheme.Light else "dark" + if style != self.cur_style: + set_stylesheet(obj) + print(type(obj)) + self.cur_style = style + return True + return False + + +THEMES = ThemeManager() + + +class IconEngine(QtGui.QIconEngine): + """Create the icons for the application.""" + + def __init__(self, filename): + super().__init__() + self.name = re.split(r"-dark.png|-light.png", filename)[0] + self.update_icon() + + def update_icon(self): + """Update the Icon.""" + scheme = QtWidgets.QApplication.styleHints().colorScheme() + style = "light" if scheme == QtCore.Qt.ColorScheme.Light else "dark" + + filename = f"{self.name}-light.png" if style == "light" else f"{self.name}-dark.png" + path = path_for(filename) + self.icon = QtGui.QIcon(path) + + def pixmap(self, size, mode, state): + """Create the pixmap. + + :param size: size + :type size: QSize + :param mode: mode + :type mode: QIcon.Mode + :param state: state + :type state: QIcon.State + """ + self.update_icon() + return self.icon.pixmap(size, mode, state) + + def paint(self, painter, rect, mode, state): + """Paint the icon. + + :param painter: painter + :type painter: QPainter + :param rect: rect + :type rect: QRect + :param mode: mode + :type mode: QIcon.Mode + :param state: state + :type state: QIcon.State + """ + self.update_icon() + return self.icon.pixmap.paint(painter, rect, mode, state) diff --git a/rascal2/ui/view.py b/rascal2/ui/view.py index 54ec00a3..6325910b 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -9,6 +9,7 @@ from rascal2.dialogs.startup_dialog import PROJECT_FILES, LoadDialog, LoadR1Dialog, NewProjectDialog, StartupDialog from rascal2.paths import EXAMPLES_PATH, EXAMPLES_TEMP_PATH, path_for from rascal2.settings import MDIGeometries, get_global_settings +from rascal2.theme import IconEngine from rascal2.widgets import ControlsWidget, PlotWidget, TerminalWidget from rascal2.widgets.project import ProjectWidget from rascal2.widgets.startup import StartUpWidget @@ -18,6 +19,24 @@ MAIN_WINDOW_TITLE = "RasCAL-2" +class TitleProxyStyle(QtWidgets.QProxyStyle): + """Set style of Title bar.""" + + def drawComplexControl(self, control, option, painter, widget=None): + if control == QtWidgets.QStyle.ComplexControl.CC_TitleBar: + option.palette.setBrush(QtGui.QPalette.ColorRole.Window, option.palette.button().color()) + super().drawComplexControl(control, option, painter, widget) + + +class MdiSubWindow(QtWidgets.QMdiSubWindow): + """Class to handle MDI sub-windows.""" + + def __init__(self, parent=None, flags=QtCore.Qt.WindowType.Widget): + super().__init__(parent, flags) + style = TitleProxyStyle(self.style()) + self.setStyle(style) + + class MainWindowView(QtWidgets.QMainWindow): """Creates the main view for the RasCAL application.""" @@ -98,13 +117,13 @@ def create_actions(self): """Create the menu and toolbar actions.""" self.new_project_action = QtGui.QAction("&New Project", self) self.new_project_action.setStatusTip("Create a new project") - self.new_project_action.setIcon(QtGui.QIcon(path_for("new-project.png"))) + self.new_project_action.setIcon(QtGui.QIcon(IconEngine("new-project-light.png"))) self.new_project_action.triggered.connect(lambda: self.show_project_dialog(NewProjectDialog)) self.new_project_action.setShortcut(QtGui.QKeySequence.StandardKey.New) self.open_project_action = QtGui.QAction("&Open Project", self) self.open_project_action.setStatusTip("Open an existing project") - self.open_project_action.setIcon(QtGui.QIcon(path_for("browse-dark.png"))) + self.open_project_action.setIcon(QtGui.QIcon(IconEngine("browse-light.png"))) self.open_project_action.triggered.connect(lambda: self.show_project_dialog(LoadDialog)) self.open_project_action.setShortcut(QtGui.QKeySequence.StandardKey.Open) @@ -114,7 +133,7 @@ def create_actions(self): self.save_project_action = QtGui.QAction("&Save", self) self.save_project_action.setStatusTip("Save project") - self.save_project_action.setIcon(QtGui.QIcon(path_for("save-project.png"))) + self.save_project_action.setIcon(QtGui.QIcon(IconEngine("save-project-light.png"))) self.save_project_action.triggered.connect(lambda: self.presenter.save_project()) self.save_project_action.setShortcut(QtGui.QKeySequence.StandardKey.Save) self.save_project_action.setEnabled(False) @@ -122,7 +141,7 @@ def create_actions(self): self.save_as_action = QtGui.QAction("Save To &Folder...", self) self.save_as_action.setStatusTip("Save project to a specified folder.") - self.save_as_action.setIcon(QtGui.QIcon(path_for("save-project.png"))) + self.save_as_action.setIcon(QtGui.QIcon(IconEngine("save-project-light.png"))) self.save_as_action.triggered.connect(lambda: self.presenter.save_project(save_as=True)) self.save_as_action.setShortcut(QtGui.QKeySequence.StandardKey.SaveAs) self.save_as_action.setEnabled(False) @@ -137,12 +156,12 @@ def create_actions(self): self.undo_action = self.undo_stack.createUndoAction(self, "&Undo") self.undo_action.setStatusTip("Undo the last action") - self.undo_action.setIcon(QtGui.QIcon(path_for("undo.png"))) + self.undo_action.setIcon(QtGui.QIcon(IconEngine("undo-light.png"))) self.undo_action.setShortcut(QtGui.QKeySequence.StandardKey.Undo) self.redo_action = self.undo_stack.createRedoAction(self, "&Redo") self.redo_action.setStatusTip("Redo the last undone action") - self.redo_action.setIcon(QtGui.QIcon(path_for("redo.png"))) + self.redo_action.setIcon(QtGui.QIcon(IconEngine("redo-light.png"))) self.redo_action.setShortcut(QtGui.QKeySequence.StandardKey.Redo) self.undo_view_action = QtGui.QAction("Undo &History", self) @@ -159,13 +178,13 @@ def create_actions(self): self.settings_action = QtGui.QAction("Settings", self) self.settings_action.setStatusTip("Settings") - self.settings_action.setIcon(QtGui.QIcon(path_for("settings.png"))) + self.settings_action.setIcon(QtGui.QIcon(IconEngine("settings-light.png"))) self.settings_action.setMenuRole(QtGui.QAction.MenuRole.PreferencesRole) self.settings_action.triggered.connect(lambda: self.show_settings_dialog()) self.open_help_action = QtGui.QAction("&Help", self) self.open_help_action.setStatusTip("Open Documentation") - self.open_help_action.setIcon(QtGui.QIcon(path_for("help.png"))) + self.open_help_action.setIcon(QtGui.QIcon(IconEngine("help-light.png"))) self.open_help_action.triggered.connect(self.open_docs) self.toggle_slider_action = QtGui.QAction("Show &Sliders", self) @@ -191,7 +210,7 @@ def create_actions(self): # Window menu actions self.tile_windows_action = QtGui.QAction("Tile Windows", self) self.tile_windows_action.setStatusTip("Arrange windows in the default grid.") - self.tile_windows_action.setIcon(QtGui.QIcon(path_for("tile.png"))) + self.tile_windows_action.setIcon(QtGui.QIcon(IconEngine("tile-light.png"))) self.tile_windows_action.triggered.connect(self.custom_tile_layout) self.tile_windows_action.setEnabled(False) self.disabled_elements.append(self.tile_windows_action) @@ -284,6 +303,7 @@ def open_docs(self): def create_toolbar(self): """Create the toolbar.""" self.toolbar = self.addToolBar("ToolBar") + self.toolbar.setStyleSheet("QToolButton { padding-left: 5px; padding-right: 5px; padding-top: 10px }") self.toolbar.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.PreventContextMenu) self.toolbar.setMovable(False) @@ -321,6 +341,8 @@ def setup_mdi(self): window = self.mdi.addSubWindow( widget, QtCore.Qt.WindowType.WindowMinMaxButtonsHint | QtCore.Qt.WindowType.WindowTitleHint ) + # window = MdiSubWindow(self.mdi) + # window.setWidget(widget) window.setWindowTitle(title) self.reset_mdi_layout() self.startup_dlg = self.takeCentralWidget() diff --git a/rascal2/widgets/inputs.py b/rascal2/widgets/inputs.py index 6a7eb969..9a5ee3df 100644 --- a/rascal2/widgets/inputs.py +++ b/rascal2/widgets/inputs.py @@ -674,13 +674,13 @@ def __init__(self, parent=None): self.setLayout(layout) self.select_menu = QtWidgets.QMenu() - add_button = QtWidgets.QToolButton(icon=QtGui.QIcon(path_for("create-dark.png"))) + add_button = QtWidgets.QToolButton(icon=QtGui.QIcon(path_for("create-light.png"))) add_button.setMinimumWidth(40) add_button.setSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.MinimumExpanding) add_button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup) add_button.setMenu(self.select_menu) - delete_button = QtWidgets.QToolButton(icon=QtGui.QIcon(path_for("delete-dark.png"))) + delete_button = QtWidgets.QToolButton(icon=QtGui.QIcon(path_for("delete-light.png"))) delete_button.setMinimumWidth(40) delete_button.clicked.connect(self.delete_items) delete_button.setSizePolicy( diff --git a/rascal2/widgets/plot.py b/rascal2/widgets/plot.py index 21948913..65ba9de2 100644 --- a/rascal2/widgets/plot.py +++ b/rascal2/widgets/plot.py @@ -9,7 +9,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from rascal2.config import SETTINGS -from rascal2.paths import path_for +from rascal2.theme import IconEngine from rascal2.widgets.inputs import MultiSelectComboBox, ProgressButton @@ -311,7 +311,7 @@ def make_interaction_layout(self): self.toolbar.hide() reset_button = QtWidgets.QToolButton(objectName="InteractButton") reset_button.setToolTip("Reset plot") - reset_button.setIcon(QtGui.QIcon(path_for("refresh.png"))) + reset_button.setIcon(QtGui.QIcon(IconEngine("refresh-dark.png"))) reset_button.clicked.connect(lambda: self.toolbar.home()) pan_button = QtWidgets.QToolButton(objectName="InteractButton") pan_button.setDefaultAction(self.toolbar._actions["pan"]) @@ -330,9 +330,9 @@ def toggle_settings(self, toggled_on: bool): """Toggles the visibility of the plot controls.""" self.plot_controls.setVisible(toggled_on) if toggled_on: - self.toggle_button.setIcon(QtGui.QIcon(path_for("hide-settings.png"))) + self.toggle_button.setIcon(QtGui.QIcon(IconEngine("hide-settings-light.png"))) else: - self.toggle_button.setIcon(QtGui.QIcon(path_for("settings.png"))) + self.toggle_button.setIcon(QtGui.QIcon(IconEngine("settings-light.png"))) @abstractmethod def make_control_layout(self) -> QtWidgets.QLayout: @@ -390,6 +390,16 @@ def export(self): dpi = self.figure.dpi if sx > 1920 else 1920 // self.figure.get_figwidth() self.figure.savefig(filepath, facecolor=SETTINGS.export_background_colour, dpi=dpi) + def changeEvent(self, event): + if event.type() == QtCore.QEvent.Type.PaletteChange: + scheme = QtWidgets.QApplication.styleHints().colorScheme() + if scheme == QtCore.Qt.ColorScheme.Light: + matplotlib.style.use("default") + else: + matplotlib.style.use("dark_background") + + super().changeEvent(event) + class RefSLDWidget(AbstractPlotWidget): """Creates a UI for displaying the path lengths from the simulation result.""" diff --git a/rascal2/widgets/project/lists.py b/rascal2/widgets/project/lists.py index a39b3914..f6181e61 100644 --- a/rascal2/widgets/project/lists.py +++ b/rascal2/widgets/project/lists.py @@ -11,7 +11,7 @@ from rascal2.config import SETTINGS from rascal2.core.readers import readers -from rascal2.paths import path_for +from rascal2.theme import IconEngine from rascal2.widgets.delegates import ProjectFieldDelegate from rascal2.widgets.inputs import RangeWidget @@ -135,12 +135,12 @@ def __init__(self, field: str, parent): button_layout = QtWidgets.QHBoxLayout() button_layout.setContentsMargins(0, 0, 0, 0) button_layout.addStretch(1) - self.add_button = QtWidgets.QToolButton(icon=QtGui.QIcon(path_for("create-dark.png"))) + self.add_button = QtWidgets.QToolButton(icon=QtGui.QIcon(IconEngine("create-light.png"))) self.add_button.setHidden(True) self.add_button.pressed.connect(self.append_item) button_layout.addWidget(self.add_button) - self.delete_button = QtWidgets.QToolButton(icon=QtGui.QIcon(path_for("delete-dark.png"))) + self.delete_button = QtWidgets.QToolButton(icon=QtGui.QIcon(IconEngine("delete-light.png"))) self.delete_button.setHidden(True) self.delete_button.pressed.connect(self.delete_item) button_layout.addWidget(self.delete_button) @@ -379,14 +379,14 @@ def setup_buttons(self): model_type = "domain contrast" if self.domains else "layer" self.add_button = QtWidgets.QPushButton( - f"Add {model_type.title()}", icon=QtGui.QIcon(path_for("create-dark.png")) + f"Add {model_type.title()}", icon=QtGui.QIcon(IconEngine("create-light.png")) ) self.add_button.setToolTip( f"Add a {model_type.title()} after the currently selected {model_type.title()} (Shift+Enter)" ) self.delete_button = QtWidgets.QPushButton( - f"Delete {model_type.title()}", icon=QtGui.QIcon(path_for("delete-dark.png")) + f"Delete {model_type.title()}", icon=QtGui.QIcon(IconEngine("delete-light.png")) ) self.delete_button.setToolTip(f"Delete the currently selected {model_type.title()} (Del)") diff --git a/rascal2/widgets/project/project.py b/rascal2/widgets/project/project.py index bdd2743a..5600b260 100644 --- a/rascal2/widgets/project/project.py +++ b/rascal2/widgets/project/project.py @@ -11,7 +11,7 @@ from ratapi.utils.custom_errors import custom_pydantic_validation_error from ratapi.utils.enums import Calculations, Geometries, LayerModels -from rascal2.paths import path_for +from rascal2.theme import IconEngine from rascal2.widgets.project.lists import ContrastWidget, DataWidget from rascal2.widgets.project.slider_view import SliderViewWidget from rascal2.widgets.project.tables import ( @@ -119,7 +119,7 @@ def create_project_view(self) -> QtWidgets.QWidget: show_sliders_button = QtWidgets.QPushButton("Show sliders") show_sliders_button.clicked.connect(self.parent.toggle_sliders) - self.edit_project_button = QtWidgets.QPushButton("Edit Project", icon=QtGui.QIcon(path_for("edit.png"))) + self.edit_project_button = QtWidgets.QPushButton("Edit Project", icon=QtGui.QIcon(IconEngine("edit-light.png"))) self.edit_project_button.clicked.connect(self.show_edit_view) button_layout = QtWidgets.QHBoxLayout() button_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight) @@ -169,11 +169,11 @@ def create_edit_view(self) -> QtWidgets.QWidget: main_layout.setSpacing(20) self.save_project_button = QtWidgets.QPushButton( - "Accept Changes", icon=QtGui.QIcon(path_for("save-project.png")) + "Accept Changes", icon=QtGui.QIcon(IconEngine("save-project-light.png")) ) self.save_project_button.clicked.connect(self.save_changes) - self.cancel_button = QtWidgets.QPushButton("Cancel", icon=QtGui.QIcon(path_for("cancel-dark.png"))) + self.cancel_button = QtWidgets.QPushButton("Cancel", icon=QtGui.QIcon(IconEngine("cancel-light.png"))) self.cancel_button.clicked.connect(self.show_project_view) buttons_layout = QtWidgets.QHBoxLayout() diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index 45eb7fd2..b765af7c 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -16,7 +16,7 @@ from rascal2.config import LOGGER, SETTINGS from rascal2.core.enums import CustomFileType from rascal2.dialogs.custom_file_editor import create_new_file, edit_file -from rascal2.paths import path_for +from rascal2.theme import IconEngine class ClassListTableModel(QtCore.QAbstractTableModel): @@ -363,7 +363,7 @@ def make_delete_button(self, index): The row to be deleted. """ - button = QtWidgets.QPushButton(icon=QtGui.QIcon(path_for("delete-dark.png"))) + button = QtWidgets.QPushButton(icon=QtGui.QIcon(IconEngine("delete-light.png"))) button.resize(button.sizeHint().width(), button.sizeHint().width()) button.pressed.connect(lambda: self.delete_item(index)) diff --git a/rascal2/widgets/startup.py b/rascal2/widgets/startup.py index 5185edd6..2845d4ef 100644 --- a/rascal2/widgets/startup.py +++ b/rascal2/widgets/startup.py @@ -50,11 +50,11 @@ def add_widgets_to_layout(self) -> None: def create_banner_and_footer(self) -> None: """Create banner and footer.""" self.banner_label = QtWidgets.QLabel() - self.banner_label.setPixmap(QtGui.QPixmap(path_for("banner.png"))) + self.banner_label.setPixmap(QtGui.QPixmap(path_for("banner-light.png"))) self.banner_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.footer_label = QtWidgets.QLabel() - self.footer_label.setPixmap(QtGui.QPixmap(path_for("footer.png"))) + self.footer_label.setPixmap(QtGui.QPixmap(path_for("footer-light.png"))) self.footer_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) def create_buttons(self) -> None: @@ -78,3 +78,15 @@ def create_labels(self) -> None: self.import_r1_label = QtWidgets.QLabel("Import RasCAL-1\nProject") self.import_r1_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + def changeEvent(self, event): + if event.type() == QtCore.QEvent.Type.PaletteChange: + scheme = QtWidgets.QApplication.styleHints().colorScheme() + if scheme == QtCore.Qt.ColorScheme.Dark: + self.banner_label.setPixmap(QtGui.QPixmap(path_for("banner-dark.png"))) + self.footer_label.setPixmap(QtGui.QPixmap(path_for("footer-dark.png"))) + else: + self.banner_label.setPixmap(QtGui.QPixmap(path_for("banner-light.png"))) + self.footer_label.setPixmap(QtGui.QPixmap(path_for("footer-light.png"))) + + super().changeEvent(event) diff --git a/requirements.txt b/requirements.txt index 652a092c..b3017b86 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PyInstaller==6.9.0 -PyQt6==6.7.1 -PyQt6-Qt6==6.7.3 +PyQt6==6.8.1 +PyQt6-Qt6==6.8.2 ratapi==0.0.0.dev14 pydantic==2.8.2 PyQt6-QScintilla==2.14.1