From 0411694265d253b4c40ca7eba59e50ef82426152 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Tue, 9 Jun 2026 16:38:04 +0100 Subject: [PATCH 1/5] downgrade pyqt to 6.9.1 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index f1d6e9f2..7278efb5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ PyInstaller==6.9.0 -PyQt6==6.10.2 -PyQt6-Qt6==6.10.2 +PyQt6==6.9.1 +PyQt6-Qt6==6.9.2 ratapi==0.0.0.dev14 pydantic==2.8.2 PyQt6-QScintilla==2.14.1 From 8cc48b23654b56188258754875a1fd0d7f6d3cb5 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Wed, 10 Jun 2026 14:11:43 +0100 Subject: [PATCH 2/5] Rework themes to work on Linux with pyqt 6.9.1 --- rascal2/app.py | 6 ++-- rascal2/settings.py | 29 +++++++-------- rascal2/theme.py | 72 +++++++++++++++++++++++++------------- rascal2/ui/view.py | 20 ----------- rascal2/widgets/plot.py | 8 +++-- rascal2/widgets/startup.py | 3 +- 6 files changed, 70 insertions(+), 68 deletions(-) diff --git a/rascal2/app.py b/rascal2/app.py index 68836ae7..b67b9d30 100644 --- a/rascal2/app.py +++ b/rascal2/app.py @@ -3,8 +3,8 @@ from PyQt6 import QtWidgets -from rascal2.config import MatlabHelper, handle_scaling, setup_logging -from rascal2.settings import get_colour_scheme +from rascal2.config import SETTINGS, MatlabHelper, handle_scaling, setup_logging +from rascal2.settings import change_ui_style from rascal2.theme import THEMES, set_stylesheet from rascal2.ui.view import MainWindowView @@ -21,7 +21,7 @@ def ui_execute(splash): app = QtWidgets.QApplication.instance() app.setStyle("Fusion") app.installEventFilter(THEMES) - app.styleHints().setColorScheme(get_colour_scheme()) + change_ui_style(SETTINGS.style) set_stylesheet(app) window = MainWindowView() diff --git a/rascal2/settings.py b/rascal2/settings.py index 5456a581..5c9cdb7d 100644 --- a/rascal2/settings.py +++ b/rascal2/settings.py @@ -229,27 +229,22 @@ def update_recent_projects(path: str | None = None) -> list[str]: 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) -> None: + """Change the style of the app GUI to the given style. - -def change_ui_style(style: Styles = Styles.System) -> None: - """Change the style of the app GUI to the given style.""" + Parameters + ---------- + style : Styles + The style to change to. + """ 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) + case _: + colour_scheme = QtCore.Qt.ColorScheme.Unknown + style_hints = app.styleHints() + style_hints.setProperty("colour_scheme", colour_scheme) + style_hints.setColorScheme(colour_scheme) diff --git a/rascal2/theme.py b/rascal2/theme.py index 0eaf6976..0dc2519e 100644 --- a/rascal2/theme.py +++ b/rascal2/theme.py @@ -6,9 +6,43 @@ from rascal2.paths import IMAGES_PATH, STATIC_PATH, path_for +def get_correct_qt_color_scheme(): + """Get the correct colour scheme.""" + # On linux PyQt 6.9.1 sometimes returns incorrect scheme. + # A workaround is to store the scheme to ensure it is correct. + # This issue is fixed in PyQt 6.10 but 6.10 is not available on IDAaas + # due to older GLIBC, so we are stuck on 6.9.1 for now. + app = QtWidgets.QApplication.instance() + scheme = app.styleHints().property("colour_scheme") + if scheme is None: + return QtCore.Qt.ColorScheme.Light + elif scheme == QtCore.Qt.ColorScheme.Unknown: + return app.styleHints().colorScheme() + return scheme + + +def colorize_icon(icon, colour): + """Change icon colour to given colour. + + Parameters + ---------- + icon : QtGui.QIcon + The icon to colourize + colour : QtGui.QColor + The new colour of the icon + """ + pixmap = icon.pixmap(200, 200) + mask = pixmap.createMaskFromColor(QtGui.QColor(QtCore.Qt.GlobalColor.transparent), QtCore.Qt.MaskMode.MaskInColor) + pixmap.fill(colour) + pixmap.setMask(mask) + + return QtGui.QIcon(pixmap) + + 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: + app.setStyleSheet("* {}") # This is a hack to force PyQt 6.9.1 to update the palette on Linux palette = app.palette() replacements = { "@Path": IMAGES_PATH.as_posix(), @@ -26,13 +60,13 @@ class ThemeManager(QtCore.QObject): def __init__(self): super().__init__() - scheme = QtWidgets.QApplication.styleHints().colorScheme() + scheme = get_correct_qt_color_scheme() 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() + scheme = get_correct_qt_color_scheme() style = "light" if scheme == QtCore.Qt.ColorScheme.Light else "dark" if style != self.cur_style: set_stylesheet(obj) @@ -45,7 +79,13 @@ def eventFilter(self, obj, event): class IconEngine(QtGui.QIconEngine): - """Create the icons for the application.""" + """Load the appropriate icon for the application theme. + + Parameters + ---------- + filename : str + The path of the icon + """ def __init__(self, filename): super().__init__() @@ -53,8 +93,8 @@ def __init__(self, filename): self.update_icon() def update_icon(self): - """Update the Icon.""" - scheme = QtWidgets.QApplication.styleHints().colorScheme() + """Update the icon to match the current theme.""" + scheme = get_correct_qt_color_scheme() 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" @@ -62,29 +102,11 @@ def update_icon(self): 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 - """ + """Create the pixmap.""" 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 - """ + """Paint the icon.""" 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 6325910b..cd7e19f3 100644 --- a/rascal2/ui/view.py +++ b/rascal2/ui/view.py @@ -19,24 +19,6 @@ 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.""" @@ -341,8 +323,6 @@ 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/plot.py b/rascal2/widgets/plot.py index 65ba9de2..c1f2d98e 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.theme import IconEngine +from rascal2.theme import IconEngine, colorize_icon, get_correct_qt_color_scheme from rascal2.widgets.inputs import MultiSelectComboBox, ProgressButton @@ -392,11 +392,15 @@ def export(self): def changeEvent(self, event): if event.type() == QtCore.QEvent.Type.PaletteChange: - scheme = QtWidgets.QApplication.styleHints().colorScheme() + scheme = get_correct_qt_color_scheme() if scheme == QtCore.Qt.ColorScheme.Light: matplotlib.style.use("default") else: matplotlib.style.use("dark_background") + for name in ["pan", "zoom"]: + icon_color = self.palette().text().color() + icon = self.toolbar._actions[name].icon() + self.toolbar._actions[name].setIcon(colorize_icon(icon, icon_color)) super().changeEvent(event) diff --git a/rascal2/widgets/startup.py b/rascal2/widgets/startup.py index 2845d4ef..c7ca9a04 100644 --- a/rascal2/widgets/startup.py +++ b/rascal2/widgets/startup.py @@ -2,6 +2,7 @@ from rascal2.dialogs.startup_dialog import LoadDialog, LoadR1Dialog, NewProjectDialog from rascal2.paths import path_for +from rascal2.theme import get_correct_qt_color_scheme class StartUpWidget(QtWidgets.QWidget): @@ -81,7 +82,7 @@ def create_labels(self) -> None: def changeEvent(self, event): if event.type() == QtCore.QEvent.Type.PaletteChange: - scheme = QtWidgets.QApplication.styleHints().colorScheme() + scheme = get_correct_qt_color_scheme() 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"))) From 14bff1a0a70e3b51cbc6f8e6b87d2b6fe7d91d18 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Thu, 11 Jun 2026 14:58:43 +0100 Subject: [PATCH 3/5] Add name for tableview button style --- rascal2/static/style.css | 6 ++++++ rascal2/widgets/project/tables.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rascal2/static/style.css b/rascal2/static/style.css index 37f492b9..da33b6f1 100644 --- a/rascal2/static/style.css +++ b/rascal2/static/style.css @@ -382,6 +382,12 @@ QTableView QPushButton { border: transparent; } +#TableButton { + border: transparent; + padding:0; + margin:0; + } + /***************************** QMenuBar Styles *****************************/ diff --git a/rascal2/widgets/project/tables.py b/rascal2/widgets/project/tables.py index b765af7c..3c7cee2e 100644 --- a/rascal2/widgets/project/tables.py +++ b/rascal2/widgets/project/tables.py @@ -363,7 +363,8 @@ def make_delete_button(self, index): The row to be deleted. """ - button = QtWidgets.QPushButton(icon=QtGui.QIcon(IconEngine("delete-light.png"))) + # The QTableView button style stopped working in PyQT 6.9, added name as workaround. + button = QtWidgets.QPushButton(icon=QtGui.QIcon(IconEngine("delete-light.png")), objectName="TableButton") button.resize(button.sizeHint().width(), button.sizeHint().width()) button.pressed.connect(lambda: self.delete_item(index)) From 72f56491faa4d7f281f9033541d154655e3ef8fc Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Tue, 16 Jun 2026 16:08:26 +0100 Subject: [PATCH 4/5] Address review comments --- rascal2/widgets/plot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rascal2/widgets/plot.py b/rascal2/widgets/plot.py index c1f2d98e..555d6f57 100644 --- a/rascal2/widgets/plot.py +++ b/rascal2/widgets/plot.py @@ -213,6 +213,7 @@ def __init__(self, parent): super().__init__(parent) self.parent = parent + self.toolbar = None self.current_plot_data = None main_layout = QtWidgets.QHBoxLayout() @@ -391,7 +392,7 @@ def export(self): self.figure.savefig(filepath, facecolor=SETTINGS.export_background_colour, dpi=dpi) def changeEvent(self, event): - if event.type() == QtCore.QEvent.Type.PaletteChange: + if self.toolbar is not None and event.type() == QtCore.QEvent.Type.PaletteChange: scheme = get_correct_qt_color_scheme() if scheme == QtCore.Qt.ColorScheme.Light: matplotlib.style.use("default") From 803981f4b8b0863530de7687ed79252dbbcda5d6 Mon Sep 17 00:00:00 2001 From: Stephen Nneji Date: Fri, 19 Jun 2026 14:13:13 +0100 Subject: [PATCH 5/5] Update ratapi to dev 15 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7278efb5..88aabcfd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ PyInstaller==6.9.0 PyQt6==6.9.1 PyQt6-Qt6==6.9.2 -ratapi==0.0.0.dev14 +ratapi==0.0.0.dev15 pydantic==2.8.2 PyQt6-QScintilla==2.14.1 nexusformat==1.0.7