diff --git a/CHANGELOG.md b/CHANGELOG.md index eaea0b35..03a483d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,16 @@ ## vx.x.x +Enhancements: + - Add `SaveableRawInputDialog` #444 + - Standalone viewer uses `SaveableRawInputDialog` - this allows user to save and reload settings for loading raw data + Build and CI: - Renamed mambaforge to miniforge to fix docker action #446 - Use ubuntu v22.04 in the actions #446 - Add setuptools as a build requirment #446 + ## v24.1.0 Enhancements: diff --git a/Wrappers/Python/ccpi/viewer/ui/__init__.py b/Wrappers/Python/ccpi/viewer/ui/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Wrappers/Python/ccpi/viewer/ui/dialogs.py b/Wrappers/Python/ccpi/viewer/ui/dialogs.py index 83217960..be2f968c 100644 --- a/Wrappers/Python/ccpi/viewer/ui/dialogs.py +++ b/Wrappers/Python/ccpi/viewer/ui/dialogs.py @@ -3,7 +3,7 @@ from eqt.ui import FormDialog from eqt.ui.SessionDialogs import AppSettingsDialog, ErrorDialog from PySide2 import QtCore, QtGui, QtWidgets -from PySide2.QtWidgets import QCheckBox, QDoubleSpinBox, QLabel, QLineEdit, QComboBox +from PySide2.QtWidgets import QCheckBox, QDoubleSpinBox, QLabel, QLineEdit, QComboBox, QPushButton import numpy as np from ccpi.viewer.utils import Converter from ccpi.viewer.utils.conversion import cilRawCroppedReader @@ -260,48 +260,6 @@ def preview(self): reader2.SetTypeCodeName(dt.name) reader2.SetStoredArrayShape(shape) reader2.Update() - # image = reader2.GetOutput() - - # rawfname = os.path.join(tempfile.gettempdir(),"test.raw") - - # offset = offset * bytes_per_element - # slices_to_read = 1 - # if shape[2] > 1: - # slices_to_read = 2 - # with open(self.fname, 'br') as f: - # f.seek(offset) - # raw_data = f.read(slice_size*bytes_per_element* slices_to_read) - # with open(rawfname, 'wb') as f2: - # f2.write(raw_data) - - # reader2 = vtk.vtkImageReader2() - # reader2.SetFileName(rawfname) - - # vtktype = Converter.dtype_name_to_vtkType[dt.name] - # reader2.SetDataScalarType(vtktype) - - # if isBigEndian: - # reader2.SetDataByteOrderToBigEndian() - # else: - # reader2.SetDataByteOrderToLittleEndian() - - # reader2.SetFileDimensionality(len(shape)) - # vtkshape = shape[:] - # if not isFortran: - # # need to reverse the shape (again) - # vtkshape = shape[::-1] - # # vtkshape = shape[:] - # slice_idx = 0 - # if dimensionality == 3: - # slice_idx = vtkshape[2]//2 - # reader2.SetDataExtent(0, vtkshape[0]-1, 0, vtkshape[1]-1, slice_idx, slice_idx+slices_to_read-1) - # # DataSpacing and DataOrigin should be added to the interface - # reader2.SetDataSpacing(1, 1, 1) - # reader2.SetDataOrigin(0, 0, 0) - - # print("reading") - # reader2.Update() - # read one slice in the middle and display it in a viewer in a modal dialog diag = QtWidgets.QDialog(parent=self) diag.setModal(True) @@ -338,6 +296,137 @@ def preview(self): diag.open() +class SaveableRawInputDialog(RawInputDialog): + ''' + This is a dialog window which allows the user to set information + for a raw file, including: + - dimensionality + - size of dimensions + - data type + - endianness + - fortran ordering + + The dialog can let the user preview the data and verify that it is correct. + + The dialog allows you to save load settings under a memorable name. + You can reload settings you have saved previously, by selecting their associated name from a dropdown. + ''' + + def __init__(self, parent, fname, qsettings): + super(SaveableRawInputDialog, self).__init__(parent, fname) + + self.settings = qsettings + + self.formWidget.addSpanningWidget(QCheckBox("Edit Parameters"), 'enable_edit') + self.getWidget('enable_edit').setChecked(True) + self.getWidget('enable_edit').stateChanged.connect(self._change_edit_state) + + self.formWidget.addTitle(QLabel('Load Settings'), 'load_settings_title') + load_label = QLabel('Settings Name: ') + load_drop_down = QComboBox() + load_drop_down.addItems(self._get_settings_names_for_dialog()) + self.formWidget.addWidget(load_drop_down, load_label, 'load_name') + + load_button = QPushButton('Load Settings') + load_button.clicked.connect(self._load_settings) + self.formWidget.addSpanningWidget(load_button, 'load') + + self.buttonBox.addButton(QtWidgets.QDialogButtonBox.Save) + self.buttonBox.button(QtWidgets.QDialogButtonBox.Save).clicked.connect(self._open_save_dialog) + + def _change_edit_state(self, editable=True): + '''Changes the edit state of the form''' + + widgets = self.getWidgets() + for widget in widgets.values(): + widget.setEnabled(editable) + + if not editable: + widgets_to_preserve = [ + 'load_name', 'load', 'enable_edit', 'load_name', 'load_settings_title', 'preview_slice', + 'preview_button' + ] + for widget in widgets_to_preserve: + self.getWidget(widget, 'field').setEnabled(True) + try: + self.getWidget(widget, 'label').setEnabled(True) + except: + pass + + def _open_save_dialog(self): + '''Opens dialog for specifiying name to save settings under''' + dialog = FormDialog(self) + dialog.formWidget.addTitle(QLabel('Save Settings'), 'save_settings_title') + save_label = QLabel('Settings Name: ') + save_box = QLineEdit() + dialog.formWidget.addWidget(save_box, save_label, 'save_name') + dialog.Cancel.clicked.connect(dialog.close) + dialog.Ok.clicked.connect(self._save_settings) + dialog.Ok.clicked.connect(dialog.close) + dialog.open() + self.save_dialog = dialog + + def _get_settings_save_name(self): + return self.save_dialog.getWidget('save_name').text() + + def _save_settings(self): + ''' + Adds a dictionary to the qsettings 'raw_dialog' + dictionary : + key: name entered by the user + value: the status of all widgets on the form + ''' + settings_dict = self.settings.value('raw_dialog', {}) + + settings_name = self._get_settings_save_name() + + self.saveAllWidgetStates() + current_widget_status = self.getSavedWidgetStates() + + settings_dict[settings_name] = current_widget_status + + self.settings.setValue('raw_dialog', settings_dict) + + def _get_settings_names_for_dialog(self): + ''' + Retrive from self.settings the names of all settings previously saved in the + 'raw_dialog' entry + ''' + settings_dict = self.settings.value('raw_dialog', {}) + return settings_dict.keys() + + def _get_name_of_state_to_load(self): + return self.formWidget.getWidget('load_name').currentText() + + def _load_settings(self): + ''' + Load all of the widget states saved in the 'raw_dialog' entry of + self.settings under the name selected by the user from the load_name comobox + + Disable editing of parameters. + ''' + settings_found = False + if self.settings.value('raw_dialog'): + settings_dict = self.settings.value('raw_dialog', {}) + + name_of_state = self._get_name_of_state_to_load() + state = settings_dict.get(name_of_state) + + if state is not None: + + # save current stae + + self.applyWidgetStates(state) + self.getWidget('enable_edit').setChecked(False) + + # load current state of dropdown + settings_found = True + + if not settings_found: + # create error dialog: + print("Settings not found") + + class HDF5InputDialog(FormDialog): ''' This is a dialog window which allows the user to set: diff --git a/Wrappers/Python/ccpi/viewer/ui/main_windows.py b/Wrappers/Python/ccpi/viewer/ui/main_windows.py index 77ff8b16..e792b24b 100644 --- a/Wrappers/Python/ccpi/viewer/ui/main_windows.py +++ b/Wrappers/Python/ccpi/viewer/ui/main_windows.py @@ -8,7 +8,7 @@ from ccpi.viewer.CILViewer2D import CILViewer2D from ccpi.viewer.CILViewer import CILViewer from ccpi.viewer.QCILViewerWidget import QCILDockableWidget -from ccpi.viewer.ui.dialogs import HDF5InputDialog, RawInputDialog, ViewerSettingsDialog +from ccpi.viewer.ui.dialogs import HDF5InputDialog, RawInputDialog, ViewerSettingsDialog, SaveableRawInputDialog from ccpi.viewer.ui.qt_widgets import ViewerCoordsDockWidget from ccpi.viewer.utils import cilPlaneClipper from ccpi.viewer.utils.io import ImageReader @@ -178,7 +178,7 @@ def selectImage(self, label=None): if hasattr(self, 'raw_dialog'): self.raw_dialog.restoreAllSavedWidgetStates() else: - raw_dialog = RawInputDialog(self, file) + raw_dialog = SaveableRawInputDialog(self, file, self.settings) raw_dialog.Ok.clicked.connect(lambda: self.getRawAttrsFromDialog(raw_dialog)) self.raw_dialog = raw_dialog # See https://doc.qt.io/qt-6/qdialog.html#exec @@ -210,7 +210,7 @@ def getRawAttrsFromDialog(self, dialog): Parameters ---------- - dialog : RawInputDialog + dialog : SaveableRawInputDialog The dialog to get the attributes from. ''' dialog.saveAllWidgetStates() diff --git a/Wrappers/Python/test/test_ui_dialogs.py b/Wrappers/Python/test/test_ui_dialogs.py index 26097a94..55bbc928 100644 --- a/Wrappers/Python/test/test_ui_dialogs.py +++ b/Wrappers/Python/test/test_ui_dialogs.py @@ -1,13 +1,17 @@ import unittest from unittest import mock -from ccpi.viewer.ui.dialogs import ViewerSettingsDialog, HDF5InputDialog, RawInputDialog +from ccpi.viewer.ui.dialogs import ViewerSettingsDialog, HDF5InputDialog, RawInputDialog, SaveableRawInputDialog from eqt.ui.SessionDialogs import AppSettingsDialog from PySide2.QtWidgets import QMainWindow import os from unittest import mock +from unittest.mock import patch from PySide2.QtWidgets import QApplication, QLabel, QFrame, QDoubleSpinBox, QCheckBox, QPushButton, QLineEdit, QComboBox, QWidget +from PySide2.QtCore import QSettings +from eqt.ui import FormDialog +from functools import partial import sys @@ -204,5 +208,65 @@ def test_getRawAttrs(self): } +@unittest.skipIf(skip_as_conda_build, "On conda builds do not do any test with interfaces") +class TestSaveableRawInputDialog(unittest.TestCase): + + def setUp(self): + global _instance + if _instance is None: + _instance = QApplication(sys.argv) + self.parent = QMainWindow() + self.settings = QSettings() + self.fname = "test.raw" + + @patch("ccpi.viewer.ui.dialogs.RawInputDialog.__init__") + def test_init_calls_raw_input_dialog_init(self, mock_init_call): + mock_init_call.return_value = partial(FormDialog.__init__) + # expect attribute error after init call: + with self.assertRaises(AttributeError): + rdi = SaveableRawInputDialog(self.parent, self.fname, self.settings) + mock_init_call.assert_called_once() + + def test_init(self): + rdi = SaveableRawInputDialog(self.parent, self.fname, self.settings) + assert rdi is not None + + @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_settings_save_name") + def test_save_settings_when_nothing_in_qsettings(self, mock_get_name): + mock_get_name.return_value = "my_name" + empty_settings = QSettings('A', 'A') + rdi = SaveableRawInputDialog(self.parent, self.fname, empty_settings) + rdi._save_settings() + the_dict = empty_settings.value('raw_dialog') + self.assertEqual(empty_settings.allKeys(), ['raw_dialog']) + self.assertEqual(the_dict, {'my_name': rdi.getAllWidgetStates()}) + + @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_settings_save_name") + def test_save_settings_when_soemthing_in_qsettings(self, mock_get_name): + mock_get_name.return_value = "my_name_2" + pop_settings = QSettings('C', 'D') + pop_settings.setValue('raw_dialog', {'hi': "I'm not empty"}) + rdi = SaveableRawInputDialog(self.parent, self.fname, pop_settings) + rdi._save_settings() + the_dict = pop_settings.value('raw_dialog') + self.assertEqual(the_dict, {'hi': "I'm not empty", 'my_name_2': rdi.getAllWidgetStates()}) + + @patch("ccpi.viewer.ui.dialogs.SaveableRawInputDialog._get_name_of_state_to_load") + def test_load_settings(self, mock_get_name): + mock_get_name.return_value = "state" + example_qsettings = QSettings('B', 'B') + rdi = SaveableRawInputDialog(self.parent, self.fname, example_qsettings) + rdi.getWidget('dim_Images').setText('10') + example_settings = rdi.getAllWidgetStates() + + example_qsettings = QSettings('B', 'B') + + example_qsettings.setValue('raw_dialog', {'state': example_settings}) + + rdi2 = SaveableRawInputDialog(self.parent, self.fname, example_qsettings) + rdi2._load_settings() + self.assertEqual(rdi2.getWidget('dim_Images').text(), "10") + + if __name__ == '__main__': unittest.main()