Source code for pymodaq_gui.managers.settings.utils

from collections.abc import Iterable
from pathlib import Path
from typing import Union, Any, TYPE_CHECKING, Sequence

from qtpy import QtWidgets, QtCore

from qtpy.QtCore import QMimeData, Qt, QModelIndex
from qtpy.QtWidgets import QDialogButtonBox, QDialog

from pymodaq_gui.managers.settings.subentries import SubEntryHandlerFactory, SubEntryHandlerTypes, \
    SubEntry
from pymodaq_utils.math_utils import get_si_prefix
from pymodaq_utils.utils import capitalize, read_binary_and_deserialize
from pymodaq_utils.logger import set_logger, get_module_name
from pymodaq_utils.enums import StrEnum
from pymodaq_utils.array_manipulation import are_elements_contiguous
from pymodaq_gui.parameter.utils import ParameterWithPath
from pymodaq_gui.parameter.ioxml import VALID_FOR_CONFIGURATION
from pymodaq_gui.qvariant import QVariant
from pymodaq_gui.parameter import ParameterTree, Parameter

from pymodaq_gui.utils.widgets.table import TableModel


from pymodaq_gui import utils as gutils

from serializall import SerializableFactory, SerializableBase


import copy

logger = set_logger(get_module_name(__file__))
ser_factory = SerializableFactory()
special_entry_factory = SubEntryHandlerFactory()


if TYPE_CHECKING:
    from pymodaq_gui.managers.settings.settings_manager import SettingsManager


[docs] class EntryActions(StrEnum): ADD = 'add_entry' REMOVE = 'remove_entry' UP = 'move_entry_up' DOWN = 'move_entry_down'
[docs] class ParameterDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs)
[docs] def createEditor(self, parent, option, index: QModelIndex): parameter: Parameter = index.model().get_data(index.row()).setting.parameter widget: QtWidgets.QWidget = parameter.itemClass(parameter, depth=0).makeWidget() widget.setParent(parent) widget.setAutoFillBackground(True) # Set size policy to fill the cell widget.setSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) # Force widget to fill cell height available_height = option.rect.height() widget.setMinimumHeight(available_height) widget.setMaximumHeight(available_height) # Remove layout margins if present if widget.layout() is not None: widget.layout().setContentsMargins(0, 0, 0, 0) widget.layout().setSpacing(0) # Connect signals for auto-commit on value change or focus loss self._connect_editor_signals(widget) return widget
def _connect_editor_signals(self, widget): """Connect widget signals to auto-commit data changes""" # Try common value changed signals # if hasattr(widget, 'toggled'): # widget.toggled.connect(lambda: self.commitData.emit(widget)) if hasattr(widget, 'currentIndexChanged'): # For comboboxes widget.currentIndexChanged.connect(lambda: self.value_changed(widget)) # elif hasattr(widget, 'editingFinished'): # widget.editingFinished.connect(lambda: self.value_changed(widget)) # elif hasattr(widget, 'stateChanged'): # For checkboxes # widget.stateChanged.connect(lambda: self.commitData.emit(widget)) # elif hasattr(widget, 'checkStateChanged'): # For checkboxes # widget.checkStateChanged.connect(lambda: self.commitData.emit(widget)) # # Install event filter to catch focus loss widget.installEventFilter(self) pass
[docs] def value_changed(self, widget: QtWidgets.QWidget): QtWidgets.QApplication.processEvents() self.commitData.emit(widget)
[docs] def setEditorData(self, editor, index: QModelIndex): try: editor.setValue(index.data()) except: super().setEditorData(editor, index)
[docs] def setModelData(self, editor, model, index: QModelIndex): model.setData(index, copy.copy(editor.value()), Qt.ItemDataRole.EditRole)
[docs] def updateEditorGeometry(self, editor, option, index): """Ensure editor fills the cell completely""" rect = QtCore.QRect(option.rect) available_height = rect.height() editor.setMinimumHeight(available_height) editor.setMaximumHeight(available_height) editor.setGeometry(rect)
[docs] def sizeHint(self, option, index): """Provide size hint for cells with widgets""" if index.column() == 2: hint = super().sizeHint(option, index) hint.setHeight(max(hint.height(), 40)) return hint return super().sizeHint(option, index)
mock_list = ['elt1', 'elt2', 'elt3'] mock_entry = SubEntry('settings', 'Photodiode', ParameterWithPath( parameter=Parameter.create( title='mytitle', name='myname', type='list', value=mock_list[0], limits=mock_list)))
[docs] class SettingsManagerModel(TableModel): update_delegate = QtCore.Signal() def __init__(self, data: list[SubEntry]=None, header=('Type', 'Module', 'Title', 'Value'), save_path: Path = None ): self._data: list[SubEntry] | None = None self.save_path = save_path if data is None: data = [] super().__init__(data, header, editable=[False, False, False, True]) pass
[docs] def columnCount(self, parent): return 4
[docs] def mimeTypes(self): types = super().mimeTypes() types.append('pymodaq/parameter_with_path') types.append('pymodaq/settings_entry') return types
[docs] def mimeData(self, items): data = QMimeData() rows = list(set([item.row() for item in items])) if are_elements_contiguous(rows): entries = [self._data[raw] for raw in rows] data.setData('pymodaq/settings_entry', ser_factory.get_apply_serializer(entries)) return data
[docs] def data(self, index: QModelIndex, role: Qt.ItemDataRole): if index.isValid(): if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole: entry: SubEntry = self._data[index.row()] if index.column() == 0: dat = capitalize(entry.entry_type) elif index.column() == 1: dat = entry.module_name elif index.column() == 2: dat = entry.setting.parameter.title() elif index.column() == 3: suffix = '' if entry.setting.parameter.opts.get('suffix', None) is not None: suffix = f' {entry.setting.parameter.opts.get("suffix")}' val, prefix = get_si_prefix(entry.setting.parameter.value()) dat = f"{val} {prefix}{suffix}" else: dat = '' return dat elif role == Qt.ItemDataRole.CheckStateRole and index.column() == 0 and self._show_checkbox: if self._checked[index.row()]: return Qt.CheckState.Checked else: return Qt.CheckState.Unchecked elif role == Qt.ItemDataRole.ToolTipRole: entry: SubEntry = self._data[index.row()] return repr(entry) return QVariant()
[docs] def dropMimeData(self, data: QMimeData, action: Qt.DropAction, row: int, column: int, parent: QModelIndex): if row == -1: row = self.rowCount(parent) if data.hasFormat('pymodaq/settings_entry'): entries: list[SubEntry] = ( ser_factory.get_apply_deserializer( data.data('pymodaq/settings_entry').data())) else: entries = [mock_entry] if action == QtCore.Qt.DropAction.MoveAction: pass # this is strange if I move things around using drag/drop # sometimes the MoveRows is called immediately without calling drop??? # the code below is therefore not needed (but is still called in case of a drop!!) # for ind, entry in enumerate(entries): # self.data_tmp = entry # start_row = self._data.index(entry) # #self.moveRows(start_row, len(entries)) # self.moveRow(parent, start_row, parent, row) elif action == QtCore.Qt.DropAction.CopyAction: #but only one item in the list in Copy mode self.data_tmp = self.split_entry(entries[0]) # in case the entry has children parameters for entry in self.data_tmp: #make sure there is no duplicate if entry in self._data: self.data_tmp.remove(entry) self.insertRows(row, len(self.data_tmp), parent) self.update_delegate.emit() return True
[docs] def setData(self, index, value, role): if index.isValid(): if role == Qt.ItemDataRole.EditRole: if self.validate_data(index.row(), index.column(), value): self._data[index.row()].setting.parameter.setValue(value) self.dataChanged.emit(index, index, [role]) return True else: return False elif role == Qt.ItemDataRole.CheckStateRole: self._checked[index.row()] = True if value == Qt.CheckState.Checked else False self.dataChanged.emit(index, index, [role]) return True return False
[docs] def split_entry(self, entry: SubEntry, entries: list[SubEntry] = None) -> list[SubEntry]: """ Split A settingsEntry into multiple entries if its underlying parameter has children""" if entries is None: entries = [] if not entry.setting.parameter.hasChildren(): if entry.setting.parameter.opts.get(VALID_FOR_CONFIGURATION, True): # only add the ones specifying they are configurable entries.append(entry) else: for child in entry.setting.parameter.children(): if child.opts.get(VALID_FOR_CONFIGURATION, True) : # only add the ones specifying they are configurable pwp = ParameterWithPath(parameter=child, path=entry.setting.path + [child.name()]) config_entry = SubEntry(entry.entry_type, entry.module_name, entry.module_type, pwp) self.split_entry(config_entry, entries) return entries
[docs] def moveRow(self, sourceParent: QModelIndex, sourceRow: int, destinationParent: QModelIndex, destinationChild: int) -> bool: if (destinationChild > self.rowCount() or destinationChild < 0): return False self.beginMoveRows(sourceParent, sourceRow, sourceRow, destinationParent, destinationChild) entry_to_be_moved = self._data.pop(sourceRow) self._data.insert(destinationChild if destinationChild < sourceRow else destinationChild -1, entry_to_be_moved) self.endMoveRows() return True
[docs] def moveRows(self, sourceParent: QModelIndex, sourceRow: int, count: int, destinationParent: QModelIndex, destinationChild: int) -> bool: if count == 1: self.moveRow(sourceParent, sourceRow, destinationParent, destinationChild) else: super().moveRows(sourceParent, sourceRow, count, destinationParent, destinationChild)
[docs] def insertRows(self, row, count, parent): self.beginInsertRows(QtCore.QModelIndex(), row, row + count - 1) for ind in range(count): self._data.insert(row + ind, self.data_tmp[ind] if (hasattr(self.data_tmp, '__len__') and len(self.data_tmp) == count) else self.data_tmp) self._checked.insert(row + ind, False) self.endInsertRows() return True
[docs] def clear(self): while self.rowCount() > 0: self.remove_row(0)
[docs] def add_data(self, row, data: SubEntry): if data is not None: if data in self._data: return self.insert_data(row, data) self.update_delegate.emit()
[docs] def remove_data(self, row): self.remove_row(row) self.update_delegate.emit()
[docs] def load(self, fname: Union[str, Path, Sequence[bytes]] = None): if fname is None: fname = gutils.select_file(start_path=self.save_path, save=False, ext='*') if fname is not None and fname != '': #first remove all the table rows while self.rowCount(self.index(-1, -1)) > 0: self.remove_row(0) if isinstance(fname, (Path, str)): # populate it from full file containning binary list od Subentry self.save_path = fname.parent data = read_binary_and_deserialize(Path(fname)) elif isinstance(fname, Iterable): #populate the table from list of bytes encoding SubEntries data = fname else: data = [] for row in data: self.insert_data(self.rowCount(self.index(-1, -1)), row) self.update_delegate.emit()
[docs] def save(self, fname: str = None, mode='wb'): if fname is None: fname = gutils.select_file(start_path=self.save_path, save=True, ext='config', force_save_extension=True) if isinstance(fname, Path): self.save_path = fname.parent with open(fname, mode) as file: file.writelines([ser_factory.get_apply_serializer(entry) for entry in self._data])
[docs] class SettingsManagerTableView(QtWidgets.QTableView): """ """ valueChanged = QtCore.Signal(list) add_data_signal = QtCore.Signal(str) remove_row_signal = QtCore.Signal(int) load_data_signal = QtCore.Signal() save_data_signal = QtCore.Signal() def __init__(self, menu=False, entry_type: str='settings'): super().__init__() self.entry_type = entry_type.capitalize() self.setmenu(menu) #self.doubleClicked.connect(self.edit_row) self.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
[docs] def edit_row(self): index = self.currentIndex() index.model().edit_data(index)
[docs] def setmenu(self, status): if status: self.menu = QtWidgets.QMenu() special_menu = self.menu.addMenu('Add Special Configuration') for entry in special_entry_factory.entries: special_entry = special_entry_factory.get_subentry_handler(entry) if special_entry.use_dialog: special_menu.addAction(entry.capitalize(), self.create_menu_slot_special_entry(entry)) self.menu.addSeparator() self.menu.addAction('Remove selected row', self.remove) self.menu.addAction('Clear all', self.clear) self.menu.addSeparator() self.menu.addAction(f'Load {self.entry_type}Manager file', lambda: self.load_data_signal.emit()) self.menu.addAction(f'Save {self.entry_type}Manager file', lambda: self.save_data_signal.emit()) else: self.menu = None
[docs] def create_menu_slot_special_entry(self, entry: str): return lambda: self.add(entry)
[docs] def contextMenuEvent(self, event): if self.menu is not None: self.menu.exec(event.globalPos())
[docs] def clear(self): self.model().clear()
[docs] def add(self, special_entry: str): self.add_data_signal.emit(special_entry)
[docs] def remove(self): """ Remove selected rows, starting from the last one (to not mess with indexing)""" rows = list(set([index.row() for index in self.selectedIndexes()])) rows.sort(key=lambda row: -row) for row in rows: self.remove_row_signal.emit(row)
[docs] def data_has_changed(self, topleft, bottomright, roles): self.valueChanged.emit([topleft, bottomright, roles])
[docs] def get_table_value(self): """ """ return self.model()
[docs] def set_table_value(self, data_model): """ """ try: self.setModel(data_model) self.model().dataChanged.connect(self.data_has_changed) except Exception as e: pass
[docs] class SettingsManagerParameterTree(ParameterTree): def __init__(self, manager:'SettingsManager', *args, handler_id='Settings', **kwargs): super().__init__(*args, **kwargs) self.manager = manager self.handler_id = handler_id
[docs] def mimeTypes(self): types = super().mimeTypes() types.append('pymodaq/parameter_with_path') types.append('pymodaq/settings_entry') return types
[docs] def mimeData(self, items): data = QMimeData() param_with_path = ParameterWithPath(items[0].param) try: module = self.manager.get_module_from_param(param_with_path) except KeyError: module = 'None' if module is not None: entry = SubEntry(self.handler_id, module, param_with_path) data.setData('pymodaq/settings_entry', ser_factory.get_apply_serializer([entry])) return data