# Copyright (C) 2021 ISIS Rutherford Appleton Laboratory UKRI
# SPDX - License - Identifier: GPL-3.0-or-later
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Any
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QFileDialog, QPushButton, QLabel, QAbstractItemView, QHeaderView, \
QTabWidget, QComboBox, QSpinBox, QGroupBox, QActionGroup, QAction
from PyQt5.QtCore import QSignalBlocker, QModelIndex, pyqtSignal
from mantidimaging.core.utility import finder
from mantidimaging.core.utility.sensible_roi import SensibleROI
from mantidimaging.gui.mvp_base import BaseMainWindowView, BaseWidget
from mantidimaging.gui.widgets.dataset_selector import DatasetSelectorWidgetView
from .model import ROI_RITS, allowed_modes
from .presenter import SpectrumViewerWindowPresenter, ExportMode
from mantidimaging.gui.widgets import RemovableRowTableView
from .spectrum_widget import SpectrumWidget
from mantidimaging.gui.windows.spectrum_viewer.roi_table_model import TableModel
from mantidimaging.gui.widgets.spectrum_widgets.tof_properties import ExperimentSetupFormWidget
from mantidimaging.gui.widgets.spectrum_widgets.roi_selection_widget import ROISelectionWidget
import numpy as np
if TYPE_CHECKING:
from mantidimaging.gui.windows.main import MainWindowView # noqa:F401 # pragma: no cover
from uuid import UUID
[docs]
class SpectrumViewerWindowView(BaseMainWindowView):
table_view: ROITableWidget
sampleStackSelector: DatasetSelectorWidgetView
normaliseStackSelector: DatasetSelectorWidgetView
normaliseCheckBox: QCheckBox
normalise_ShutterCount_CheckBox: QCheckBox
imageLayout: QVBoxLayout
fittingLayout: QVBoxLayout
exportLayout: QVBoxLayout
exportButton: QPushButton
exportTabs: QTabWidget
normaliseErrorIcon: QLabel
shuttercountErrorIcon: QLabel
normalise_error_issue: str = ""
shuttercount_error_issue: str = ""
image_output_mode_combobox: QComboBox
transmission_error_mode_combobox: QComboBox
bin_size_spinBox: QSpinBox
bin_step_spinBox: QSpinBox
roi_properties_widget: ROIPropertiesTableWidget
spectrum_widget: SpectrumWidget
experimentSetupGroupBox: QGroupBox
experimentSetupFormWidget: ExperimentSetupFormWidget
def __init__(self, main_window: MainWindowView):
super().__init__(None, 'gui/ui/spectrum_viewer.ui')
self.main_window = main_window
icon_path = finder.ROOT_PATH + "/gui/ui/images/exclamation-triangle-red.png"
self.normalise_error_icon_pixmap = QPixmap(icon_path)
self.presenter = SpectrumViewerWindowPresenter(self, main_window)
self.spectrum_widget = SpectrumWidget(main_window)
self.spectrum = self.spectrum_widget.spectrum_plot_widget
self.imageLayout.addWidget(self.spectrum_widget)
self.fittingLayout.addWidget(QLabel("fitting"))
self.exportLayout.addWidget(QLabel("export"))
self.spectrum.range_changed.connect(self.presenter.handle_range_slide_moved)
self.roiSelectionWidget = ROISelectionWidget(self)
self.fittingFormLayout.layout().addWidget(self.roiSelectionWidget)
self.spectrum_widget.roi_clicked.connect(self.presenter.handle_roi_clicked)
self.spectrum_widget.roi_changed.connect(self.presenter.handle_roi_moved)
self.spectrum_widget.roiColorChangeRequested.connect(self.presenter.change_roi_colour)
self.spectrum_right_click_menu = self.spectrum.spectrum_viewbox.menu
self.units_menu = self.spectrum_right_click_menu.addMenu("Units")
self.tof_mode_select_group = QActionGroup(self)
for mode in allowed_modes.keys():
action = QAction(mode, self.tof_mode_select_group)
action.setCheckable(True)
action.setObjectName(mode)
self.units_menu.addAction(action)
action.triggered.connect(self.presenter.handle_tof_unit_change_via_menu)
if mode == "Image Index":
action.setChecked(True)
if self.presenter.model.tof_data is None:
self.tof_mode_select_group.setEnabled(False)
self.current_dataset_id: UUID | None = None
self.sampleStackSelector.stack_selected_uuid.connect(self.presenter.handle_sample_change)
self.sampleStackSelector.stack_selected_uuid.connect(self.presenter.handle_button_enabled)
self.normaliseStackSelector.stack_selected_uuid.connect(self.presenter.handle_normalise_stack_change)
self.normaliseStackSelector.stack_selected_uuid.connect(self.presenter.handle_button_enabled)
self.normaliseCheckBox.stateChanged.connect(self.normaliseStackSelector.setEnabled)
self.normaliseCheckBox.stateChanged.connect(self.presenter.handle_enable_normalised)
self.normaliseCheckBox.stateChanged.connect(self.presenter.handle_button_enabled)
self.normalise_ShutterCount_CheckBox.stateChanged.connect(self.presenter.set_shuttercount_error)
self.normalise_ShutterCount_CheckBox.stateChanged.connect(self.presenter.handle_button_enabled)
self.exportTabs.currentChanged.connect(self.presenter.handle_export_tab_change)
self.image_output_mode_combobox.currentTextChanged.connect(self.set_binning_visibility)
self.set_binning_visibility()
# ROI action buttons
self.addBtn.clicked.connect(self.set_new_roi)
self.removeBtn.clicked.connect(self.remove_roi)
self._configure_dropdown(self.sampleStackSelector)
self._configure_dropdown(self.normaliseStackSelector)
self.sampleStackSelector.select_eligible_stack()
self.try_to_select_relevant_normalise_stack("Flat")
self.presenter.handle_tof_unit_change()
self.exportButton.clicked.connect(self.presenter.handle_export_csv)
self.exportButtonRITS.clicked.connect(self.presenter.handle_rits_export)
self.table_view.clicked.connect(self.handle_table_click)
self.roi_properties_widget.roi_changed.connect(self.presenter.do_adjust_roi)
self.spectrum_widget.roi_changed.connect(self.set_roi_properties)
self.set_roi_properties()
self.experimentSetupFormWidget = ExperimentSetupFormWidget(self.experimentSetupGroupBox)
self.experimentSetupFormWidget.flight_path = 56.4
self.experimentSetupFormWidget.connect_value_changed(self.presenter.handle_experiment_setup_properties_change)
self.table_view.selection_changed.connect(self.set_roi_properties)
self.table_view.name_changed.connect(self.spectrum_widget.rename_roi)
self.table_view.name_changed.connect(self.set_roi_properties)
self.table_view.visibility_changed.connect(self.on_visibility_change)
self.formTabs.currentChanged.connect(self.handle_change_tab)
[docs]
def show(self) -> None:
super().show()
self.activateWindow()
[docs]
def cleanup(self) -> None:
self.sampleStackSelector.unsubscribe_from_main_window()
self.normaliseStackSelector.unsubscribe_from_main_window()
self.main_window.spectrum_viewer = None
[docs]
def handle_change_tab(self, tab_index: int):
self.imageTabs.setCurrentIndex(tab_index)
[docs]
def on_visibility_change(self) -> None:
"""
When the visibility of an ROI is changed, update the visibility of the ROI in the spectrum widget
"""
if self.presenter.export_mode == ExportMode.ROI_MODE:
self.set_roi_visibility_flags(ROI_RITS, visible=False)
if self.table_view.row_count() == 0:
self.disable_roi_properties()
else:
self.set_roi_properties()
for row in range(self.table_view.row_count()):
roi_name = self.table_view.get_roi_name_by_row(row)
roi_visible = self.table_view.get_roi_visibility_by_row(row)
self.set_roi_visibility_flags(roi_name, visible=roi_visible)
if roi_visible:
self.presenter.redraw_spectrum(roi_name)
elif self.presenter.export_mode == ExportMode.IMAGE_MODE:
for row in range(self.table_view.row_count()):
roi_name = self.table_view.get_roi_name_by_row(row)
self.set_roi_visibility_flags(roi_name, visible=False)
self.set_roi_visibility_flags(ROI_RITS, visible=True)
self.presenter.redraw_spectrum(ROI_RITS)
self.set_roi_properties()
def _configure_dropdown(self, selector: DatasetSelectorWidgetView) -> None:
selector.presenter.show_stacks = True
selector.subscribe_to_main_window(self.main_window)
[docs]
def try_to_select_relevant_normalise_stack(self, name: str) -> None:
self.normaliseStackSelector.try_to_select_relevant_stack(name)
[docs]
def get_normalise_stack(self) -> UUID | None:
return self.normaliseStackSelector.current()
[docs]
def get_csv_filename(self) -> Path | None:
path = QFileDialog.getSaveFileName(self, "Save CSV file", "", "CSV file (*.csv)")[0]
if path:
return Path(path)
else:
return None
[docs]
def get_rits_export_directory(self) -> Path | None:
"""
Get the path to save the RITS file too
"""
path = QFileDialog.getExistingDirectory(self, "Select Directory", "", QFileDialog.ShowDirsOnly)
if path:
return Path(path)
else:
return None
[docs]
def get_rits_export_filename(self) -> Path | None:
"""
Get the path to save the RITS file too
"""
path = QFileDialog.getSaveFileName(self, "Save DAT file", "", "DAT file (*.dat)")[0]
if path:
return Path(path)
else:
return None
[docs]
def set_image(self, image_data: np.ndarray, autoLevels: bool = True) -> None:
self.spectrum_widget.image.setImage(image_data, autoLevels=autoLevels)
[docs]
def set_spectrum(self, name: str, spectrum_data: np.ndarray) -> None:
"""
Try to set the spectrum data for a given ROI assuming the
roi may not exist in the spectrum widget yet depending on when method is called
"""
self.spectrum_widget.spectrum_data_dict[name] = spectrum_data
self.spectrum_widget.spectrum.clearPlots()
self.show_visible_spectrums()
[docs]
def clear(self) -> None:
self.spectrum_widget.spectrum_data_dict = {}
self.spectrum_widget.image.setImage(np.zeros((1, 1)))
self.spectrum_widget.spectrum.clearPlots()
[docs]
def auto_range_image(self) -> None:
self.spectrum_widget.image.vb.autoRange()
[docs]
def set_normalise_error(self, norm_issue: str) -> None:
self.normalise_error_issue = norm_issue
self.display_normalise_error()
[docs]
def display_normalise_error(self) -> None:
if self.normalise_error_issue and self.normalisation_enabled():
self.normaliseErrorIcon.setPixmap(self.normalise_error_icon_pixmap)
self.normaliseErrorIcon.setToolTip(self.normalise_error_issue)
else:
self.normaliseErrorIcon.setPixmap(QPixmap())
self.normaliseErrorIcon.setToolTip("")
[docs]
def normalisation_enabled(self) -> bool:
return self.normaliseCheckBox.isChecked()
[docs]
def set_shuttercount_error(self, shuttercount_issue: str) -> None:
self.shuttercount_error_issue = shuttercount_issue
self.display_shuttercount_error()
[docs]
def handle_shuttercount_change(self) -> None:
self.presenter.set_shuttercount_error(self.normalise_ShutterCount_CheckBox.isChecked())
self.normalise_ShutterCount_CheckBox.setEnabled(self.shuttercount_error_issue == "")
[docs]
def display_shuttercount_error(self) -> None:
if (self.shuttercount_error_issue and self.normalisation_enabled() and self.shuttercount_norm_enabled()
and self.normalise_error_issue == ""):
self.shuttercountErrorIcon.setPixmap(self.normalise_error_icon_pixmap)
self.shuttercountErrorIcon.setToolTip(self.shuttercount_error_issue)
else:
self.shuttercountErrorIcon.setPixmap(QPixmap())
self.shuttercountErrorIcon.setToolTip("")
[docs]
def update_roi_dropdown(self) -> None:
""" Updates the ROI dropdown menu with the available ROIs. """
roi_names = self.presenter.get_roi_names()
self.roiSelectionWidget.update_roi_list(roi_names)
[docs]
def shuttercount_norm_enabled(self) -> bool:
return self.normalise_ShutterCount_CheckBox.isChecked()
[docs]
def set_new_roi(self) -> None:
"""
Set a new ROI on the image
"""
self.presenter.do_add_roi()
self.roi_properties_widget.enable_widgets(True)
self.set_roi_properties()
[docs]
def handle_table_click(self, index: QModelIndex) -> None:
if index.isValid() and index.column() == 1:
roi_name = self.table_view.get_roi_name_by_row(index.row())
self.set_spectum_roi_color(roi_name)
[docs]
def set_spectum_roi_color(self, roi_name: str) -> None:
spectrum_roi = self.spectrum_widget.roi_dict[roi_name]
spectrum_roi.change_color_action.trigger()
[docs]
def set_roi_visibility_flags(self, roi_name: str, visible: bool) -> None:
"""
Set the visibility for the selected ROI and update the spectrum to reflect the change.
A check is made on the spectrum to see if it exists as it may not have been created yet.
@param visible: Whether the ROI is visible.
"""
self.spectrum_widget.set_roi_visibility_flags(roi_name, visible=visible)
if not visible:
self.spectrum_widget.spectrum_data_dict[roi_name] = None
self.spectrum_widget.spectrum.clearPlots()
self.spectrum_widget.spectrum.update()
self.show_visible_spectrums()
[docs]
def show_visible_spectrums(self) -> None:
for key, value in self.spectrum_widget.spectrum_data_dict.items():
if value is not None and key in self.spectrum_widget.roi_dict:
self.spectrum_widget.spectrum.plot(self.presenter.model.tof_data,
value,
name=key,
pen=self.spectrum_widget.roi_dict[key].colour)
[docs]
def add_roi_table_row(self, name: str, colour: tuple[int, int, int, int]) -> None:
"""
Add a new row to the ROI table
@param name: The name of the ROI
@param colour: The colour of the ROI
"""
self.table_view.add_row(name, colour, self.presenter.get_roi_names())
self.removeBtn.setEnabled(True)
[docs]
def remove_roi(self) -> None:
"""
Clear the selected ROI in the table view
"""
roi_name = self.table_view.get_roi_name_by_row(self.table_view.selected_row)
roi_object = self.spectrum_widget.roi_dict[roi_name]
self.table_view.remove_row(self.table_view.selected_row)
self.presenter.do_remove_roi(roi_name)
self.spectrum_widget.spectrum_data_dict.pop(roi_name)
self.presenter.handle_roi_moved(roi_object)
if self.table_view.roi_table_model.rowCount() == 0:
self.removeBtn.setEnabled(False)
self.disable_roi_properties()
else:
self.set_roi_properties()
@property
def transmission_error_mode(self) -> str:
return self.transmission_error_mode_combobox.currentText()
@property
def image_output_mode(self) -> str:
return self.image_output_mode_combobox.currentText()
@property
def bin_size(self) -> int:
return self.bin_size_spinBox.value()
@property
def bin_step(self) -> int:
return self.bin_step_spinBox.value()
[docs]
def set_binning_visibility(self) -> None:
hide_binning = self.image_output_mode != "2D Binned"
self.bin_size_label.setHidden(hide_binning)
self.bin_size_spinBox.setHidden(hide_binning)
self.bin_step_label.setHidden(hide_binning)
self.bin_step_spinBox.setHidden(hide_binning)
@property
def tof_units_mode(self) -> str:
return self.tof_mode_select_group.checkedAction().text()
[docs]
def set_roi_properties(self) -> None:
if self.presenter.export_mode == ExportMode.IMAGE_MODE:
roi_name = ROI_RITS
else:
roi_name = self.table_view.get_roi_name_by_row(self.table_view.selected_row)
if roi_name not in self.presenter.view.spectrum_widget.roi_dict:
return
current_roi = self.presenter.view.spectrum_widget.get_roi(roi_name)
self.roi_properties_widget.set_roi_name(roi_name)
self.roi_properties_widget.set_roi_values(current_roi)
self.presenter.redraw_spectrum(roi_name)
self.roi_properties_widget.enable_widgets(True)
[docs]
def disable_roi_properties(self) -> None:
self.roi_properties_widget.set_roi_name("None selected")
self.roi_properties_widget.enable_widgets(False)
[docs]
def setup_roi_properties_spinboxes(self) -> None:
assert self.spectrum_widget.image.image_data is not None
self.roi_properties_widget.set_roi_limits(self.spectrum_widget.image.image_data.shape)