diff --git a/docs/release_notes/next/feature-2027-Spectrum-ROI-Adjustable b/docs/release_notes/next/feature-2027-Spectrum-ROI-Adjustable
new file mode 100644
index 00000000000..1c6d6156b91
--- /dev/null
+++ b/docs/release_notes/next/feature-2027-Spectrum-ROI-Adjustable
@@ -0,0 +1 @@
+#2027: The Spectrum ROI details display in an adjustable table via spinboxes
\ No newline at end of file
diff --git a/mantidimaging/gui/ui/spectrum_viewer.ui b/mantidimaging/gui/ui/spectrum_viewer.ui
index b00fe2be2cc..52be844f773 100644
--- a/mantidimaging/gui/ui/spectrum_viewer.ui
+++ b/mantidimaging/gui/ui/spectrum_viewer.ui
@@ -6,8 +6,8 @@
0
0
- 921
- 739
+ 905
+ 752
@@ -351,17 +351,54 @@
-
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 200
+
+
+
+ ROI Properties
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 200
+
+
+
+ QAbstractScrollArea::AdjustToContentsOnFirstShow
+
+
+
+
+
+
+ -
+
Qt::Vertical
-
- QSizePolicy::MinimumExpanding
-
20
- 20
+ 40
diff --git a/mantidimaging/gui/windows/spectrum_viewer/presenter.py b/mantidimaging/gui/windows/spectrum_viewer/presenter.py
index d1aaa910c14..2b77a9aea23 100644
--- a/mantidimaging/gui/windows/spectrum_viewer/presenter.py
+++ b/mantidimaging/gui/windows/spectrum_viewer/presenter.py
@@ -80,6 +80,7 @@ def handle_sample_change(self, uuid: Optional['UUID']) -> None:
self.add_rits_roi()
self.view.set_normalise_error(self.model.normalise_issue())
self.show_new_sample()
+ self.view.on_visibility_change()
def handle_normalise_stack_change(self, normalise_uuid: Optional['UUID']) -> None:
if normalise_uuid == self.current_norm_stack_uuid:
@@ -118,6 +119,8 @@ def show_new_sample(self) -> None:
self.view.set_image(self.model.get_averaged_image())
self.view.spectrum_widget.spectrum_plot_widget.add_range(*self.model.tof_range)
self.view.auto_range_image()
+ if self.view.get_roi_properties_spinboxes():
+ self.view.set_roi_properties()
def handle_range_slide_moved(self, tof_range) -> None:
self.model.tof_range = tof_range
@@ -133,6 +136,10 @@ def handle_roi_moved(self, force_new_spectrums: bool = False) -> None:
self.model.set_roi(name, roi)
self.view.set_spectrum(name, self.model.get_spectrum(name, self.spectrum_mode))
+ def handle_roi_clicked(self, roi) -> None:
+ self.view.current_roi = roi.name
+ self.view.set_roi_properties()
+
def redraw_spectrum(self, name: str) -> None:
"""
Redraw the spectrum with the given name
diff --git a/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py b/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py
index 338d4cf9036..d9f3e8902ca 100644
--- a/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py
+++ b/mantidimaging/gui/windows/spectrum_viewer/spectrum_widget.py
@@ -40,6 +40,7 @@ def __init__(self, name: str, sensible_roi: SensibleROI, *args, **kwargs):
self.addScaleHandle([0, 0], [1, 1])
self.addScaleHandle([0, 1], [1, 0])
self._selected_row = None
+ self.roi.setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
self.menu = QMenu()
change_color_action = QAction("Change ROI Colour", self)
@@ -82,6 +83,10 @@ def colour(self, colour: tuple[int, int, int, int]) -> None:
def selected_row(self) -> Optional[int]:
return self._selected_row
+ def adjust_spec_roi(self, roi: SensibleROI) -> None:
+ self.setPos((roi.left, roi.top))
+ self.setSize((roi.width, roi.height))
+
class SpectrumWidget(QWidget):
"""
@@ -91,6 +96,13 @@ class SpectrumWidget(QWidget):
"""
image: MIMiniImageView
spectrum: PlotItem
+
+ range_control: LinearRegionItem
+ roi_dict: dict[Optional[str], ROI]
+ last_clicked_roi: str
+
+ range_changed = pyqtSignal(object)
+ roi_clicked = pyqtSignal(object)
roi_changed = pyqtSignal()
roiColorChangeRequested = pyqtSignal(str, tuple)
@@ -160,8 +172,9 @@ def set_roi_visibility_flags(self, name: str, visible: bool) -> None:
for handle in handles:
handle.setVisible(visible)
self.roi_dict[name].setVisible(visible)
- self.roi_dict[name].setAcceptedMouseButtons(Qt.NoButton)
+ self.roi_dict[name].setAcceptedMouseButtons(Qt.MouseButton.LeftButton)
self.roi_dict[name].sigRegionChanged.connect(self.roi_changed.emit)
+ self.roi_dict[name].sigClicked.connect(self.roi_clicked.emit)
def set_roi_alpha(self, name: str, alpha: float) -> None:
"""
@@ -190,9 +203,18 @@ def add_roi(self, roi: SensibleROI, name: str) -> None:
self.roi_dict[name] = roi_object.roi
self.max_roi_size = roi_object.size()
self.roi_dict[name].sigRegionChanged.connect(self.roi_changed.emit)
+ self.roi_dict[name].sigClicked.connect(self.roi_clicked.emit)
self.image.vb.addItem(self.roi_dict[name])
self.roi_dict[name].hoverPen = mkPen(self.roi_dict[name].colour, width=3)
+ def adjust_roi(self, new_roi: SensibleROI, roi_name: str):
+ """
+ Adjust the existing ROI with the given name.
+ @param new_roi: The new SpectrumROI to replace the existing SpectrumROI
+ @param roi_name: The name of the existing ROI.
+ """
+ self.roi_dict[roi_name].adjust_spec_roi(new_roi)
+
def get_roi(self, roi_name: str) -> SensibleROI:
"""
Get the ROI with the given name. If no name is given, the default ROI is returned.
diff --git a/mantidimaging/gui/windows/spectrum_viewer/view.py b/mantidimaging/gui/windows/spectrum_viewer/view.py
index 300269aed3a..2d875c3b47a 100644
--- a/mantidimaging/gui/windows/spectrum_viewer/view.py
+++ b/mantidimaging/gui/windows/spectrum_viewer/view.py
@@ -7,7 +7,8 @@
from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QCheckBox, QVBoxLayout, QFileDialog, QPushButton, QLabel, QAbstractItemView, QHeaderView, \
- QTabWidget, QComboBox, QSpinBox
+ QTabWidget, QComboBox, QSpinBox, QTableWidget, QTableWidgetItem, QGroupBox
+from PyQt5.QtCore import QSignalBlocker, Qt
from mantidimaging.core.utility import finder
from mantidimaging.gui.mvp_base import BaseMainWindowView
@@ -17,6 +18,7 @@
from mantidimaging.gui.widgets import RemovableRowTableView
from .spectrum_widget import SpectrumWidget
from mantidimaging.gui.windows.spectrum_viewer.roi_table_model import TableModel
+from mantidimaging.core.utility.sensible_roi import SensibleROI
import numpy as np
@@ -42,6 +44,11 @@ class SpectrumViewerWindowView(BaseMainWindowView):
bin_size_spinBox: QSpinBox
bin_step_spinBox: QSpinBox
+ roiPropertiesTableWidget: QTableWidget
+ roiPropertiesGroupBox: QGroupBox
+
+ last_clicked_roi: str
+
spectrum_widget: SpectrumWidget
def __init__(self, main_window: 'MainWindowView'):
@@ -55,6 +62,8 @@ def __init__(self, main_window: 'MainWindowView'):
self.selected_row: int = 0
self.current_roi: str = ""
self.selected_row_data: Optional[list] = None
+ self.roiPropertiesSpinBoxes: dict[str, QSpinBox] = {}
+ self.roiPropertiesLabels: dict[str, QLabel] = {}
self.presenter = SpectrumViewerWindowPresenter(self, main_window)
@@ -64,6 +73,8 @@ def __init__(self, main_window: 'MainWindowView'):
self.imageLayout.addWidget(self.spectrum_widget)
self.spectrum.range_changed.connect(self.presenter.handle_range_slide_moved)
+
+ 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)
@@ -98,7 +109,53 @@ def __init__(self, main_window: 'MainWindowView'):
self.tableView.setSelectionMode(QAbstractItemView.SingleSelection)
self.tableView.setAlternatingRowColors(True)
+ # Roi Prop table
+ self.roi_table_properties = ["Top", "Bottom", "Left", "Right"]
+ self.roi_table_properties_secondary = ["Width", "Height"]
+ self.roiPropertiesTableWidget.setColumnCount(3)
+ self.roiPropertiesTableWidget.setRowCount(3)
+ self.roiPropertiesTableWidget.setColumnWidth(0, 80)
+ self.roiPropertiesTableWidget.setColumnWidth(1, 50)
+ self.roiPropertiesTableWidget.setColumnWidth(2, 50)
+
+ for prop in self.roi_table_properties:
+ spin_box = QSpinBox()
+ if prop == "Top" or prop == "Bottom":
+ spin_box.setMaximum(self.spectrum_widget.image.image_data.shape[0])
+ if prop == "Left" or prop == "Right":
+ spin_box.setMaximum(self.spectrum_widget.image.image_data.shape[1])
+ spin_box.valueChanged.connect(self.adjust_roi)
+ self.roiPropertiesSpinBoxes[prop] = spin_box
+ for prop in self.roi_table_properties_secondary:
+ label = QLabel()
+ self.roiPropertiesLabels[prop] = label
+
+ self.roiPropertiesTableWidget.horizontalHeader().hide()
+ self.roiPropertiesTableWidget.verticalHeader().hide()
+ self.roiPropertiesTableWidget.setShowGrid(False)
+
+ roiPropertiesTableText = ["x1, x2", "y1, y2", "Size"]
+ self.roiPropertiesTableTextDict = {}
+ for text in roiPropertiesTableText:
+ item = QTableWidgetItem(text)
+ item.setFlags(Qt.ItemIsSelectable)
+ self.roiPropertiesTableTextDict[text] = item
+
+ self.roiPropertiesTableWidget.setItem(0, 0, self.roiPropertiesTableTextDict["x1, x2"])
+ self.roiPropertiesTableWidget.setCellWidget(0, 1, self.roiPropertiesSpinBoxes["Left"])
+ self.roiPropertiesTableWidget.setCellWidget(0, 2, self.roiPropertiesSpinBoxes["Right"])
+ self.roiPropertiesTableWidget.setItem(1, 0, self.roiPropertiesTableTextDict["y1, y2"])
+ self.roiPropertiesTableWidget.setCellWidget(1, 1, self.roiPropertiesSpinBoxes["Top"])
+ self.roiPropertiesTableWidget.setCellWidget(1, 2, self.roiPropertiesSpinBoxes["Bottom"])
+ self.roiPropertiesTableWidget.setItem(2, 0, self.roiPropertiesTableTextDict["Size"])
+ self.roiPropertiesTableWidget.setCellWidget(2, 1, self.roiPropertiesLabels["Width"])
+ self.roiPropertiesTableWidget.setCellWidget(2, 2, self.roiPropertiesLabels["Height"])
+
+ self.spectrum_widget.roi_changed.connect(self.set_roi_properties)
+
_ = self.roi_table_model # Initialise model
+ self.current_roi = self.last_clicked_roi = self.roi_table_model.roi_names()[0]
+ self.set_roi_properties()
def on_row_change(item, _) -> None:
"""
@@ -110,6 +167,7 @@ def on_row_change(item, _) -> None:
selected_row_data = self.roi_table_model.row_data(item.row())
self.selected_row = item.row()
self.current_roi = selected_row_data[0]
+ self.set_roi_properties()
self.tableView.selectionModel().currentRowChanged.connect(on_row_change)
@@ -156,6 +214,11 @@ 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.current_roi = self.last_clicked_roi
+ if self.roi_table_model.rowCount() == 0:
+ self.disable_roi_properties()
+ if not self.roi_table_model.rowCount() == 0:
+ self.set_roi_properties()
for roi_name, _, roi_visible in self.roi_table_model:
if roi_visible is False:
self.set_roi_alpha(0, roi_name)
@@ -169,6 +232,12 @@ def on_visibility_change(self) -> None:
if self.presenter.export_mode == ExportMode.IMAGE_MODE:
self.set_roi_alpha(255, ROI_RITS)
self.presenter.redraw_spectrum(ROI_RITS)
+ if self.current_roi != ROI_RITS:
+ self.last_clicked_roi = self.current_roi
+ self.current_roi = ROI_RITS
+ for _, spinbox in self.roiPropertiesSpinBoxes.items():
+ spinbox.setEnabled(True)
+ self.set_roi_properties()
else:
self.set_roi_alpha(0, ROI_RITS)
@@ -266,6 +335,10 @@ def set_new_roi(self) -> None:
Set a new ROI on the image
"""
self.presenter.do_add_roi()
+ for _, spinbox in self.roiPropertiesSpinBoxes.items():
+ if not spinbox.isEnabled():
+ spinbox.setEnabled(True)
+ self.set_roi_properties()
def update_roi_color_in_table(self, roi_name: str, new_color: tuple):
"""
@@ -339,6 +412,7 @@ def remove_roi(self) -> None:
if self.roi_table_model.rowCount() == 0:
self.removeBtn.setEnabled(False)
+ self.disable_roi_properties()
def clear_all_rois(self) -> None:
"""
@@ -348,6 +422,7 @@ def clear_all_rois(self) -> None:
self.spectrum_widget.spectrum_data_dict = {}
self.spectrum_widget.spectrum.clearPlots()
self.removeBtn.setEnabled(False)
+ self.disable_roi_properties()
@property
def transmission_error_mode(self) -> str:
@@ -371,3 +446,49 @@ def set_binning_visibility(self) -> None:
self.bin_size_spinBox.setHidden(hide_binning)
self.bin_step_label.setHidden(hide_binning)
self.bin_step_spinBox.setHidden(hide_binning)
+
+ def set_roi_properties(self) -> None:
+ if self.presenter.export_mode == ExportMode.IMAGE_MODE:
+ self.current_roi = ROI_RITS
+ if self.current_roi not in self.presenter.model.get_list_of_roi_names() or not self.roiPropertiesSpinBoxes:
+ return
+ else:
+ current_roi = self.presenter.model.get_roi(self.current_roi)
+ self.roiPropertiesGroupBox.setTitle(f"Roi Properties: {self.current_roi}")
+ roi_iter_order = ["Left", "Top", "Right", "Bottom"]
+ for row, pos in enumerate(current_roi):
+ with QSignalBlocker(self.roiPropertiesSpinBoxes[roi_iter_order[row]]):
+ self.roiPropertiesSpinBoxes[roi_iter_order[row]].setValue(pos)
+ self.set_roi_spinbox_ranges()
+ self.presenter.redraw_spectrum(self.current_roi)
+ self.roiPropertiesLabels["Width"].setText(str(current_roi.width))
+ self.roiPropertiesLabels["Height"].setText(str(current_roi.height))
+ for spinbox in self.roiPropertiesSpinBoxes.values():
+ spinbox.setEnabled(True)
+
+ def adjust_roi(self) -> None:
+ roi_iter_order = ["Left", "Top", "Right", "Bottom"]
+ new_points = [self.roiPropertiesSpinBoxes[prop].value() for prop in roi_iter_order]
+ new_roi = SensibleROI().from_list(new_points)
+ self.presenter.model.set_roi(self.current_roi, new_roi)
+ self.spectrum_widget.adjust_roi(new_roi, self.current_roi)
+
+ def set_roi_spinbox_ranges(self):
+ self.roiPropertiesSpinBoxes["Left"].setMaximum(self.roiPropertiesSpinBoxes["Right"].value() - 1)
+ self.roiPropertiesSpinBoxes["Right"].setMinimum(self.roiPropertiesSpinBoxes["Left"].value() + 1)
+ self.roiPropertiesSpinBoxes["Top"].setMaximum(self.roiPropertiesSpinBoxes["Bottom"].value() - 1)
+ self.roiPropertiesSpinBoxes["Bottom"].setMinimum(self.roiPropertiesSpinBoxes["Top"].value() + 1)
+
+ def disable_roi_properties(self):
+ self.roiPropertiesGroupBox.setTitle("Roi Properties: None selected")
+ self.last_clicked_roi = "roi"
+ for _, spinbox in self.roiPropertiesSpinBoxes.items():
+ with QSignalBlocker(spinbox):
+ spinbox.setMinimum(0)
+ spinbox.setValue(0)
+ spinbox.setDisabled(True)
+ for _, label in self.roiPropertiesLabels.items():
+ label.setText("0")
+
+ def get_roi_properties_spinboxes(self):
+ return self.roiPropertiesSpinBoxes