diff --git a/.gitignore b/.gitignore index 0b9d4c1d..90558d7f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ docs/build/* docs/source/_build/* src/idvc/version.py build/* +__pycache__/ diff --git a/CHANGES.md b/CHANGES.md index f3303dd9..795ab282 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,14 @@ # ChangeLog +## v26.0.0 + +Enhancements: + * use QtPy abstraction layer + * enables use of Qt6 + * removes legacy qdarkstyle + * cancels DVC run correctly (probably due to Qt6 changes) + + ## v25.0.0 Enhancements: diff --git a/recipe/dev_environment.yml b/recipe/dev_environment.yml index 53242c25..af1ce183 100644 --- a/recipe/dev_environment.yml +++ b/recipe/dev_environment.yml @@ -1,20 +1,33 @@ -name: idvc_dev +name: idvc_qt6 channels: - conda-forge - ccpi dependencies: - - eqt >=1.0.1 - - openpyxl - - python - - numpy - - pandas - - pyside2 - - scipy - - ccpi::ccpi-viewer >=25.0.0 - - ccpi::ccpi-dvc >=22.0.0 - - natsort - - docopt - - matplotlib - - openmp # [osx] - - qdarkstyle - - vtk + - python =3.11 + - pip + - numpy #=2.4.6 + - scipy #=1.17.1 + - pandas #=3.1.0 + - matplotlib-base >3.8 #=3.11.2 + - h5py #=3.16.0 + - openpyxl + - pyyaml + - schema + - natsort + - docopt + - filetype + - qtpy #=2.4.3 + - pyside6 #=6.11.2 + - llvm-openmp + - ccpi::ccpi-dvc =25.0.0 + - pip: + # Kitware wheels (not conda-forge vtk) to avoid Qt5/Qt6 conflicts on macOS. + # 9.7.x crashes with this app; 9.4.2 works. + - --extra-index-url https://wheels.vtk.org + - vtk==9.4.2 + - git+https://github.com/TomographicImaging/eqt.git@remove-qdarkstyle + - git+https://github.com/TomographicImaging/CILViewer.git@macos-fix#subdirectory=Wrappers/Python + # dev tools + - pre-commit + - pysnooper + diff --git a/recipe/meta.yaml b/recipe/meta.yaml index 49a3b47a..2658173e 100644 --- a/recipe/meta.yaml +++ b/recipe/meta.yaml @@ -39,15 +39,15 @@ requirements: - python - numpy - pandas - - pyside2 - scipy - ccpi-viewer >=25.0.0 - ccpi-dvc >=22.0.0 - natsort - docopt - matplotlib - - openmp # [osx] - - qdarkstyle + - llvm-openmp + - qtpy + - pyside2 about: home: http://www.ccpi.ac.uk diff --git a/setup.py b/setup.py index b8c538bc..deb23970 100644 --- a/setup.py +++ b/setup.py @@ -68,7 +68,6 @@ def version2pep440(version): "docopt", "matplotlib", # "openmp; platform_system=='Darwin'", - "qdarkstyle", "vtk", "openpyxl", ], diff --git a/src/idvc/dvc_interface.py b/src/idvc/dvc_interface.py index 0c5c9e8d..906c1e9e 100644 --- a/src/idvc/dvc_interface.py +++ b/src/idvc/dvc_interface.py @@ -16,11 +16,11 @@ import os from openpyxl import load_workbook import sys -from PySide2 import QtCore, QtGui, QtWidgets -from PySide2.QtCore import (QByteArray, QRegExp, QSettings, QSize, Qt, +from qtpy import QtCore, QtGui, QtWidgets +from qtpy.QtCore import (QByteArray, QRegularExpression, QSettings, QSize, Qt, QThreadPool) -from PySide2.QtGui import QCloseEvent, QKeySequence, QRegExpValidator -from PySide2.QtWidgets import (QAction, QCheckBox, QComboBox, +from qtpy.QtGui import QCloseEvent, QKeySequence, QRegularExpressionValidator +from qtpy.QtWidgets import (QAction, QCheckBox, QComboBox, QDockWidget, QDoubleSpinBox, QFileDialog, QFormLayout, QFrame, QGroupBox, QLabel, QLineEdit, @@ -82,9 +82,6 @@ from eqt.ui import FormDialog -import qdarkstyle -from qdarkstyle.dark.palette import DarkPalette -from qdarkstyle.light.palette import LightPalette from idvc import version as gui_version @@ -104,7 +101,8 @@ allowed_point_cloud_file_formats = ('.roi', '.txt', '.csv', '.xlsx', '.inp') class MainWindow(QMainWindow): - def __init__(self): + def __init__(self, logger=None): + self.logger = logger if logger is not None else logging.getLogger(__name__) """Creates the menu bar: File, Settings, Help.""" QMainWindow.__init__(self) @@ -180,7 +178,7 @@ def __init__(self): self.file_menu.addAction(exit_action) # # Window dimensions - geometry = qApp.desktop().availableGeometry(self) + geometry = QtWidgets.QApplication.primaryScreen().availableGeometry() border = 50 self.setGeometry(border, border, geometry.width()-2*border, geometry.height()-2*border) @@ -219,13 +217,9 @@ def createPopupMenu(self): return QtWidgets.QMenu(self) # Create a new menu def SetAppStyle(self): - if self.settings.value("dark_mode") is None: - self.settings.setValue("dark_mode", True) - if self.settings.value("dark_mode") == "true": - style = qdarkstyle.load_stylesheet(palette=DarkPalette) - else: - style = qdarkstyle.load_stylesheet(palette=LightPalette) - self.setStyleSheet(style) + # No stylesheet is applied: the application follows the system theme. + # The qdarkstyle stylesheet hid the VTK viewers with Qt6 on macOS. + pass #Setting up the session: @@ -417,7 +411,6 @@ def CreateViewerSettingsPanel(self): self.visualisation_setting_widgets = vs_widgets scroll_area = dockWidget.widget() - scroll_area.apply_qdarkstyle_to_buttons(self.viewer_settings_panel[1]) def updateCoordinates(self): viewers_2D = [self.vis_widget_2D.frame.viewer] @@ -640,7 +633,6 @@ def CreateSelectImagePanel(self): self.addDockWidget(QtCore.Qt.LeftDockWidgetArea,dockWidget) scroll_area = dockWidget.widget() - scroll_area.apply_qdarkstyle_to_buttons(self.select_image_panel[1]) self.si_widgets = si_widgets @@ -792,7 +784,7 @@ def displayFileErrorDialog(self, message, title, action_button=None): pass if action_button is not None: msg.addButton(action_button, msg.ActionRole) - msg.exec_() + msg.exec() def view_image(self): """Called when the view image button is clicked in the first tab, or @@ -973,6 +965,7 @@ def visualise(self): self.select_image_dock.raise_() def create_progress_window(self, title, text, max = 100, cancel = None): + self.logger.debug("Creating progress window with title: '{}' and text: '{}'".format(title, text)) self.progress_window = QProgressDialog(text, "Cancel", 0,max, self, QtCore.Qt.Dialog) self.progress_window.setWindowTitle(title) @@ -981,8 +974,10 @@ def create_progress_window(self, title, text, max = 100, cancel = None): self.progress_window.setWindowFlag(QtCore.Qt.WindowCloseButtonHint, True) self.progress_window.setWindowFlag(QtCore.Qt.WindowMaximizeButtonHint, False) self.progress_window.setAutoClose(True) + self.logger.debug(f"Cancel callback is {'set' if cancel is not None else 'not set'}") if cancel is None: self.progress_window.setCancelButton(None) + else: self.progress_window.canceled.connect(cancel) @@ -1313,7 +1308,6 @@ def CreateRegistrationPanel(self): self.registration_parameters = rp scroll_area = dockWidget.widget() - scroll_area.apply_qdarkstyle_to_buttons(self.registration_panel[1]) def createRegistrationViewer(self): # print("Create reg viewer") @@ -2355,7 +2349,6 @@ def CreateMaskPanel(self): self.addDockWidget(QtCore.Qt.LeftDockWidgetArea, dockWidget) scroll_area = dockWidget.widget() - scroll_area.apply_qdarkstyle_to_buttons(self.mask_panel[1]) def warnIfUnchecking(self): if not self.mask_parameters['extendMaskCheck'].isChecked() and self.mask_parameters['extendMaskCheck'].isEnabled(): @@ -3109,7 +3102,6 @@ def CreatePointCloudPanel(self): self.graphWidgetFL.setWidget(widgetno, QFormLayout.FieldRole, pc['pc_points_value']) - scroll_area_point_cloud.apply_qdarkstyle_to_buttons(self.dockWidget) def _generatePointCloudClicked(self): self.pointcloud_is = 'generated' @@ -4267,8 +4259,8 @@ def CreateRunDVCPanel(self): formLayout.setWidget(widgetno, QFormLayout.LabelRole, rdvc_widgets['name_label']) rdvc_widgets['name_entry'] = QLineEdit(self) - rx = QRegExp("[A-Za-z0-9]+") - validator = QRegExpValidator(rx, rdvc_widgets['name_entry']) #need to check this + rx = QRegularExpression("[A-Za-z0-9]+") + validator = QRegularExpressionValidator(rx, rdvc_widgets['name_entry']) #need to check this rdvc_widgets['name_entry'].setValidator(validator) formLayout.setWidget(widgetno, QFormLayout.FieldRole, rdvc_widgets['name_entry']) widgetno += 1 @@ -4545,7 +4537,6 @@ def CreateRunDVCPanel(self): self.rdvc_widgets = rdvc_widgets scroll_area = dockWidget.widget() - scroll_area.apply_qdarkstyle_to_buttons(self.run_dvc_panel[1]) def _set_num_points_in_run_to_all(self): if hasattr(self, 'pc_no_points'): @@ -5005,8 +4996,7 @@ def CreateViewDVCResultsPanel(self): self.result_widgets = result_widgets scroll_area = dockWidget.widget() - scroll_area.apply_qdarkstyle_to_buttons(self.dvc_results_panel[1]) - + def show_run_pcs(self): #show pointcloud files in list self.result_widgets['pc_entry'].clear() @@ -5120,8 +5110,8 @@ def CreateSaveWindow(self, cancel_text, event): qlabel.setText("Save session as:") qwidget = QtWidgets.QLineEdit(dialog.groupBox) qwidget.setClearButtonEnabled(True) - rx = QRegExp("[A-Za-z0-9]+") - validator = QRegExpValidator(rx, dialog) #need to check this + rx = QRegularExpression("[A-Za-z0-9]+") + validator = QRegularExpressionValidator(rx, dialog) #need to check this qwidget.setValidator(validator) # finally add to the form widget dialog.addWidget(qwidget, qlabel, 'session_name') @@ -5530,7 +5520,7 @@ def CreateSessionSelector(self, stage): dialog.Cancel.clicked.connect(self.load_session_new) self.SessionSelectionWindow = dialog # Try to centre the load session window - # from PySide2.QtGui import QScreen + # from qtpy.QtGui import QScreen # geom = QScreen().availableGeometry() # print (geom) # centrex = geom.topRight().x() + geom.topLeft().x() @@ -5896,7 +5886,7 @@ def warningDialog(self, message='', window_title='', detailed_text=''): dialog.setWindowTitle(window_title) dialog.setDetailedText(detailed_text) dialog.setStandardButtons(QMessageBox.Ok) - retval = dialog.exec_() + retval = dialog.exec() return retval # Loading and Error windows: diff --git a/src/idvc/dvc_runner.py b/src/idvc/dvc_runner.py index 386c2a4f..e7370b4a 100644 --- a/src/idvc/dvc_runner.py +++ b/src/idvc/dvc_runner.py @@ -15,15 +15,18 @@ import os import numpy as np -from PySide2 import QtCore +from qtpy import QtCore from datetime import datetime -from PySide2.QtWidgets import QMessageBox +from qtpy.QtWidgets import QMessageBox import json import time import shutil import platform from .io import save_tiff_stack_as_raw, save_nxs_as_raw +import logging +logger = logging.getLogger(__name__) + class PrintCallback(object): '''Class to handle the emit call when no callback is provided''' def emit(self, *args, **kwargs): @@ -155,7 +158,7 @@ def displayFileErrorDialog(main_window, message, title): msg.setWindowTitle(title) msg.setText(message) #msg.setDetailedText(main_window.e.ErrorMessage()) - msg.exec_() + msg.exec() def cancel_run(main_window, process, run_succeeded): @@ -442,6 +445,7 @@ def set_up(self, *args, **kwargs): progress_callback.emit(100) def run_dvc(self, **kwargs): + logger.debug("Starting DVC run") main_window = self.main_window input_file = self.input_file finish_fn = self.finish_fn @@ -474,6 +478,7 @@ def run_dvc(self, **kwargs): process.finished.connect(self.finished_run) process.started.connect(self.onStarted) if self.process_num == 0: + logger.debug("Creating progress window for the first process") main_window.create_progress_window("Running", "Running DVC code {}/{}".format(self.process_num +1, len(self.processes)), 100, #lambda: cancel_run(main_window, process, run_succeeded) @@ -494,18 +499,19 @@ def onStarted(self): pass def onCancel(self, process): + logger.debug("Cancelling run for process with state: {}".format(process.state())) main_window = self.main_window run_succeeded = self.run_succeeded state = process.state() # print ("Process state", state) - if state in [2,1]: - # print ("Cancelling run") + if state == QtCore.QProcess.ProcessState.Running: + logger.debug ("Cancelling run") process.kill() main_window.alert = QMessageBox(QMessageBox.NoIcon,"Cancelled","The run was cancelled.", QMessageBox.Ok) main_window.alert.show() self.run_succeeded = False - elif state == 0: - print ("all OK, all processes ended") + else: + logger.info (f"Processes state: {state}") def finished_run(self, exitCode, exitStatus): main_window = self.main_window @@ -518,11 +524,14 @@ def finished_run(self, exitCode, exitStatus): .format(self.process_num + 1, required_runs, len(self.processes), exitCode, exitStatus)) - if exitStatus == 0: + logger.debug("Finished process {} with exit code {} and status {}".format(self.process_num + + 1, exitCode, exitStatus)) + logger.debug("Pre self.run_succeeded: {}".format(self.run_succeeded)) + if exitCode == 0: self.run_succeeded = self.run_succeeded and True else: self.run_succeeded = self.run_succeeded and False - + logger.debug("Post self.run_succeeded: {}".format(self.run_succeeded)) if self.process_num == required_runs-1: main_window.progress_window.close() if self.run_succeeded: diff --git a/src/idvc/idvc.py b/src/idvc/idvc.py index ee9f168c..51d55522 100644 --- a/src/idvc/idvc.py +++ b/src/idvc/idvc.py @@ -1,20 +1,20 @@ -from PySide2 import QtWidgets, QtGui +from qtpy import QtWidgets, QtGui import os, sys import logging import argparse - +logger = logging.getLogger(__name__) def main(): parser = argparse.ArgumentParser(description='iDVC - Digital Volume Correlation Software') - parser.add_argument('--debug', type=str) + parser.add_argument('--log', type=str) args = parser.parse_args() - if args.debug in ['debug', 'info', 'warning', 'error', 'critical']: - level = eval(f'logging.{args.debug.upper()}') + if args.log in ['debug', 'info', 'warning', 'error', 'critical']: + level = eval(f'logging.{args.log.upper()}') logging.basicConfig(level=level) - logging.info(f"iDVC: Setting debugging level to {args.debug.upper()}") + logger.info(f"iDVC: Setting debugging level to {args.log.upper()}") app = QtWidgets.QApplication([]) # Set a global font for the application default_font_family = app.font().family() @@ -35,14 +35,14 @@ def main(): vtk.vtkOutputWindow.SetInstance(err) from idvc.dvc_interface import MainWindow - window = MainWindow() + window = MainWindow(logger) window.show() splash.finish(window) - sys.exit(app.exec_()) + sys.exit(app.exec()) if __name__ == "__main__": main() diff --git a/src/idvc/io.py b/src/idvc/io.py index 92aa4158..30e01cd5 100644 --- a/src/idvc/io.py +++ b/src/idvc/io.py @@ -36,9 +36,9 @@ Converter) from ccpi.viewer.ui.dialogs import RawInputDialog, HDF5InputDialog from eqt.threading import Worker -from PySide2 import QtCore, QtGui -from PySide2.QtCore import QThreadPool -from PySide2.QtWidgets import (QComboBox, QDialog, QDialogButtonBox, +from qtpy import QtCore, QtGui +from qtpy.QtCore import QThreadPool +from qtpy.QtWidgets import (QComboBox, QDialog, QDialogButtonBox, QFormLayout, QGroupBox, QLabel, QLineEdit, QMessageBox, QProgressDialog, QVBoxLayout, QWidget) @@ -397,7 +397,7 @@ def displayErrorDialogFromWorker(main_window, error): msg.setWindowTitle(title) msg.setText(message) msg.setDetailedText(detailed_message) - msg.exec_() + msg.exec() def warningDialog(main_window, message='', window_title='', detailed_text=''): dialog = QMessageBox(main_window) @@ -406,7 +406,7 @@ def warningDialog(main_window, message='', window_title='', detailed_text=''): dialog.setWindowTitle(window_title) dialog.setDetailedText(detailed_text) dialog.setStandardButtons(QMessageBox.Ok) - retval = dialog.exec_() + retval = dialog.exec() return retval # Load images: diff --git a/src/idvc/ui/dialogs.py b/src/idvc/ui/dialogs.py index a4271cd8..44ba9604 100644 --- a/src/idvc/ui/dialogs.py +++ b/src/idvc/ui/dialogs.py @@ -1,7 +1,7 @@ -import PySide2 -from PySide2.QtWidgets import * -from PySide2.QtCore import * -from PySide2.QtGui import * +import qtpy +from qtpy.QtWidgets import * +from qtpy.QtCore import * +from qtpy.QtGui import * import multiprocessing import vtk @@ -24,18 +24,6 @@ def __init__(self, parent, title="Settings"): self.fontsize_widget.setSingleStep(1) self.fontsize_widget.setValue(12) self.addWidget(self.fontsize_widget, self.fontsize_label, 'fontsize') - self.dark_checkbox = QCheckBox("Dark Mode") - # populate from settings - if self.parent.settings.value("dark_mode") is not None: - if self.parent.settings.value("dark_mode") == "true": - self.dark_checkbox.setChecked(True) - else: - self.dark_checkbox.setChecked(False) - else: - self.dark_checkbox.setChecked(True) - - self.addWidget(self.dark_checkbox, '', 'darkmode') - self.copy_files_checkbox = QCheckBox("Allow a copy of the image files to be stored. ") self.copy_files_checkbox.setChecked(False) self.addWidget(self.copy_files_checkbox, '', 'copy_file_checkbox') @@ -89,14 +77,10 @@ def __init__(self, parent, title="Settings"): def onOk(self): - default_font_family = PySide2.QtWidgets.QApplication.font().family() - font = PySide2.QtGui.QFont(default_font_family, self.fontsize_widget.value()) - PySide2.QtWidgets.QApplication.setFont(font) + default_font_family = qtpy.QtWidgets.QApplication.font().family() + font = qtpy.QtGui.QFont(default_font_family, self.fontsize_widget.value()) + qtpy.QtWidgets.QApplication.setFont(font) #self.parent.settings.setValue("settings_chosen", 1) - if self.dark_checkbox.isChecked(): - self.parent.settings.setValue("dark_mode", True) - else: - self.parent.settings.setValue("dark_mode", False) self.parent.SetAppStyle() if self.copy_files_checkbox.isChecked(): diff --git a/src/idvc/ui/graphs_widgets.py b/src/idvc/ui/graphs_widgets.py index 1277e90e..0165dbaf 100644 --- a/src/idvc/ui/graphs_widgets.py +++ b/src/idvc/ui/graphs_widgets.py @@ -1,6 +1,6 @@ -from PySide2.QtWidgets import * -from PySide2.QtCore import * -from PySide2.QtGui import * +from qtpy.QtWidgets import * +from qtpy.QtCore import * +from qtpy.QtGui import * import numpy as np from matplotlib.figure import Figure from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas diff --git a/src/idvc/ui/save_widgets.py b/src/idvc/ui/save_widgets.py index 5b94bffc..f5d63ddc 100644 --- a/src/idvc/ui/save_widgets.py +++ b/src/idvc/ui/save_widgets.py @@ -1,7 +1,7 @@ -from PySide2 import QtWidgets, QtCore -from PySide2.QtWidgets import * -from PySide2.QtCore import * -from PySide2.QtGui import * +from qtpy import QtWidgets, QtCore +from qtpy.QtWidgets import * +from qtpy.QtCore import * +from qtpy.QtGui import * import shutil import os import tempfile @@ -31,8 +31,8 @@ def __init__(self, parent, object_type, save_only): #self.setInputMode(QtWidgets.QInputDialog.TextInput) self.textbox = QLineEdit(self) - rx = QRegExp("[A-Za-z0-9]+") - validator = QRegExpValidator(rx, self.textbox) #need to check this + rx = QRegularExpression("[A-Za-z0-9]+") + validator = QRegularExpressionValidator(rx, self.textbox) #need to check this self.textbox.setValidator(validator) self.save_button = QPushButton("Save") diff --git a/src/idvc/ui/windows.py b/src/idvc/ui/windows.py index 766f70ee..3aac9688 100644 --- a/src/idvc/ui/windows.py +++ b/src/idvc/ui/windows.py @@ -8,7 +8,7 @@ from ccpi.viewer.QCILViewerWidget import QCILViewerWidget from ccpi.viewer.utils import * from ccpi.viewer.utils.error_handling import ErrorObserver -from PySide2 import QtCore, QtGui, QtWidgets +from qtpy import QtCore, QtGui, QtWidgets from idvc.ui.graphs_widgets import * import logging @@ -216,7 +216,7 @@ def __init__(self, parent=None): self.setDockOptions(QMainWindow.ForceTabbedDocks) # Window dimensions - geometry = qApp.desktop().availableGeometry(self) + geometry = QtWidgets.QApplication.primaryScreen().availableGeometry() self.setGeometry(50,50, geometry.width()-100, geometry.height()-100) #self.setFixedSize(geometry.width() * 0.6, geometry.height() * 0.8) diff --git a/src/idvc/utilities.py b/src/idvc/utilities.py index a50916a4..c1b1a351 100644 --- a/src/idvc/utilities.py +++ b/src/idvc/utilities.py @@ -1,8 +1,8 @@ -import PySide2 -from PySide2 import QtWidgets, QtCore, QtGui -from PySide2.QtWidgets import * -from PySide2.QtCore import * -from PySide2.QtGui import * +import qtpy +from qtpy import QtWidgets, QtCore, QtGui +from qtpy.QtWidgets import * +from qtpy.QtCore import * +from qtpy.QtGui import * import numpy as np import os from eqt.ui.NoBorderScrollArea import NoBorderScrollArea