391 lines
13 KiB
Python
391 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from numpy import ndarray, iscomplexobj, asarray
|
|
|
|
from ..Qt import QtGui, QtCore, QtWidgets
|
|
from .._py.valueeditor import Ui_MaskDialog
|
|
from ..lib.pg_objects import PlotItem
|
|
|
|
|
|
class ValueEditWidget(QtWidgets.QWidget, Ui_MaskDialog):
|
|
requestData = QtCore.pyqtSignal(str)
|
|
maskSignal = QtCore.pyqtSignal(str, list)
|
|
itemChanged = QtCore.pyqtSignal(str, tuple, object)
|
|
itemDeleted = QtCore.pyqtSignal(str, list)
|
|
itemAdded = QtCore.pyqtSignal(str)
|
|
values_selected = QtCore.pyqtSignal(str, str)
|
|
split_signal = QtCore.pyqtSignal(str, int)
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
|
|
self.setupUi(self)
|
|
|
|
self.connected_figure = None
|
|
self.shown_set = None
|
|
self.items = {}
|
|
|
|
self.model = ValueModel()
|
|
self.model.itemChanged.connect(self.update_items)
|
|
|
|
self.selection_model = QtCore.QItemSelectionModel(self.model)
|
|
self.selection_model.selectionChanged.connect(self.show_position)
|
|
|
|
self.tableView.setModel(self.model)
|
|
self.tableView.setSelectionModel(self.selection_model)
|
|
self.tableView.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
|
self.tableView.customContextMenuRequested.connect(self.ctx)
|
|
|
|
self.selection_real = PlotItem(x=[], y=[], symbolSize=25, symbol='x',
|
|
pen=None, symbolPen='#c9308e', symbolBrush='#c9308e')
|
|
self.selection_imag = PlotItem(x=[], y=[], symbolSize=25, symbol='+',
|
|
pen=None, symbolPen='#dcdcdc', symbolBrush='#dcdcdc')
|
|
|
|
def __call__(self, items: dict):
|
|
self.items = items
|
|
|
|
self.graph_combobox.blockSignals(True)
|
|
self.graph_combobox.clear()
|
|
for k, v in items.items():
|
|
self.graph_combobox.addItem(k[1], userData=k[0])
|
|
self.graph_combobox.blockSignals(False)
|
|
|
|
idx = self.graph_combobox.findData(self.connected_figure)
|
|
if idx == -1:
|
|
idx = 0
|
|
|
|
self.graph_combobox.setCurrentIndex(idx)
|
|
self.graph_combobox.currentIndexChanged.emit(idx)
|
|
|
|
return self
|
|
|
|
@QtCore.pyqtSlot(int, name='on_graph_combobox_currentIndexChanged')
|
|
def _populate_sets(self, idx: int):
|
|
if idx == -1:
|
|
self.graph_combobox.setCurrentIndex(0)
|
|
return
|
|
|
|
self.set_combobox.blockSignals(True)
|
|
self.set_combobox.clear()
|
|
old_figure = self.connected_figure
|
|
self.connected_figure = self.graph_combobox.currentData()
|
|
|
|
if self.items:
|
|
for sid, name in self.items[(self.graph_combobox.currentData(), self.graph_combobox.currentText())]:
|
|
self.set_combobox.addItem(name, userData=sid)
|
|
self.set_combobox.blockSignals(False)
|
|
|
|
sidx = self.set_combobox.findData(self.shown_set)
|
|
if sidx == -1:
|
|
sidx = 0
|
|
|
|
self.set_combobox.setCurrentIndex(sidx)
|
|
self.set_combobox.currentIndexChanged.emit(sidx)
|
|
|
|
self.values_selected.emit(old_figure, self.connected_figure)
|
|
|
|
@QtCore.pyqtSlot(int, name='on_set_combobox_currentIndexChanged')
|
|
def _populate_table(self, idx):
|
|
self.selection_model.clearSelection()
|
|
self.shown_set = self.set_combobox.itemData(idx)
|
|
self.requestData.emit(self.set_combobox.itemData(idx))
|
|
|
|
def set_data(self, data: list, mask: ndarray):
|
|
self.selection_model.clearSelection()
|
|
self.model.loadData(data, mask)
|
|
self.spinBox.setMaximum(self.model.rowCount())
|
|
|
|
def remove_graph(self):
|
|
# remove everything
|
|
self.connected_figure = None
|
|
self.items = {}
|
|
self.selection_model.clear()
|
|
while self.model.rowCount():
|
|
self.model.removeRow(0)
|
|
self.graph_combobox.clear()
|
|
self.set_combobox.clear()
|
|
|
|
def ctx(self, pos: QtCore.QPoint):
|
|
idx = self.tableView.indexAt(pos)
|
|
if not idx.isValid():
|
|
return
|
|
|
|
menu = QtWidgets.QMenu()
|
|
menu.addSeparator()
|
|
|
|
hide_action = menu.addAction('Hide/Show')
|
|
del_action = menu.addAction('Delete')
|
|
split_action = menu.addAction('Split after selection')
|
|
cpc_action = menu.addAction('Copy to clipboard')
|
|
action = menu.exec(self.tableView.viewport().mapToGlobal(pos))
|
|
|
|
if action == hide_action:
|
|
self.mask_row()
|
|
elif action == del_action:
|
|
self.delete_item()
|
|
elif action == cpc_action:
|
|
self.copy_selection()
|
|
elif action == split_action:
|
|
self.split()
|
|
|
|
def keyPressEvent(self, evt):
|
|
if evt.matches(QtGui.QKeySequence.Copy):
|
|
self.copy_selection()
|
|
elif evt.key() == QtCore.Qt.Key.Key_Delete:
|
|
self.delete_item()
|
|
else:
|
|
super().keyPressEvent(evt)
|
|
|
|
def copy_selection(self) -> str:
|
|
table = ''
|
|
for r in self.selection_model.selectedRows():
|
|
table += self.model.data(r) + '\t'
|
|
table += '\t'.join([self.model.data(r.sibling(r.row(), i)) for i in [1, 2]]) + '\n'
|
|
|
|
QtWidgets.QApplication.clipboard().setText(table)
|
|
|
|
return table
|
|
|
|
@QtCore.pyqtSlot(name='on_split_button_clicked')
|
|
def split(self):
|
|
self.split_signal.emit(self.set_combobox.currentData(),
|
|
self.selection_model.selectedRows()[-1].row()+1)
|
|
|
|
@QtCore.pyqtSlot(name='on_mask_button_clicked')
|
|
def mask_row(self):
|
|
for r in self.selection_model.selectedRows():
|
|
self.model.setData(r, not self.model.data(r, ValueModel.maskRole), ValueModel.maskRole)
|
|
self.maskSignal.emit(self.set_combobox.currentData(), self.model.mask)
|
|
|
|
@QtCore.pyqtSlot(name='on_unmaskbutton_clicked')
|
|
def unmask(self):
|
|
self.model.unmask()
|
|
self.maskSignal.emit(self.set_combobox.currentData(), self.model.mask)
|
|
|
|
@QtCore.pyqtSlot(name='on_delete_button_clicked')
|
|
def delete_item(self):
|
|
idx = [r.row() for r in self.selection_model.selectedRows()]
|
|
success = False
|
|
for i in sorted(idx, reverse=True):
|
|
success = self.model.removeRow(i)
|
|
|
|
if success:
|
|
self.itemDeleted.emit(self.set_combobox.currentData(), idx)
|
|
self.spinBox.setMaximum(self.spinBox.maximum()-len(idx))
|
|
|
|
@QtCore.pyqtSlot(name='on_add_button_clicked')
|
|
def new_value(self):
|
|
success = self.model.addRows()
|
|
if success:
|
|
self.spinBox.setMaximum(self.spinBox.maximum()+1)
|
|
self.itemAdded.emit(self.set_combobox.currentData())
|
|
|
|
@QtCore.pyqtSlot(int, int, str)
|
|
def update_items(self, col, row, val):
|
|
sid = self.set_combobox.currentData()
|
|
new_value = complex(val)
|
|
new_value = new_value.real if new_value.imag == 0 else new_value
|
|
|
|
# table view loses focus when itemChanged is emitted
|
|
# if edit of item is cause of change resume editing at next item
|
|
prev_state = self.tableView.state()
|
|
idx = self.tableView.currentIndex()
|
|
idx = idx.sibling((col+1)//3+row, (col+1) % 3)
|
|
self.itemChanged.emit(sid, (col, row), new_value)
|
|
if prev_state == self.tableView.State.EditingState:
|
|
self.tableView.setCurrentIndex(idx)
|
|
self.tableView.edit(idx)
|
|
|
|
@QtCore.pyqtSlot(QtCore.QItemSelection, QtCore.QItemSelection)
|
|
def show_position(self, *_):
|
|
xvals = []
|
|
yvals = []
|
|
for idx in self.selection_model.selectedRows():
|
|
xvals.append(float(self.model.data(idx)))
|
|
try:
|
|
yvals.append(float(self.model.data(idx.sibling(idx.row(), 1))))
|
|
except ValueError:
|
|
yvals.append(complex(self.model.data(idx.sibling(idx.row(), 1))))
|
|
|
|
yvals = asarray(yvals)
|
|
|
|
if iscomplexobj(yvals):
|
|
self.selection_real.setData(x=xvals, y=yvals.real)
|
|
self.selection_imag.setData(x=xvals, y=yvals.imag)
|
|
else:
|
|
self.selection_real.setData(x=xvals, y=yvals)
|
|
self.selection_imag.setData(x=[], y=[])
|
|
|
|
@QtCore.pyqtSlot(name='on_toolButton_clicked')
|
|
def goto(self):
|
|
self.tableView.scrollTo(self.model.index(self.spinBox.value()-1, 0))
|
|
|
|
|
|
class ValueModel(QtCore.QAbstractTableModel):
|
|
"""
|
|
TableModel with lazy loading
|
|
"""
|
|
itemChanged = QtCore.pyqtSignal(int, int, str)
|
|
load_number = 20
|
|
maskRole = QtCore.Qt.ItemDataRole.UserRole+321
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
|
|
self._data = None
|
|
self.total_rows = 0
|
|
self.rows_loaded = 0
|
|
self.mask = None
|
|
self.headers = ['x', 'y', '\u0394y']
|
|
for i, hd in enumerate(self.headers):
|
|
self.setHeaderData(i, QtCore.Qt.Orientation.Horizontal, hd)
|
|
|
|
def rowCount(self, *args, **kwargs) -> int:
|
|
return self.total_rows
|
|
|
|
def columnCount(self, *args, **kwargs) -> int:
|
|
return len(self.headers)
|
|
|
|
def loadData(self, data: list[ndarray], mask: ndarray):
|
|
self.beginResetModel()
|
|
self._data = []
|
|
for x, y, y_err in zip(*data):
|
|
self._data.append([x, y, y_err])
|
|
self.total_rows = len(self._data)
|
|
|
|
self.mask = mask.tolist()
|
|
|
|
self.endResetModel()
|
|
self.dataChanged.emit(
|
|
self.index(0, 0),
|
|
self.index(0, 1), [QtCore.Qt.ItemDataRole.DisplayRole]
|
|
)
|
|
|
|
def data(self, idx: QtCore.QModelIndex, role=QtCore.Qt.ItemDataRole.DisplayRole) -> Any:
|
|
if not idx.isValid():
|
|
return
|
|
|
|
row = idx.row()
|
|
if role in [QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.ItemDataRole.EditRole]:
|
|
val = self._data[row][idx.column()]
|
|
return self.as_string(val)
|
|
|
|
elif role == QtCore.Qt.ItemDataRole.BackgroundRole:
|
|
pal = QtGui.QGuiApplication.palette()
|
|
if not self.mask[row]:
|
|
return pal.color(QtGui.QPalette.Disabled, QtGui.QPalette.Base)
|
|
else:
|
|
return pal.color(QtGui.QPalette.Base)
|
|
|
|
elif role == QtCore.Qt.ItemDataRole.ForegroundRole:
|
|
pal = QtGui.QGuiApplication.palette()
|
|
if not self.mask[row]:
|
|
return pal.color(QtGui.QPalette.Disabled, QtGui.QPalette.Text)
|
|
else:
|
|
return pal.color(QtGui.QPalette.Text)
|
|
|
|
elif role == ValueModel.maskRole:
|
|
return self.mask[row]
|
|
|
|
else:
|
|
return
|
|
|
|
def setData(self, idx: QtCore.QModelIndex, value: str | bool, role=QtCore.Qt.ItemDataRole.DisplayRole) -> Any:
|
|
col, row = idx.column(), idx.row()
|
|
|
|
if role == ValueModel.maskRole:
|
|
self.mask[row] = bool(value)
|
|
self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [role])
|
|
|
|
return True
|
|
|
|
if value:
|
|
if role == QtCore.Qt.ItemDataRole.EditRole:
|
|
if value == self.as_string(self._data[row][col]):
|
|
return True
|
|
|
|
try:
|
|
value = complex(value)
|
|
except ValueError:
|
|
# not a number
|
|
return False
|
|
|
|
value = value.real if value.imag == 0 else value
|
|
self._data[row][col] = value.real if value.imag == 0 else value
|
|
self.itemChanged.emit(col, row, str(value))
|
|
self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [role])
|
|
|
|
else:
|
|
return super().setData(idx, value, role=role)
|
|
|
|
return True
|
|
|
|
else:
|
|
return False
|
|
|
|
def headerData(self, section: int, orientation, role=QtCore.Qt.ItemDataRole.DisplayRole) -> Any:
|
|
if role == QtCore.Qt.ItemDataRole.DisplayRole:
|
|
if orientation == QtCore.Qt.Orientation.Horizontal:
|
|
return self.headers[section]
|
|
else:
|
|
return str(section+1)
|
|
|
|
return
|
|
|
|
def canFetchMore(self, idx: QtCore.QModelIndex) -> bool:
|
|
if not idx.isValid():
|
|
return False
|
|
|
|
return self.total_rows > self.rows_loaded
|
|
|
|
def fetchMore(self, idx: QtCore.QModelIndex):
|
|
remaining = self.total_rows - self.rows_loaded
|
|
to_be_loaded = min(remaining, ValueModel.load_number)
|
|
|
|
self.beginInsertRows(QtCore.QModelIndex(), self.rows_loaded, self.rows_loaded + to_be_loaded - 1)
|
|
self.rows_loaded += to_be_loaded
|
|
self.endInsertRows()
|
|
|
|
def flags(self, idx: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag:
|
|
return QtCore.QAbstractTableModel.flags(self, idx) | QtCore.Qt.ItemFlag.ItemIsEditable
|
|
|
|
def removeRows(self, pos: int, rows: int, parent=None, *args, **kwargs) -> bool:
|
|
self.beginRemoveRows(parent, pos, pos+rows-1)
|
|
|
|
for _ in range(rows):
|
|
self._data.pop(pos)
|
|
self.mask.pop(pos)
|
|
|
|
self.endRemoveRows()
|
|
self.total_rows -= rows
|
|
|
|
return True
|
|
|
|
def addRows(self, num=1):
|
|
return self.insertRows(self.rowCount(), num)
|
|
|
|
def insertRows(self, pos: int, rows: int, parent=QtCore.QModelIndex(), *args, **kwargs):
|
|
self.beginInsertRows(parent, pos, pos+rows-1)
|
|
|
|
for _ in range(rows):
|
|
self._data.insert(pos, [0.0] * self.columnCount())
|
|
self.mask.insert(pos, True)
|
|
self.total_rows += rows
|
|
|
|
self.endInsertRows()
|
|
|
|
return True
|
|
|
|
def unmask(self):
|
|
self.mask = [True] * self.total_rows
|
|
self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [ValueModel.maskRole])
|
|
|
|
@staticmethod
|
|
def as_string(value) -> str:
|
|
if isinstance(value, complex):
|
|
return f'{value.real:.8g}{value.imag:+.8g}j'
|
|
else:
|
|
return f'{value:.8g}'
|