Esempio n. 1
0
 def show_recon_window(self):
     if not self.recon:
         self.recon = ReconstructWindowView(self)
         self.recon.show()
     else:
         self.recon.activateWindow()
         self.recon.raise_()
Esempio n. 2
0
 def show_recon_window(self):
     if not self.recon:
         self.recon = ReconstructWindowView(self)
         self.recon.recon_applied.connect(self.recon_applied.emit)
         self.recon.show()
     else:
         self.recon.activateWindow()
         self.recon.raise_()
         self.recon.show()
Esempio n. 3
0
 def setUp(self) -> None:
     with mock.patch("mantidimaging.gui.windows.main.view.WelcomeScreenPresenter"):
         self.main_window = MainWindowView()
     self.view = ReconstructWindowView(self.main_window)
     self.view.presenter = self.presenter = mock.Mock()
     self.view.image_view = self.image_view = mock.Mock()
     self.view.tableView = self.tableView = mock.Mock()
     self.view.resultCor = self.resultCor = mock.Mock()
     self.view.resultTilt = self.resultTilt = mock.Mock()
     self.view.resultSlope = self.resultSlope = mock.Mock()
     self.view.numIter = self.numIter = mock.Mock()
     self.view.pixelSize = self.pixelSize = mock.Mock()
     self.view.alphaSpinbox = self.alpha = mock.Mock()
     self.view.algorithmName = self.algorithmName = mock.Mock()
     self.view.filterName = self.filterName = mock.Mock()
     self.view.maxProjAngle = self.maxProjAngle = mock.Mock()
     self.view.autoFindMethod = self.autoFindMethod = mock.Mock()
Esempio n. 4
0
class ReconstructWindowViewTest(unittest.TestCase):
    def setUp(self) -> None:
        with mock.patch("mantidimaging.gui.windows.main.view.WelcomeScreenPresenter"):
            self.main_window = MainWindowView()
        self.view = ReconstructWindowView(self.main_window)
        self.view.presenter = self.presenter = mock.Mock()
        self.view.image_view = self.image_view = mock.Mock()
        self.view.tableView = self.tableView = mock.Mock()
        self.view.resultCor = self.resultCor = mock.Mock()
        self.view.resultTilt = self.resultTilt = mock.Mock()
        self.view.resultSlope = self.resultSlope = mock.Mock()
        self.view.numIter = self.numIter = mock.Mock()
        self.view.pixelSize = self.pixelSize = mock.Mock()
        self.view.alphaSpinbox = self.alpha = mock.Mock()
        self.view.algorithmName = self.algorithmName = mock.Mock()
        self.view.filterName = self.filterName = mock.Mock()
        self.view.maxProjAngle = self.maxProjAngle = mock.Mock()
        self.view.autoFindMethod = self.autoFindMethod = mock.Mock()

    @mock.patch("mantidimaging.gui.windows.recon.view.QMessageBox")
    def test_check_stack_for_invalid_180_deg_proj_when_proj_180_degree_shape_matches_images_is_false(
            self, qmessagebox_mock):
        self.main_window.get_images_from_stack_uuid = mock.Mock()
        selected_images = self.main_window.get_images_from_stack_uuid.return_value
        selected_images.has_proj180deg.return_value = True
        self.presenter.proj_180_degree_shape_matches_images.return_value = False

        uuid = mock.Mock()
        self.view.check_stack_for_invalid_180_deg_proj(uuid)
        self.main_window.get_images_from_stack_uuid.assert_called_once_with(uuid)
        self.presenter.proj_180_degree_shape_matches_images.assert_called_once_with(selected_images)
        qmessagebox_mock.warning.assert_called_once_with(
            self.view, "Potential Failure",
            "The shapes of the selected stack and it's 180 degree projections do not match! This is going to cause an "
            "error when calculating the COR. Fix the shape before continuing!")

    @mock.patch("mantidimaging.gui.windows.recon.view.QMessageBox")
    def test_check_stack_for_invalid_180_deg_proj_when_proj_180_degree_shape_matches_images_is_true(
            self, qmessagebox_mock):
        self.main_window.get_images_from_stack_uuid = mock.Mock()
        selected_images = self.main_window.get_images_from_stack_uuid.return_value
        selected_images.has_proj180deg.return_value = True
        self.presenter.proj_180_degree_shape_matches_images.return_value = True

        uuid = mock.Mock()
        self.view.check_stack_for_invalid_180_deg_proj(uuid)
        self.main_window.get_images_from_stack_uuid.assert_called_once_with(uuid)
        self.presenter.proj_180_degree_shape_matches_images.assert_called_once_with(selected_images)
        qmessagebox_mock.warning.assert_not_called()

    def test_remove_selected_cor(self):
        assert self.view.remove_selected_cor() == self.tableView.removeSelectedRows.return_value

    def test_clear_cor_table(self):
        assert self.view.clear_cor_table() == self.tableView.model.return_value.removeAllRows.return_value

    def test_cleanup(self):
        self.view.stackSelector = stack_selector_mock = mock.Mock()

        self.view.cleanup()
        stack_selector_mock.unsubscribe_from_main_window.assert_called_once()
        assert self.main_window.recon is None

    @mock.patch("mantidimaging.gui.windows.recon.view.CorTiltPointQtModel")
    def test_cor_table_model_when_model_is_none(self, cortiltpointqtmodel_mock):
        self.tableView.model.return_value = None
        mdl = cortiltpointqtmodel_mock.return_value

        assert self.view.cor_table_model == self.tableView.model.return_value
        cortiltpointqtmodel_mock.assert_called_once_with(self.tableView)
        self.tableView.setModel.assert_called_once_with(mdl)

    @mock.patch("mantidimaging.gui.windows.recon.view.CorTiltPointQtModel")
    def test_cor_table_model_when_model_is_not_none(self, cortiltpointqtmodel_mock):
        assert self.view.cor_table_model == self.tableView.model.return_value
        cortiltpointqtmodel_mock.assert_not_called()
        self.tableView.setModel.assert_not_called()

    def test_cor_table_model_selected_rows(self):
        mock_row = mock.Mock(row=mock.Mock(return_value=1))
        mock_selection_model = mock.Mock(selectedRows=lambda: [mock_row, mock_row])
        self.tableView.selectionModel = mock.Mock(return_value=mock_selection_model)

        ret = self.view.get_cor_table_selected_rows()

        self.assertEqual(ret, [1, 1])

    def test_set_results(self):
        cor_val = 20
        tilt_val = 30
        slope_val = 40
        cor = ScalarCoR(cor_val)
        tilt = Degrees(tilt_val)
        slope = Slope(slope_val)

        self.view.set_results(cor, tilt, slope)
        self.resultCor.setValue.assert_called_once_with(cor_val)
        self.resultTilt.setValue.assert_called_once_with(tilt_val)
        self.resultSlope.setValue.assert_called_once_with(slope_val)
        self.image_view.set_tilt.assert_called_once_with(tilt)

    def test_preview_image_on_button_press(self):
        event_mock = mock.Mock()
        event_mock.button = 1
        event_mock.ydata = ydata = 20.3

        self.view.preview_image_on_button_press(event_mock)
        self.presenter.set_preview_slice_idx.assert_called_once_with(int(ydata))

    def test_no_preview_image_on_button_press(self):
        event_mock = mock.Mock()
        event_mock.button = 2
        event_mock.ydata = 20.3

        self.view.preview_image_on_button_press(event_mock)
        self.presenter.set_preview_slice_idx.assert_not_called()

    @mock.patch("mantidimaging.gui.windows.recon.view.QSignalBlocker")
    def test_update_projection(self, _):
        image_data = mock.Mock()
        preview_slice_idx = 13
        tilt_angle = Degrees(30)

        self.view.previewSliceIndex = preview_slice_index_mock = mock.Mock()
        self.view.update_projection(image_data, preview_slice_idx, tilt_angle)

        preview_slice_index_mock.setValue.assert_called_once_with(preview_slice_idx)
        self.image_view.update_projection.assert_called_once_with(image_data, preview_slice_idx, tilt_angle)

    def test_update_sinogram(self):
        image_data = mock.Mock()
        self.view.update_sinogram(image_data)
        self.image_view.update_sinogram.assert_called_once_with(image_data)

    def test_update_recon_preview_no_hist(self):
        image_data = mock.Mock()
        self.view.update_recon_hist_needed = False

        self.view.update_recon_preview(image_data)
        self.image_view.update_recon.assert_called_once_with(image_data)

    def test_update_recon_preview_and_hist(self):
        image_data = mock.Mock()
        self.view.update_recon_hist_needed = True

        self.view.update_recon_preview(image_data)
        self.image_view.update_recon.assert_called_once_with(image_data)
        self.image_view.update_recon_hist.assert_called_once_with()

    def test_reset_image_recon_preview(self):
        self.view.reset_image_recon_preview()
        self.image_view.clear_recon.assert_called_once()

    def test_reset_slice_and_tilt(self):
        slice_index = 5
        self.view.reset_slice_and_tilt(slice_index)
        self.image_view.reset_slice_and_tilt.assert_called_once_with(slice_index)

    def test_on_table_row_count_change(self):
        self.tableView.model.return_value.empty = empty = False
        self.view.removeBtn = remove_button_mock = mock.Mock()
        self.view.clearAllBtn = clear_all_button_mock = mock.Mock()
        self.view.on_table_row_count_change()

        remove_button_mock.setEnabled.assert_called_once_with(not empty)
        clear_all_button_mock.setEnabled.assert_called_once_with(not empty)

    def test_add_cor_table_row(self):
        row = 3
        slice_index = 4
        cor = 5.0

        self.view.add_cor_table_row(row, slice_index, cor)

        self.tableView.model.return_value.appendNewRow.assert_called_once_with(row, slice_index, cor)
        self.tableView.selectRow.assert_called_once_with(row)

    def test_rotation_centre_property(self):
        assert self.view.rotation_centre == self.resultCor.value.return_value

    def test_tilt_property(self):
        assert self.view.tilt == self.resultTilt.value.return_value

    def test_slope_property(self):
        assert self.view.slope == self.resultSlope.value.return_value

    def test_max_proj_angle(self):
        assert self.view.max_proj_angle == self.maxProjAngle.value.return_value

    def test_algorithm_name(self):
        assert self.view.algorithm_name == self.algorithmName.currentText.return_value

    def test_filter_name(self):
        assert self.view.filter_name == self.filterName.currentText.return_value

    def test_num_iter_property(self):
        assert self.view.num_iter == self.numIter.value.return_value

    def test_num_iter_setter(self):
        iters = 123
        self.view.num_iter = iters
        self.numIter.setValue.assert_called_once_with(iters)

    def test_pixel_size_property(self):
        assert self.view.pixel_size == self.pixelSize.value.return_value

    @mock.patch("mantidimaging.gui.windows.recon.view.QSignalBlocker")
    def test_pixel_size_setter(self, _):
        value = 123
        self.view.pixel_size = value
        self.pixelSize.setValue.assert_called_once_with(value)

    @mock.patch("mantidimaging.gui.windows.recon.view.ReconstructionParameters")
    def test_recon_params(self, recon_params_mock):
        self.view.recon_params()
        recon_params_mock.assert_called_once_with(algorithm=self.algorithmName.currentText.return_value,
                                                  filter_name=self.filterName.currentText.return_value,
                                                  num_iter=self.numIter.value.return_value,
                                                  cor=ScalarCoR(self.resultCor.value.return_value),
                                                  tilt=Degrees(self.resultTilt.value.return_value),
                                                  pixel_size=self.pixelSize.value.return_value,
                                                  alpha=self.alpha.value.return_value,
                                                  max_projection_angle=self.maxProjAngle.value.return_value,
                                                  beam_hardening_coefs=self.view.beam_hardening_coefs)

    def test_set_table_point(self):
        idx = 12
        slice_idx = 34
        cor = 12.34

        self.view.set_table_point(idx, slice_idx, cor)
        self.tableView.model.return_value.set_point.assert_called_once_with(idx, slice_idx, cor, reset_results=False)

    def test_show_recon_volume(self):
        data = mock.Mock()
        self.main_window.create_new_stack = create_new_stack_mock = mock.Mock()
        self.main_window.add_recon_to_dataset = add_to_dataset_mock = mock.Mock()
        stack_id = "id"
        self.view.show_recon_volume(data, stack_id)
        create_new_stack_mock.assert_called_once_with(data)
        add_to_dataset_mock.assert_called_once_with(data, stack_id)

    def test_get_stack_visualiser_when_uuid_is_none(self):
        assert self.view.get_stack_visualiser(None) is None

    def test_get_stack_visualiser_when_uuid_is_not_none(self):
        uuid = mock.Mock()
        self.main_window.get_stack_visualiser = mock.Mock()

        assert self.view.get_stack_visualiser(uuid) == self.main_window.get_stack_visualiser.return_value
        self.main_window.get_stack_visualiser.assert_called_once_with(uuid)

    def test_hide_tilt(self):
        self.view.hide_tilt()
        self.image_view.hide_tilt.assert_called_once()

    def test_set_filters_for_recon_tool(self):
        filters = ["abc" for _ in range(3)]
        self.view.set_filters_for_recon_tool(filters)
        self.filterName.clear.assert_called_once()
        self.filterName.insertItems.assert_called_once_with(0, filters)

    @mock.patch("mantidimaging.gui.windows.recon.view.QInputDialog")
    def test_get_number_of_cors_when_accepted_is_true(self, qinputdialog_mock):
        num = 2
        accepted = True
        qinputdialog_mock.getInt.return_value = (num, accepted)

        assert self.view.get_number_of_cors() == num
        qinputdialog_mock.getInt.assert_called_once_with(self.view,
                                                         "Number of slices",
                                                         "On how many slices to run the automatic CoR finding?",
                                                         value=6,
                                                         min=2,
                                                         max=30,
                                                         step=1)

    @mock.patch("mantidimaging.gui.windows.recon.view.QInputDialog")
    def test_get_number_of_cors_when_accepted_is_false(self, qinputdialog_mock):
        num = 2
        accepted = False
        qinputdialog_mock.getInt.return_value = (num, accepted)

        assert self.view.get_number_of_cors() is None
        qinputdialog_mock.getInt.assert_called_once_with(self.view,
                                                         "Number of slices",
                                                         "On how many slices to run the automatic CoR finding?",
                                                         value=6,
                                                         min=2,
                                                         max=30,
                                                         step=1)

    def test_get_auto_cor_method_when_current_is_correlation(self):
        self.autoFindMethod.currentText.return_value = "Correlation"
        assert self.view.get_auto_cor_method() == AutoCorMethod.CORRELATION

    def test_get_auto_cor_method_when_current_is_not_correlation(self):
        self.autoFindMethod.currentText.return_value = "NotCorrelation"
        assert self.view.get_auto_cor_method() == AutoCorMethod.MINIMISATION_SQUARE_SUM

    def test_set_correlate_buttons_enables(self):
        self.view.correlateBtn = correlate_button_mock = mock.Mock()
        self.view.minimiseBtn = minimise_button_mock = mock.Mock()

        for enabled in [True, False]:
            with self.subTest(enabled=enabled):
                self.view.set_correlate_buttons_enabled(enabled)
                correlate_button_mock.setEnabled.assert_called_once_with(enabled)
                minimise_button_mock.setEnabled.assert_called_once_with(enabled)
            if enabled:
                correlate_button_mock.reset_mock()
                minimise_button_mock.reset_mock()

    @mock.patch("mantidimaging.gui.windows.recon.view.open_help_webpage")
    def test_open_help_webpage_when_no_exception(self, open_help_webpage_mock):
        page = "page"
        self.view.open_help_webpage(page)
        open_help_webpage_mock.assert_called_once_with(SECTION_USER_GUIDE, page)

    @mock.patch("mantidimaging.gui.windows.recon.view.open_help_webpage")
    def test_open_help_webpage_when_exception(self, open_help_webpage_mock):
        page = "page"
        open_help_webpage_mock.side_effect = RuntimeError
        self.view.show_error_dialog = show_error_dialog_mock = mock.Mock()
        self.view.open_help_webpage(page)
        open_help_webpage_mock.assert_called_once_with(SECTION_USER_GUIDE, page)
        show_error_dialog_mock.assert_called_once_with(str(RuntimeError()))

    def test_change_refine_iterations_when_algorithm_name_is_sirt(self):
        self.view.refineIterationsBtn = refine_iterations_button_mock = mock.Mock()
        self.view.algorithmName = mock.Mock()
        self.view.algorithmName.currentText.return_value = "SIRT_CUDA"

        self.view.change_refine_iterations()
        refine_iterations_button_mock.setEnabled.assert_called_once_with(True)

    def test_change_refine_iterations_when_algorithm_name_is_not_sirt(self):
        self.view.refineIterationsBtn = refine_iterations_button_mock = mock.Mock()
        self.view.algorithmName = mock.Mock()
        self.view.algorithmName.currentText.return_value = "FBP_CUDA"

        self.view.change_refine_iterations()
        refine_iterations_button_mock.setEnabled.assert_called_once_with(False)
Esempio n. 5
0
class MainWindowView(BaseMainWindowView):
    NOT_THE_LATEST_VERSION = "This is not the latest version"
    UNCAUGHT_EXCEPTION = "Uncaught exception"

    model_changed = pyqtSignal()
    filter_applied = pyqtSignal()
    recon_applied = pyqtSignal()
    backend_message = pyqtSignal(bytes)

    menuFile: QMenu
    menuWorkflow: QMenu
    menuImage: QMenu
    menuHelp: QMenu

    actionRecon: QAction
    actionFilters: QAction
    actionCompareImages: QAction
    actionSampleLoadLog: QAction
    actionLoadProjectionAngles: QAction
    actionLoad180deg: QAction
    actionLoadDataset: QAction
    actionLoadImages: QAction
    actionLoadNeXusFile: QAction
    actionSave: QAction
    actionExit: QAction

    filters: Optional[FiltersWindowView] = None
    recon: Optional[ReconstructWindowView] = None

    load_dialogue: Optional[MWLoadDialog] = None
    save_dialogue: Optional[MWSaveDialog] = None
    nexus_load_dialog: Optional[NexusLoadDialog] = None

    def __init__(self, open_dialogs: bool = True):
        super().__init__(None, "gui/ui/main_window.ui")

        self.setWindowTitle("Mantid Imaging")

        self.presenter = MainWindowPresenter(self)

        status_bar = self.statusBar()
        self.status_bar_label = QLabel("", self)
        status_bar.addPermanentWidget(self.status_bar_label)

        self.setup_shortcuts()
        self.update_shortcuts()

        self.setAcceptDrops(True)
        base_path = finder.ROOT_PATH

        self.open_dialogs = open_dialogs
        if self.open_dialogs:
            if versions.get_conda_installed_label() != "main":
                self.setWindowTitle("Mantid Imaging Unstable")
                bg_image = os.path.join(
                    base_path,
                    "gui/ui/images/mantid_imaging_unstable_64px.png")
            else:
                bg_image = os.path.join(
                    base_path, "gui/ui/images/mantid_imaging_64px.png")
        else:
            bg_image = os.path.join(base_path,
                                    "gui/ui/images/mantid_imaging_64px.png")
        self.setWindowIcon(QIcon(bg_image))

        self.welcome_window = None
        if self.open_dialogs and WelcomeScreenPresenter.show_today():
            self.show_about()

        self.wizard = None

        args = CommandLineArguments()
        if args.path():
            self.presenter.load_stacks_from_folder(args.path())

        if args.operation():
            self.presenter.show_operation(args.operation())

        if args.recon():
            self.show_recon_window()

        self.dataset_tree_widget = QTreeWidget()
        self.dataset_tree_widget.setContextMenuPolicy(Qt.CustomContextMenu)
        self.dataset_tree_widget.customContextMenuRequested.connect(
            self._open_tree_menu)
        self.dataset_tree_widget.itemDoubleClicked.connect(
            self._bring_stack_tab_to_front)

        self.splitter = QSplitter(Qt.Horizontal, self)
        self.splitter.addWidget(self.dataset_tree_widget)

        self.dataset_tree_widget.setMinimumWidth(250)
        self.dataset_tree_widget.setMaximumWidth(300)
        self.dataset_tree_widget.setHeaderLabel("")

        self.setCentralWidget(self.splitter)

    def setup_shortcuts(self):
        self.actionLoadDataset.triggered.connect(self.show_load_dialogue)
        self.actionLoadImages.triggered.connect(self.load_image_stack)
        self.actionLoadNeXusFile.triggered.connect(self.show_load_nexus_dialog)
        self.actionSampleLoadLog.triggered.connect(self.load_sample_log_dialog)
        self.actionLoad180deg.triggered.connect(self.load_180_deg_dialog)
        self.actionLoadProjectionAngles.triggered.connect(
            self.load_projection_angles)
        self.actionSave.triggered.connect(self.show_save_dialogue)
        self.actionExit.triggered.connect(self.close)

        self.menuImage.aboutToShow.connect(self.populate_image_menu)

        self.actionOnlineDocumentation.triggered.connect(
            self.open_online_documentation)
        self.actionAbout.triggered.connect(self.show_about)
        self.actionWizard.triggered.connect(self.show_wizard)

        self.actionFilters.triggered.connect(self.show_filters_window)
        self.actionRecon.triggered.connect(self.show_recon_window)

        self.actionCompareImages.triggered.connect(
            self.show_stack_select_dialog)

        self.model_changed.connect(self.update_shortcuts)

    def populate_image_menu(self):
        self.menuImage.clear()
        current_stack = self.current_showing_stack()
        if current_stack is None:
            self.menuImage.addAction("No stack loaded!")
        else:
            populate_menu(self.menuImage, current_stack.actions)

    def current_showing_stack(self) -> Optional[StackVisualiserView]:
        for stack in self.findChildren(StackVisualiserView):
            if not stack.visibleRegion().isEmpty():
                return stack
        return None

    def update_shortcuts(self):
        enabled = len(self.presenter.stacks.values()) > 0
        self.actionSave.setEnabled(enabled)
        self.actionSampleLoadLog.setEnabled(enabled)
        self.actionLoad180deg.setEnabled(enabled)
        self.actionLoadProjectionAngles.setEnabled(enabled)
        self.menuWorkflow.setEnabled(enabled)
        self.menuImage.setEnabled(enabled)

    @staticmethod
    def open_online_documentation():
        url = QUrl("https://mantidproject.github.io/mantidimaging/")
        QDesktopServices.openUrl(url)

    def show_about(self):
        self.welcome_window = WelcomeScreenPresenter(self)
        self.welcome_window.show()

    def show_load_dialogue(self):
        self.load_dialogue = MWLoadDialog(self)
        self.load_dialogue.show()

    def show_load_nexus_dialog(self):
        self.nexus_load_dialog = NexusLoadDialog(self)
        self.nexus_load_dialog.show()

    def show_wizard(self):
        if self.wizard is None:
            self.wizard = WizardPresenter(self)
        self.wizard.show()

    @staticmethod
    def _get_file_name(caption: str, file_filter: str) -> str:
        selected_file, _ = QFileDialog.getOpenFileName(
            caption=caption,
            filter=f"{file_filter};;All (*.*)",
            initialFilter=file_filter)
        return selected_file

    def load_image_stack(self):
        # Open file dialog
        selected_file = self._get_file_name("Image",
                                            "Image File (*.tif *.tiff)")

        # Cancel/Close was clicked
        if selected_file == "":
            return

        self.presenter.load_image_stack(selected_file)

    def load_sample_log_dialog(self):
        stack_selector = StackSelectorDialog(
            main_window=self,
            title="Stack Selector",
            message="Which stack is the log being loaded for?")
        # Was closed without accepting (e.g. via x button or ESC)
        if QDialog.DialogCode.Accepted != stack_selector.exec():
            return
        stack_to_add_log_to = stack_selector.selected_stack

        # Open file dialog
        selected_file = self._get_file_name("Log to be loaded",
                                            "Log File (*.txt *.log *.csv)")

        # Cancel/Close was clicked
        if selected_file == "":
            return

        self.presenter.add_log_to_sample(stack_name=stack_to_add_log_to,
                                         log_file=selected_file)

        QMessageBox.information(
            self, "Load complete", f"{selected_file} was loaded as a log into "
            f"{stack_to_add_log_to}.")

    def load_180_deg_dialog(self):
        dataset_selector = DatasetSelectorDialog(main_window=self,
                                                 title="Dataset Selector")
        # Was closed without accepting (e.g. via x button or ESC)
        if QDialog.DialogCode.Accepted != dataset_selector.exec():
            return
        dataset_to_add_180_deg_to = dataset_selector.selected_dataset

        # Open file dialog
        selected_file = self._get_file_name("180 Degree Image",
                                            "Image File (*.tif *.tiff)")

        # Cancel/Close was clicked
        if selected_file == "":
            return

        _180_images = self.presenter.add_180_deg_to_dataset(
            dataset_id=dataset_to_add_180_deg_to, _180_deg_file=selected_file)
        self.create_new_180_stack(_180_images)

    LOAD_PROJECTION_ANGLES_DIALOG_MESSAGE = "Which stack are the projection angles in DEGREES being loaded for?"
    LOAD_PROJECTION_ANGLES_FILE_DIALOG_CAPTION = "File with projection angles in DEGREES"

    def load_projection_angles(self):
        stack_selector = StackSelectorDialog(
            main_window=self,
            title="Stack Selector",
            message=self.LOAD_PROJECTION_ANGLES_DIALOG_MESSAGE)
        # Was closed without accepting (e.g. via x button or ESC)
        if QDialog.DialogCode.Accepted != stack_selector.exec():
            return

        stack_name = stack_selector.selected_stack

        selected_file, _ = QFileDialog.getOpenFileName(
            caption=self.LOAD_PROJECTION_ANGLES_FILE_DIALOG_CAPTION,
            filter="All (*.*)")
        if selected_file == "":
            return

        pafp = ProjectionAngleFileParser(selected_file)
        projection_angles = pafp.get_projection_angles()

        self.presenter.add_projection_angles_to_sample(stack_name,
                                                       projection_angles)
        QMessageBox.information(
            self, "Load complete",
            f"Angles from {selected_file} were loaded into into "
            f"{stack_name}.")

    def execute_save(self):
        self.presenter.notify(PresNotification.SAVE)

    def execute_load(self):
        self.presenter.notify(PresNotification.LOAD)

    def execute_nexus_load(self):
        self.presenter.notify(PresNotification.NEXUS_LOAD)

    def show_save_dialogue(self):
        self.save_dialogue = MWSaveDialog(self, self.stack_list)
        self.save_dialogue.show()

    def show_recon_window(self):
        if not self.recon:
            self.recon = ReconstructWindowView(self)
            self.recon.recon_applied.connect(self.recon_applied.emit)
            self.recon.show()
        else:
            self.recon.activateWindow()
            self.recon.raise_()
            self.recon.show()

    def show_filters_window(self):
        if not self.filters:
            self.filters = FiltersWindowView(self)
            self.filters.filter_applied.connect(self.filter_applied.emit)
            self.filters.show()
        else:
            self.filters.activateWindow()
            self.filters.raise_()

    @property
    def stack_list(self):
        return self.presenter.stack_list

    @property
    def dataset_list(self):
        return self.presenter.dataset_list

    @property
    def stack_names(self):
        return self.presenter.stack_names

    def get_stack_visualiser(self, stack_uuid):
        return self.presenter.get_stack_visualiser(stack_uuid)

    def get_images_from_stack_uuid(self, stack_uuid) -> Images:
        return self.presenter.get_stack_visualiser(stack_uuid).presenter.images

    def get_all_stack_visualisers(self):
        return self.presenter.get_active_stack_visualisers()

    def get_all_stack_visualisers_with_180deg_proj(self):
        return self.presenter.get_all_stack_visualisers_with_180deg_proj()

    def get_stack_history(self, stack_uuid):
        return self.presenter.get_stack_history(stack_uuid)

    def create_new_stack(self, images: Images):
        self.presenter.create_new_stack(images)

    def create_new_180_stack(self, images: Images):
        self.presenter.create_new_180_stack(images)

    def update_stack_with_images(self, images: Images):
        self.presenter.update_stack_with_images(images)

    def get_stack_with_images(self, images: Images) -> StackVisualiserView:
        return self.presenter.get_stack_with_images(images)

    def create_stack_window(self,
                            stack: Images,
                            position: Qt.DockWidgetArea = Qt.DockWidgetArea.
                            RightDockWidgetArea,
                            floating: bool = False) -> StackVisualiserView:
        stack.make_name_unique(self.stack_names)
        stack_vis = StackVisualiserView(self, stack)

        # this puts the new stack window into the centre of the window
        self.splitter.addWidget(stack_vis)
        self.setCentralWidget(self.splitter)

        # add the dock widget into the main window
        self.addDockWidget(position, stack_vis)

        stack_vis.setFloating(floating)

        self.presenter.add_stack_to_dictionary(stack_vis)
        return stack_vis

    def rename_stack(self, current_name: str, new_name: str):
        self.presenter.notify(PresNotification.RENAME_STACK,
                              current_name=current_name,
                              new_name=new_name)

    def closeEvent(self, event):
        """
        Handles a request to quit the application from the user.
        """
        should_close = True

        if self.presenter.have_active_stacks and self.open_dialogs:
            # Show confirmation box asking if the user really wants to quit if
            # they have data loaded
            msg_box = QMessageBox.question(
                self,
                "Quit",
                "Are you sure you want to quit with loaded data?",
                defaultButton=QMessageBox.No)
            should_close = msg_box == QMessageBox.Yes

        if should_close:
            # Pass close event to parent
            super().closeEvent(event)

        else:
            # Ignore the close event, keeping window open
            event.ignore()

    def uncaught_exception(self, user_error_msg, log_error_msg):
        QMessageBox.critical(self, self.UNCAUGHT_EXCEPTION,
                             f"{user_error_msg}")
        getLogger(__name__).error(log_error_msg)

    def show_stack_select_dialog(self):
        dialog = MultipleStackSelect(self)
        if dialog.exec() == QDialog.DialogCode.Accepted:
            one = self.presenter.get_stack_visualiser(
                dialog.stack_one.current()).presenter.images
            two = self.presenter.get_stack_visualiser(
                dialog.stack_two.current()).presenter.images

            stack_choice = StackComparePresenter(one, two, self)
            stack_choice.show()

            return stack_choice

    def find_images_stack_title(self, images: Images) -> str:
        return self.presenter.get_stack_with_images(images).name

    def dragEnterEvent(self, event: QDragEnterEvent):
        if event.mimeData().hasUrls():
            event.acceptProposedAction()

    def dropEvent(self, event: QDropEvent):
        for url in event.mimeData().urls():
            file_path = url.toLocalFile()
            if not os.path.exists(file_path):
                continue
            if os.path.isdir(file_path):
                # Load directory as stack
                sample_loading = self.presenter.load_stacks_from_folder(
                    file_path)
                if not sample_loading:
                    QMessageBox.critical(
                        self, "Load not possible!",
                        "Please provide a directory that has .tif or .tiff files in it, or "
                        "a sub directory that do not contain dark, flat, or 180 in their title name, that represents a"
                        " sample.")
                    return
            else:
                QMessageBox.critical(
                    self, "Load not possible!",
                    "Please drag and drop only folders/directories!")
                return

    def ask_to_use_closest_to_180(self, diff_rad: float):
        """
        Asks the user if they want to use the projection that is closest to 180 degrees as the 180deg.
        :param diff_rad: The difference from the closest projection to 180 in radians.
        :return: True if the answer wants to use the closest projection, False otherwise.
        """
        diff_deg = round(np.rad2deg(diff_rad), 2)
        return QMessageBox.Yes == QMessageBox.question(
            self, "180 Projection",
            f"Unable to find a 180 degree projection. The closest projection is {str(diff_deg)} degrees away from 180. "
            f"Use anyway?")

    def create_dataset_tree_widget_item(
            self, title: str, id: uuid.UUID) -> QTreeDatasetWidgetItem:
        dataset_tree_item = QTreeDatasetWidgetItem(self.dataset_tree_widget,
                                                   id)
        dataset_tree_item.setText(0, title)
        return dataset_tree_item

    @staticmethod
    def create_child_tree_item(parent: QTreeDatasetWidgetItem,
                               dataset_id: UUID, name: str):
        child = QTreeDatasetWidgetItem(parent, dataset_id)
        child.setText(0, name)
        parent.addChild(child)

    def add_item_to_tree_view(self, item: QTreeWidgetItem):
        self.dataset_tree_widget.insertTopLevelItem(
            self.dataset_tree_widget.topLevelItemCount(), item)
        item.setExpanded(True)

    def _open_tree_menu(self, position: QPoint):
        """
        Opens the tree view menu.
        :param position: The position of the cursor when the menu was opened relative to the main window.
        """
        menu = QMenu()
        delete_action = menu.addAction("Delete")
        delete_action.triggered.connect(self._delete_container)
        menu.exec_(self.dataset_tree_widget.viewport().mapToGlobal(position))

    def _delete_container(self):
        """
        Sends the signal to the presenter to delete data corresponding with an item on the dataset tree view.
        """
        container_id = self.dataset_tree_widget.selectedItems()[0].id
        self.presenter.notify(PresNotification.REMOVE_STACK,
                              container_id=container_id)

    def _bring_stack_tab_to_front(self, item: QTreeDatasetWidgetItem):
        """
        Sends the signal to the presenter to bring a make a stack tab visible and bring it to the front.
        :param item: The QTreeDatasetWidgetItem that was double clicked.
        """
        self.presenter.notify(PresNotification.FOCUS_TAB, stack_id=item.id)

    def add_recon_to_dataset(self, recon_data: Images, stack_id: uuid.UUID):
        self.presenter.notify(PresNotification.ADD_RECON,
                              recon_data=recon_data,
                              stack_id=stack_id)
Esempio n. 6
0
class MainWindowView(BaseMainWindowView):
    active_stacks_changed = Qt.pyqtSignal()
    backend_message = Qt.pyqtSignal(bytes)

    actionRecon: QAction
    actionFilters: QAction
    actionSavuFilters: QAction

    filters: Optional[FiltersWindowView] = None
    savu_filters: Optional[SavuFiltersWindowView] = None
    recon: Optional[ReconstructWindowView] = None

    load_dialogue: Optional[MWLoadDialog] = None
    save_dialogue: Optional[MWSaveDialog] = None

    actionDebug_Me: QAction

    def __init__(self):
        super(MainWindowView, self).__init__(None, "gui/ui/main_window.ui")

        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setWindowTitle("Mantid Imaging")

        self.presenter = MainWindowPresenter(self)

        status_bar = self.statusBar()
        self.status_bar_label = QLabel("", self)
        status_bar.addPermanentWidget(self.status_bar_label)

        self.setup_shortcuts()
        self.update_shortcuts()
        find_if_latest_version(self.not_latest_version_warning)

    def setup_shortcuts(self):
        self.actionLoad.triggered.connect(self.show_load_dialogue)
        self.actionSave.triggered.connect(self.show_save_dialogue)
        self.actionExit.triggered.connect(self.close)

        self.actionOnlineDocumentation.triggered.connect(
            self.open_online_documentation)
        self.actionAbout.triggered.connect(self.show_about)

        self.actionFilters.triggered.connect(self.show_filters_window)
        self.actionSavuFilters.triggered.connect(self.show_savu_filters_window)
        self.actionRecon.triggered.connect(self.show_recon_window)

        self.active_stacks_changed.connect(self.update_shortcuts)

        self.actionDebug_Me.triggered.connect(self.attach_debugger)

    def update_shortcuts(self):
        self.actionSave.setEnabled(len(self.presenter.stack_names) > 0)

    @staticmethod
    def open_online_documentation():
        url = QtCore.QUrl("https://mantidproject.github.io/mantidimaging/")
        QtGui.QDesktopServices.openUrl(url)

    def show_about(self):
        from mantidimaging import __version__ as version_no

        msg_box = QtWidgets.QMessageBox(self)
        msg_box.setWindowTitle("About MantidImaging")
        msg_box.setTextFormat(QtCore.Qt.RichText)
        msg_box.setText(
            '<a href="https://github.com/mantidproject/mantidimaging">MantidImaging</a>'
            '<br>Version: <a href="https://github.com/mantidproject/mantidimaging/releases/tag/{0}">{0}</a>'
            .format(version_no))
        msg_box.show()

    def show_load_dialogue(self):
        self.load_dialogue = MWLoadDialog(self)
        self.load_dialogue.show()

    def execute_save(self):
        self.presenter.notify(PresNotification.SAVE)

    def execute_load(self):
        self.presenter.notify(PresNotification.LOAD)

    def show_save_dialogue(self):
        self.save_dialogue = MWSaveDialog(self, self.stack_list)
        self.save_dialogue.show()

    def show_recon_window(self):
        if not self.recon:
            self.recon = ReconstructWindowView(self)
            self.recon.show()
        else:
            self.recon.activateWindow()
            self.recon.raise_()

    def show_filters_window(self):
        if not self.filters:
            self.filters = FiltersWindowView(self)
            self.filters.show()
        else:
            self.filters.activateWindow()
            self.filters.raise_()

    def show_savu_filters_window(self):
        if not self.savu_filters:
            try:
                self.savu_filters = SavuFiltersWindowView(self)
                self.savu_filters.show()
            except RuntimeError as e:
                QtWidgets.QMessageBox.warning(self,
                                              "Savu Backend not available",
                                              str(e))
        else:
            self.savu_filters.activateWindow()
            self.savu_filters.raise_()

    @property
    def stack_list(self):
        return self.presenter.stack_list

    @property
    def stack_names(self):
        return self.presenter.stack_names

    def get_stack_visualiser(self, stack_uuid):
        return self.presenter.get_stack_visualiser(stack_uuid)

    def get_stack_history(self, stack_uuid):
        return self.presenter.get_stack_history(stack_uuid)

    def create_new_stack(self, images: Images, title: str):
        self.presenter.create_new_stack(images, title)

    def update_stack_with_images(self, images: Images):
        self.presenter.update_stack_with_images(images)

    def _create_stack_window(self,
                             stack: Images,
                             title: str,
                             position=QtCore.Qt.TopDockWidgetArea,
                             floating=False) -> Qt.QDockWidget:
        dock = Qt.QDockWidget(title, self)

        # this puts the new stack window into the centre of the window
        self.setCentralWidget(dock)

        # add the dock widget into the main window
        self.addDockWidget(position, dock)

        # we can get the stack visualiser widget with dock_widget.widget
        dock.setWidget(StackVisualiserView(self, dock, stack))

        # proof of concept above
        assert isinstance(
            dock.widget(), StackVisualiserView
        ), "Widget inside dock_widget is not an StackVisualiserView!"

        dock.setFloating(floating)

        return dock

    def remove_stack(self, obj: StackVisualiserView):
        getLogger(__name__).debug("Removing stack with uuid %s", obj.uuid)
        self.presenter.notify(PresNotification.REMOVE_STACK, uuid=obj.uuid)

    def rename_stack(self, current_name: str, new_name: str):
        self.presenter.notify(PresNotification.RENAME_STACK,
                              current_name=current_name,
                              new_name=new_name)

    def closeEvent(self, event):
        """
        Handles a request to quit the application from the user.
        """
        should_close = True

        if self.presenter.have_active_stacks:
            # Show confirmation box asking if the user really wants to quit if
            # they have data loaded
            msg_box = QtWidgets.QMessageBox.question(
                self,
                "Quit",
                "Are you sure you want to quit with loaded data?",
                defaultButton=QtWidgets.QMessageBox.No)
            should_close = msg_box == QtWidgets.QMessageBox.Yes

        if should_close:
            # allows to properly cleanup the socket IO connection
            if self.savu_filters:
                self.savu_filters.close()

            # Pass close event to parent
            super(MainWindowView, self).closeEvent(event)

        else:
            # Ignore the close event, keeping window open
            event.ignore()

    def not_latest_version_warning(self, msg: str):
        QtWidgets.QMessageBox.warning(self, "This is not the latest version",
                                      msg)

    def uncaught_exception(self, user_error_msg, log_error_msg):
        QtWidgets.QMessageBox.critical(self, "Uncaught exception",
                                       f"{user_error_msg}")
        getLogger(__name__).error(log_error_msg)

    def attach_debugger(self):
        port, accepted = QInputDialog.getInt(self,
                                             "Debug port",
                                             "Get PyCharm debug listen port",
                                             value=25252)
        if accepted:
            import pydevd_pycharm
            pydevd_pycharm.settrace('ndlt1104.isis.cclrc.ac.uk',
                                    port=port,
                                    stdoutToServer=True,
                                    stderrToServer=True)
Esempio n. 7
0
class MainWindowView(BaseMainWindowView):
    NOT_THE_LATEST_VERSION = "This is not the latest version"
    UNCAUGHT_EXCEPTION = "Uncaught exception"

    active_stacks_changed = pyqtSignal()
    backend_message = pyqtSignal(bytes)

    menuFile: QMenu
    menuWorkflow: QMenu
    menuImage: QMenu
    menuHelp: QMenu

    actionRecon: QAction
    actionFilters: QAction
    actionCompareImages: QAction
    actionSampleLoadLog: QAction
    actionLoadProjectionAngles: QAction
    actionLoad180deg: QAction
    actionLoad: QAction
    actionSave: QAction
    actionExit: QAction

    filters: Optional[FiltersWindowView] = None
    recon: Optional[ReconstructWindowView] = None

    load_dialogue: Optional[MWLoadDialog] = None
    save_dialogue: Optional[MWSaveDialog] = None

    actionDebug_Me: QAction

    def __init__(self):
        super(MainWindowView, self).__init__(None, "gui/ui/main_window.ui")

        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
        self.setWindowTitle("Mantid Imaging")

        self.presenter = MainWindowPresenter(self)

        status_bar = self.statusBar()
        self.status_bar_label = QLabel("", self)
        status_bar.addPermanentWidget(self.status_bar_label)

        self.setup_shortcuts()
        self.update_shortcuts()
        is_main_label = check_version_and_label(
            self.not_latest_version_warning)

        if not is_main_label:
            self.setWindowTitle("Mantid Imaging Unstable")
            self.setWindowIcon(
                QIcon("./images/mantid_imaging_unstable_64px.png"))

    def setup_shortcuts(self):
        self.actionLoad.triggered.connect(self.show_load_dialogue)
        self.actionSampleLoadLog.triggered.connect(self.load_sample_log_dialog)
        self.actionLoad180deg.triggered.connect(self.load_180_deg_dialog)
        self.actionLoadProjectionAngles.triggered.connect(
            self.load_projection_angles)
        self.actionSave.triggered.connect(self.show_save_dialogue)
        self.actionExit.triggered.connect(self.close)

        self.menuImage.aboutToShow.connect(self.populate_image_menu)

        self.actionOnlineDocumentation.triggered.connect(
            self.open_online_documentation)
        self.actionAbout.triggered.connect(self.show_about)

        self.actionFilters.triggered.connect(self.show_filters_window)
        self.actionRecon.triggered.connect(self.show_recon_window)

        self.actionCompareImages.triggered.connect(
            self.show_stack_select_dialog)

        self.active_stacks_changed.connect(self.update_shortcuts)

        self.actionDebug_Me.triggered.connect(self.attach_debugger)

    def populate_image_menu(self):
        self.menuImage.clear()
        current_stack = self.current_showing_stack()
        if current_stack is None:
            self.menuImage.addAction("No stack loaded!")
        else:
            populate_menu(self.menuImage, current_stack.actions)

    def current_showing_stack(self) -> Optional[StackVisualiserView]:
        for stack in self.findChildren(StackVisualiserView):
            if not stack.visibleRegion().isEmpty():
                return stack
        return None

    def update_shortcuts(self):
        enabled = len(self.presenter.stack_names) > 0
        self.actionSave.setEnabled(enabled)
        self.actionSampleLoadLog.setEnabled(enabled)
        self.actionLoad180deg.setEnabled(enabled)
        self.actionLoadProjectionAngles.setEnabled(enabled)
        self.menuWorkflow.setEnabled(enabled)
        self.menuImage.setEnabled(enabled)

    @staticmethod
    def open_online_documentation():
        url = QtCore.QUrl("https://mantidproject.github.io/mantidimaging/")
        QtGui.QDesktopServices.openUrl(url)

    def show_about(self):
        from mantidimaging import __version__ as version_no

        msg_box = QtWidgets.QMessageBox(self)
        msg_box.setWindowTitle("About MantidImaging")
        msg_box.setTextFormat(QtCore.Qt.RichText)
        msg_box.setText(
            '<a href="https://github.com/mantidproject/mantidimaging">MantidImaging</a>'
            '<br>Version: <a href="https://github.com/mantidproject/mantidimaging/releases/tag/{0}">{0}</a>'
            .format(version_no))
        msg_box.show()

    def show_load_dialogue(self):
        self.load_dialogue = MWLoadDialog(self)
        self.load_dialogue.show()

    def load_sample_log_dialog(self):
        stack_selector = StackSelectorDialog(
            main_window=self,
            title="Stack Selector",
            message="Which stack is the log being loaded for?")
        # Was closed without accepting (e.g. via x button or ESC)
        if QDialog.Accepted != stack_selector.exec():
            return
        stack_to_add_log_to = stack_selector.selected_stack

        # Open file dialog
        file_filter = "Log File (*.txt *.log)"
        selected_file, _ = QFileDialog.getOpenFileName(
            caption="Log to be loaded",
            filter=f"{file_filter};;All (*.*)",
            initialFilter=file_filter)
        # Cancel/Close was clicked
        if selected_file == "":
            return

        self.presenter.add_log_to_sample(stack_name=stack_to_add_log_to,
                                         log_file=selected_file)

        QMessageBox.information(
            self, "Load complete", f"{selected_file} was loaded as a log into "
            f"{stack_to_add_log_to}.")

    def load_180_deg_dialog(self):
        stack_selector = StackSelectorDialog(
            main_window=self,
            title="Stack Selector",
            message="Which stack is the 180 degree projection being loaded for?"
        )
        # Was closed without accepting (e.g. via x button or ESC)
        if QDialog.Accepted != stack_selector.exec():
            return
        stack_to_add_180_deg_to = stack_selector.selected_stack

        # Open file dialog
        file_filter = "Image File (*.tif *.tiff)"
        selected_file, _ = QFileDialog.getOpenFileName(
            caption="180 Degree Image",
            filter=f"{file_filter};;All (*.*)",
            initialFilter=file_filter)
        # Cancel/Close was clicked
        if selected_file == "":
            return

        _180_dataset = self.presenter.add_180_deg_to_sample(
            stack_name=stack_to_add_180_deg_to, _180_deg_file=selected_file)
        self.create_new_stack(_180_dataset,
                              self.presenter.create_stack_name(selected_file))

    LOAD_PROJECTION_ANGLES_DIALOG_MESSAGE = "Which stack are the projection angles in DEGREES being loaded for?"
    LOAD_PROJECTION_ANGLES_FILE_DIALOG_CAPTION = "File with projection angles in DEGREES"

    def load_projection_angles(self):
        stack_selector = StackSelectorDialog(
            main_window=self,
            title="Stack Selector",
            message=self.LOAD_PROJECTION_ANGLES_DIALOG_MESSAGE)
        # Was closed without accepting (e.g. via x button or ESC)
        if QDialog.Accepted != stack_selector.exec():
            return

        stack_name = stack_selector.selected_stack

        selected_file, _ = QFileDialog.getOpenFileName(
            caption=self.LOAD_PROJECTION_ANGLES_FILE_DIALOG_CAPTION,
            filter="All (*.*)")
        if selected_file == "":
            return

        pafp = ProjectionAngleFileParser(selected_file)
        projection_angles = pafp.get_projection_angles()

        self.presenter.add_projection_angles_to_sample(stack_name,
                                                       projection_angles)
        QMessageBox.information(
            self, "Load complete",
            f"Angles from {selected_file} were loaded into into "
            f"{stack_name}.")

    def execute_save(self):
        self.presenter.notify(PresNotification.SAVE)

    def execute_load(self):
        self.presenter.notify(PresNotification.LOAD)

    def show_save_dialogue(self):
        self.save_dialogue = MWSaveDialog(self, self.stack_list)
        self.save_dialogue.show()

    def show_recon_window(self):
        if not self.recon:
            self.recon = ReconstructWindowView(self)
            self.recon.show()
        else:
            self.recon.activateWindow()
            self.recon.raise_()

    def show_filters_window(self):
        if not self.filters:
            self.filters = FiltersWindowView(self)
            self.filters.show()
        else:
            self.filters.activateWindow()
            self.filters.raise_()

    @property
    def stack_list(self):
        return self.presenter.stack_list

    @property
    def stack_names(self):
        return self.presenter.stack_names

    def get_stack_visualiser(self, stack_uuid):
        return self.presenter.get_stack_visualiser(stack_uuid)

    def get_all_stack_visualisers(self):
        return self.presenter.get_all_stack_visualisers()

    def get_all_stack_visualisers_with_180deg_proj(self):
        return self.presenter.get_all_stack_visualisers_with_180deg_proj()

    def get_stack_history(self, stack_uuid):
        return self.presenter.get_stack_history(stack_uuid)

    def create_new_stack(self, images: Images, title: str):
        self.presenter.create_new_stack(images, title)

    def update_stack_with_images(self, images: Images):
        self.presenter.update_stack_with_images(images)

    def get_stack_with_images(self, images: Images) -> StackVisualiserView:
        return self.presenter.get_stack_with_images(images)

    def create_stack_window(self,
                            stack: Images,
                            title: str,
                            position=QtCore.Qt.TopDockWidgetArea,
                            floating=False) -> QDockWidget:
        dock = QDockWidget(title, self)

        # this puts the new stack window into the centre of the window
        self.setCentralWidget(dock)

        # add the dock widget into the main window
        self.addDockWidget(position, dock)

        # we can get the stack visualiser widget with dock_widget.widget
        dock.setWidget(StackVisualiserView(self, dock, stack))

        dock.setFloating(floating)

        return dock

    def remove_stack(self, obj: StackVisualiserView):
        getLogger(__name__).debug("Removing stack with uuid %s", obj.uuid)
        self.presenter.notify(PresNotification.REMOVE_STACK, uuid=obj.uuid)

    def rename_stack(self, current_name: str, new_name: str):
        self.presenter.notify(PresNotification.RENAME_STACK,
                              current_name=current_name,
                              new_name=new_name)

    def closeEvent(self, event):
        """
        Handles a request to quit the application from the user.
        """
        should_close = True

        if self.presenter.have_active_stacks:
            # Show confirmation box asking if the user really wants to quit if
            # they have data loaded
            msg_box = QtWidgets.QMessageBox.question(
                self,
                "Quit",
                "Are you sure you want to quit with loaded data?",
                defaultButton=QtWidgets.QMessageBox.No)
            should_close = msg_box == QtWidgets.QMessageBox.Yes

        if should_close:
            # Pass close event to parent
            super(MainWindowView, self).closeEvent(event)

        else:
            # Ignore the close event, keeping window open
            event.ignore()

    def not_latest_version_warning(self, msg: str):
        QtWidgets.QMessageBox.warning(self, self.NOT_THE_LATEST_VERSION, msg)

    def uncaught_exception(self, user_error_msg, log_error_msg):
        QtWidgets.QMessageBox.critical(self, self.UNCAUGHT_EXCEPTION,
                                       f"{user_error_msg}")
        getLogger(__name__).error(log_error_msg)

    def attach_debugger(self):
        port, accepted = QInputDialog.getInt(self,
                                             "Debug port",
                                             "Get PyCharm debug listen port",
                                             value=25252)
        if accepted:
            import pydevd_pycharm
            pydevd_pycharm.settrace('ndlt1104.isis.cclrc.ac.uk',
                                    port=port,
                                    stdoutToServer=True,
                                    stderrToServer=True)

    def show_stack_select_dialog(self):
        dialog = MultipleStackSelect(self)
        if dialog.exec() == QDialog.Accepted:
            one = self.presenter.get_stack_visualiser(
                dialog.stack_one.current()).presenter.images
            two = self.presenter.get_stack_visualiser(
                dialog.stack_two.current()).presenter.images

            stack_choice = StackComparePresenter(one, two, self)
            stack_choice.show()

    def set_images_in_stack(self, uuid: UUID, images: Images):
        self.presenter.set_images_in_stack(uuid, images)

    def find_images_stack_title(self, images: Images) -> str:
        return self.presenter.get_stack_with_images(images).name