Source code for ewoksid24.gui.xasmap_select

import json
import os
from typing import List
from typing import Optional
from typing import Tuple

from silx.gui import icons
from silx.gui import qt

from .settings import get_settings


[docs] class XasMapInputWidget(qt.QWidget): # Signal to notify the main window when the input is changed data_changed = qt.pyqtSignal(dict) def __init__(self, parent=None): super().__init__(parent) self._settings: Optional[qt.QSettings] = None # Buttons to add/remove XAS maps xasmap_box = qt.QGroupBox("XAS maps:") xasmap_layout = qt.QVBoxLayout(xasmap_box) # List of XAS maps self._xasmaps_widget = qt.QListWidget() self._xasmaps_widget.setSelectionMode(qt.QListWidget.SingleSelection) self._xasmaps_widget.itemSelectionChanged.connect(self._select_xasmap) self._xasmaps_widget.setContextMenuPolicy(qt.Qt.CustomContextMenu) self._xasmaps_widget.customContextMenuRequested.connect( self._xasmaps_context_menu ) delete_button = qt.QPushButton("Remove") delete_button.clicked.connect(self._remove_selected_xasmap) # Layout to hold the current XAS maps xasmap_layout.addWidget(self._xasmaps_widget) xasmap_layout.addWidget(delete_button, alignment=qt.Qt.AlignRight) # Layout to hold the XAS map parameters parameters_box = qt.QGroupBox("Parameters:") parameters_layout = qt.QVBoxLayout(parameters_box) # XAS map parameters: files self._last_dir = os.path.expanduser("~") replace_files_button = qt.QToolButton() replace_files_button.setIcon(icons.getQIcon("document-open")) replace_files_button.setToolTip("Replace file") replace_files_button.clicked.connect(self._replace_files) add_files_button = qt.QToolButton() add_files_button.setIcon(icons.getQIcon("folder")) add_files_button.setToolTip("Add files") add_files_button.clicked.connect(self._add_files) self._filenames_widget = qt.QListWidget() self._filenames_widget.setSelectionMode(qt.QListWidget.ExtendedSelection) self._filenames_widget.setContextMenuPolicy(qt.Qt.CustomContextMenu) self._filenames_widget.customContextMenuRequested.connect( self._filenames_context_menu ) self._filenames_widget.itemChanged.connect(self._parameters_changed) self._filenames_widget.model().rowsInserted.connect(self._parameters_changed) self._filenames_widget.model().rowsRemoved.connect(self._parameters_changed) file_layout = qt.QHBoxLayout() file_layout.addWidget(self._filenames_widget) file_selection_layout = qt.QVBoxLayout() file_selection_layout.addWidget(replace_files_button) file_selection_layout.addWidget(add_files_button) file_layout.addLayout(file_selection_layout) file_row_widget = qt.QWidget() file_row_widget.setLayout(file_layout) # XAS map parameters: all others parameter_form = qt.QFormLayout() parameters_layout.addLayout(parameter_form) self._dim0_counter_input = qt.QLineEdit() self._dim1_counter_input = qt.QLineEdit() self._energy_counter_input = qt.QLineEdit() self._mu_counter_input = qt.QLineEdit() self._scan_range_input = qt.QLineEdit() self._exclude_scans_input = qt.QLineEdit() self._dim0_counter_input.textChanged.connect(self._parameters_changed) self._dim1_counter_input.textChanged.connect(self._parameters_changed) self._energy_counter_input.textChanged.connect(self._parameters_changed) self._mu_counter_input.textChanged.connect(self._parameters_changed) self._scan_range_input.textChanged.connect(self._parameters_changed) self._exclude_scans_input.textChanged.connect(self._parameters_changed) parameter_form.addRow("Files", file_row_widget) parameter_form.addRow("X-Axis", self._dim0_counter_input) parameter_form.addRow("Y-Axis", self._dim1_counter_input) parameter_form.addRow("Energy", self._energy_counter_input) parameter_form.addRow("Mu", self._mu_counter_input) parameter_form.addRow("Scan ranges", self._scan_range_input) parameter_form.addRow("Exclude scans", self._exclude_scans_input) button_layout = qt.QHBoxLayout() parameters_layout.addLayout(button_layout) create_button = qt.QPushButton("New") create_button.clicked.connect(self._create_xasmap) self._modify_button = qt.QPushButton("Update") self._modify_button.setEnabled(False) self._modify_button.clicked.connect(self._modify_xasmap) self._reset_button = qt.QPushButton("Reset") self._reset_button.setEnabled(False) self._reset_button.clicked.connect(self._select_xasmap) button_layout.addWidget(create_button) button_layout.addWidget(self._modify_button) button_layout.addWidget(self._reset_button) # Add all components to the main layout main_layout = qt.QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.addWidget(xasmap_box) main_layout.addWidget(parameters_box)
[docs] def add_xasmap( self, filenames: Optional[List[str]] = None, dim0_counter: Optional[str] = None, dim1_counter: Optional[str] = None, energy_counter: Optional[str] = None, mu_counter: Optional[str] = None, scan_ranges: Optional[List[List[int]]] = None, exclude_scans: Optional[List[int]] = None, ) -> None: do_create = False if filenames: self._set_filenames(filenames) do_create = True if dim0_counter is not None: self._set_parameter_as_string(self._dim0_counter_input, dim0_counter) do_create = True if dim1_counter is not None: self._set_parameter_as_string(self._dim1_counter_input, dim1_counter) do_create = True if energy_counter is not None: self._set_parameter_as_string(self._energy_counter_input, energy_counter) do_create = True if mu_counter is not None: self._set_parameter_as_string(self._mu_counter_input, mu_counter) do_create = True if scan_ranges: scan_ranges = _parse_list_of_lists_of_ints(scan_ranges, two_tuple=True) self._set_scan_ranges(scan_ranges) do_create = True elif filenames: self._set_scan_ranges(None) if exclude_scans: exclude_scans = _parse_list_of_lists_of_ints(exclude_scans, two_tuple=False) self._set_exclude_scans(exclude_scans) do_create = True elif filenames: self._set_exclude_scans(None) if do_create: self._create_xasmap()
[docs] def get_current_xasmap(self) -> Optional[dict]: return self._get_selected_xasmap()
[docs] def save_settings(self) -> None: if self._settings is None: return xasmaps = self._get_xasmaps() xasmap_index = self._xasmaps_widget.currentRow() self._settings.beginGroup("XasMapInputWidget") self._settings.setValue("xasmaps", xasmaps) self._settings.setValue("xasmap_index", xasmap_index) self._settings.endGroup()
[docs] def load_settings(self) -> None: """Load input sets from QSettings and restore the form fields.""" if self._settings is None: self._settings = get_settings() self._settings.beginGroup("XasMapInputWidget") xasmaps = self._settings.value("xasmaps", None) xasmap_index = int(self._settings.value("xasmap_index", -1)) self._settings.endGroup() self._set_xasmaps(xasmaps) if xasmap_index != -1: self._xasmaps_widget.setCurrentRow(xasmap_index)
def _add_files(self) -> None: """Open a QFileDialog to select multiple files to add to the current ones.""" filenames, _ = qt.QFileDialog.getOpenFileNames( self, "Select XAS Data Files", self._last_dir, "HDF5 Files (*.h5);;All Files (*)", ) self._add_filenames(filenames) def _replace_files(self) -> None: """Open a QFileDialog to select a single file to replace the current ones.""" filename, _ = qt.QFileDialog.getOpenFileName( self, "Select XAS Data File", self._last_dir, "HDF5 Files (*.h5);;All Files (*)", ) self._set_filenames([filename]) def _get_filenames(self) -> List[str]: nitems = self._filenames_widget.count() return [ self._filenames_widget.item(i).data(qt.Qt.UserRole) for i in range(nitems) ] def _set_filenames(self, filenames: Optional[List[Tuple[str, str]]]) -> None: self._filenames_widget.clear() if not filenames: return for filename in filenames: _ = self._add_filename(filename) def _add_filenames(self, filenames: Optional[List[str]]) -> None: if not filenames: return for filename in filenames: self._add_filename(filename) self.save_settings() def _add_filename(self, filename: str) -> None: self._last_dir = os.path.dirname(filename) filelabel = os.path.basename(filename) filename = os.path.abspath(filename) item = qt.QListWidgetItem(filelabel) item.setData(qt.Qt.UserRole, filename) self._filenames_widget.addItem(item) def _filenames_context_menu(self, pos) -> None: """Show the context menu for right-click on the file list.""" context_menu = qt.QMenu(self) duplicate_action = context_menu.addAction("Duplicate") remove_action = context_menu.addAction("Remove") action = context_menu.exec_(self._filenames_widget.mapToGlobal(pos)) if action == remove_action: self._remove_selected_filenames() elif action == duplicate_action: self._duplicate_selected_filenames() def _remove_selected_filenames(self) -> None: """Remove the selected files from the file list.""" selected_items = self._filenames_widget.selectedItems() if selected_items is None: return for selected_item in selected_items: index = self._filenames_widget.row(selected_item) self._filenames_widget.takeItem(index) self.save_settings() def _duplicate_selected_filenames(self) -> None: """Duplicate the selected files in the file list.""" selected_items = self._filenames_widget.selectedItems() if selected_items is None: return for selected_item in selected_items: filename = selected_item.data(qt.Qt.UserRole) self._add_filename(filename) self.save_settings() def _get_xasmaps(self) -> List[dict]: nitems = self._xasmaps_widget.count() return [ self._xasmaps_widget.item(i).data(qt.Qt.UserRole) for i in range(nitems) ] def _set_xasmaps(self, xasmaps: Optional[List[dict]]) -> None: self._xasmaps_widget.clear() if xasmaps: for xasmap in xasmaps: self._add_xasmap(xasmap) def _add_xasmap(self, xasmap: dict) -> None: label = self._get_xasmap_id(xasmap) item = qt.QListWidgetItem(label) item.setData(qt.Qt.UserRole, xasmap) self._xasmaps_widget.addItem(item) def _select_last_xasmap(self) -> None: count = self._xasmaps_widget.count() if count > 0: self._xasmaps_widget.setCurrentRow(count - 1) def _get_xasmap_id(self, xasmap: dict) -> str: filenames = xasmap.get("filenames", []) scan_ranges = xasmap.get("scan_ranges", []) if not scan_ranges: scan_ranges = [None] * len(filenames) names = [] for filename, scan_range in zip(filenames, scan_ranges): name = os.path.basename(filename) if scan_range: name = f"{name}: {scan_range[0]} -> {scan_range[1]}" names.append(name) return ", ".join(names) def _get_selected_xasmap(self) -> Optional[dict]: selected_item = self._xasmaps_widget.currentItem() if selected_item: return selected_item.data(qt.Qt.UserRole) def _xasmaps_context_menu(self, pos) -> None: """Show the context menu for right-click on the XAS map list.""" context_menu = qt.QMenu(self) remove_action = context_menu.addAction("Remove") action = context_menu.exec_(self._xasmaps_widget.mapToGlobal(pos)) if action == remove_action: self._remove_selected_xasmap() def _remove_selected_xasmap(self) -> None: """Remove the current XAS map.""" selected_index = self._xasmaps_widget.currentRow() if selected_index != -1: self._xasmaps_widget.takeItem(selected_index) self.save_settings() self._select_xasmap() def _create_xasmap(self) -> None: """Add the current parameters as a new XAS map.""" xasmap = self._get_edited_xasmap() if xasmap: self._add_xasmap(xasmap) self.save_settings() self._select_last_xasmap() def _emit_xasmap(self, xasmap: dict) -> None: self.data_changed.emit(xasmap) def _modify_xasmap(self) -> None: selected_item = self._xasmaps_widget.currentItem() if selected_item is None: return xasmap = self._get_edited_xasmap() if xasmap is None: return selected_item.setData(qt.Qt.UserRole, xasmap) label = self._get_xasmap_id(xasmap) selected_item.setText(label) self.save_settings() self._select_xasmap() def _get_edited_xasmap(self) -> Optional[dict]: try: filenames = self._get_xasmap_filenames() scan_ranges = self._get_scan_ranges() exclude_scans = self._get_exclude_scans() dim0_counter = self._get_parameter_as_string( self._dim0_counter_input, "X-Axis" ) dim1_counter = self._get_parameter_as_string( self._dim1_counter_input, "Y-Axis" ) energy_counter = self._get_parameter_as_string( self._energy_counter_input, "Energy" ) mu_counter = self._get_parameter_as_string(self._mu_counter_input, "Mu") except _Return: return None return { "filenames": filenames, "dim0_counter": dim0_counter, "dim1_counter": dim1_counter, "energy_counter": energy_counter, "mu_counter": mu_counter, "scan_ranges": scan_ranges, "exclude_scans": exclude_scans, } def _select_xasmap(self) -> None: xasmap = self._get_selected_xasmap() if not xasmap: return self._set_parameter_as_string( self._dim0_counter_input, xasmap.get("dim0_counter") ) self._set_parameter_as_string( self._dim1_counter_input, xasmap.get("dim1_counter") ) self._set_parameter_as_string( self._energy_counter_input, xasmap.get("energy_counter") ) self._set_parameter_as_string(self._mu_counter_input, xasmap.get("mu_counter")) self._set_scan_ranges(xasmap.get("scan_ranges")) self._set_exclude_scans(xasmap.get("exclude_scans")) self._set_filenames(xasmap.get("filenames")) self._update_button_states(False) self._emit_xasmap(xasmap) def _get_xasmap_filenames(self) -> List[str]: filenames = self._get_filenames() if not filenames: qt.QMessageBox.warning( self, "No Files Selected", "Please select at least one file." ) raise _Return return filenames def _get_parameter_as_string(self, w: qt.QLineEdit, name: str) -> str: value = w.text() if not value: qt.QMessageBox.warning(self, "Invalid Input", f"'{name}' is not defined") raise _Return return value def _set_parameter_as_string(self, w: qt.QLineEdit, value: Optional[str]) -> None: if value is None: w.setText("") else: w.setText(str(value)) def _get_scan_ranges(self) -> Optional[List[List[int]]]: scan_ranges = self._scan_range_input.text() or None if not scan_ranges: return None try: return _parse_list_of_lists_of_ints(scan_ranges, two_tuple=True) except (TypeError, json.JSONDecodeError) as e: qt.QMessageBox.warning( self, "Invalid Value", f"The scan ranges must be a list of lists of two integers ({e}).", ) raise _Return def _set_scan_ranges(self, scan_ranges: Optional[List[List[int]]]) -> None: if scan_ranges: self._scan_range_input.setText(json.dumps(scan_ranges)) else: self._scan_range_input.setText("") def _get_exclude_scans(self) -> Optional[List[List[int]]]: exclude_scans = self._exclude_scans_input.text() or None if not exclude_scans: return None try: return _parse_list_of_lists_of_ints(exclude_scans, two_tuple=False) except (TypeError, json.JSONDecodeError) as e: qt.QMessageBox.warning( self, "Invalid Value", f"The scans to exclude must be a list of lists of integers ({e}).", ) raise _Return def _set_exclude_scans(self, exclude_scans: Optional[List[List[int]]]) -> None: if exclude_scans: self._exclude_scans_input.setText(json.dumps(exclude_scans)) else: self._exclude_scans_input.setText("") def _parameters_changed(self): self._update_button_states(True) def _update_button_states(self, enable: bool) -> None: self._modify_button.setEnabled(enable) self._reset_button.setEnabled(enable)
def _parse_list_of_lists_of_ints( value: str, two_tuple: bool ) -> Optional[List[List[int]]]: value = json.loads(value) if value is None: return None if isinstance(value, int): if two_tuple: value = [[value, value]] else: value = [[value]] if not isinstance(value, list): raise TypeError("Not a list") if len({isinstance(v, int) for v in value}) == 2: raise TypeError("List is a mixture of integers and other types") if all(isinstance(v, int) for v in value): value = [value] for lst in value: if not all(isinstance(v, int) for v in lst): raise TypeError("Sub-list must contain integers") if two_tuple and len(lst) != 2: raise TypeError("Sub-list only has two values (first and last scan number)") return value class _Return(Exception): pass