from __future__ import annotations from nmreval.lib.colors import available_cycles from .properties import PropWidget from ...Qt import QtWidgets, QtGui, QtCore from ..._py.datawidget import Ui_DataWidget from ...lib import make_action_icons from ...lib.delegates import HeaderDelegate class DataTree(QtWidgets.QTreeWidget): stateChanged = QtCore.pyqtSignal(list, list) # selected, deselected keyChanged = QtCore.pyqtSignal(str, str) # id, text positionChanged = QtCore.pyqtSignal(QtWidgets.QTreeWidgetItem) deleteItem = QtCore.pyqtSignal(list) moveItem = QtCore.pyqtSignal(list, str, str, int) # items, from, to, new row copyItem = QtCore.pyqtSignal(list, str) saveFits = QtCore.pyqtSignal(list) extendFits = QtCore.pyqtSignal(list) # noinspection PyUnresolvedReferences def __init__(self, parent=None): super().__init__(parent=parent) self.invisibleRootItem().setFlags(self.invisibleRootItem().flags() ^ QtCore.Qt.ItemIsDropEnabled) self.itemChanged.connect(self.data_change) self.itemClicked.connect(self.new_selection) self.setColumnCount(2) self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) self.setDragDropMode(QtWidgets.QTreeView.InternalMove) self.setDefaultDropAction(QtCore.Qt.IgnoreAction) self.setSelectionMode(QtWidgets.QTreeView.ExtendedSelection) self.setSelectionBehavior(QtWidgets.QTreeView.SelectRows) self._checked_graphs = set() self._checked_sets = set() self.management = None header = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, self) self.setHeader(header) header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) header.setVisible(False) header.moveSection(1, 0) self.setItemDelegateForColumn(1, HeaderDelegate()) def add_graph(self, idd: str, name: str): item = QtWidgets.QTreeWidgetItem() item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) item.setText(0, name) item.setData(0, QtCore.Qt.UserRole, idd) item.setCheckState(0, QtCore.Qt.Checked) self.addTopLevelItem(item) self._checked_graphs.add(idd) item.setExpanded(True) self.update_indexes() def add_item(self, items: (tuple | list[tuple]), gid: str): if isinstance(items, tuple): items = [items] for row in range(self.invisibleRootItem().childCount()): graph = self.invisibleRootItem().child(row) if graph.data(0, QtCore.Qt.UserRole) == gid: for (idd, name, value) in items: item = QtWidgets.QTreeWidgetItem([name]) item.setToolTip(0, f'Value: {value}') item.setData(0, QtCore.Qt.UserRole, idd) item.setCheckState(0, QtCore.Qt.Checked) item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) graph.addChild(item) self._checked_sets.add(idd) self.resizeColumnToContents(0) break self.update_indexes() @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem) def data_change(self, item: QtWidgets.QTreeWidgetItem) -> tuple[set, set]: idd = item.data(0, QtCore.Qt.UserRole) is_selected = item.checkState(0) == QtCore.Qt.Checked to_be_hidden = set() to_be_shown = set() # item is top-level item / graph if item.parent() is None: was_selected = idd in self._checked_graphs # check state changed to selected if is_selected != was_selected: # check state changed to checked if is_selected: self._checked_graphs.add(idd) iterator = QtWidgets.QTreeWidgetItemIterator(item) iterator += 1 self.blockSignals(True) for i in range(item.childCount()): child = item.child(i) child.setCheckState(0, QtCore.Qt.Checked) to_be_shown.add(child.data(0, QtCore.Qt.UserRole)) self._checked_sets.add(child.data(0, QtCore.Qt.UserRole)) self.blockSignals(False) # check state change to unchecked else: self._checked_graphs.remove(idd) self.blockSignals(True) for i in range(item.childCount()): child = item.child(i) child.setCheckState(0, QtCore.Qt.Unchecked) to_be_hidden.add(child.data(0, QtCore.Qt.UserRole)) try: self._checked_sets.remove(child.data(0, QtCore.Qt.UserRole)) except KeyError: pass self.blockSignals(False) else: self.keyChanged.emit(idd, item.text(0)) # item is a set else: was_selected = idd in self._checked_sets if is_selected != was_selected: if is_selected: to_be_shown.add(idd) self._checked_sets.add(idd) else: to_be_hidden.add(idd) try: self._checked_sets.remove(idd) except KeyError: pass else: self.keyChanged.emit(idd, item.text(0)) if to_be_shown or to_be_hidden: self.stateChanged.emit(list(to_be_shown), list(to_be_hidden)) return to_be_shown, to_be_hidden @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem) def new_selection(self, item: QtWidgets.QTreeWidgetItem): if item.parent() is None: self.management.select_window(item.data(0, QtCore.Qt.UserRole)) def dropEvent(self, evt: QtGui.QDropEvent): dropped_index = self.indexAt(evt.pos()) if not dropped_index.isValid(): return to_parent = self.itemFromIndex(dropped_index).parent() append = False if not to_parent: # dropped on graph item -> append items to_parent = self.itemFromIndex(dropped_index) append = True # index may change persistent_drop = QtCore.QPersistentModelIndex(dropped_index) tobemoved = [] take_from = [] for it in self.selectedItems(): from_parent = it.parent() if from_parent is None: continue from_parent.removeChild(it) tobemoved.append(it) take_from.append(from_parent.data(0, QtCore.Qt.UserRole)) pos = QtCore.QModelIndex(persistent_drop) if self.dropIndicatorPosition() == QtWidgets.QAbstractItemView.BelowItem: pos = pos.sibling(pos.row()+1, 0) row = pos.row() if (row == -1) or append: to_parent.addChildren(tobemoved) else: to_parent.insertChildren(row, tobemoved) self.management.move_sets([it.data(0, QtCore.Qt.UserRole) for it in tobemoved], to_parent.data(0, QtCore.Qt.UserRole), take_from, pos=-1 if append else row) self.update_indexes() def move_sets(self, sid: str, gid_in: str, gid_out: str): self.blockSignals(True) to_parent = None from_parent = None it = None iterator = QtWidgets.QTreeWidgetItemIterator(self) while iterator.value(): item = iterator.value() if item is not None: data = item.data(0, QtCore.Qt.UserRole) if data == gid_out: from_parent = item elif data == gid_in: to_parent = item elif data == sid: it = item iterator += 1 if (from_parent is None) or (to_parent is None) or (it is None): print('Komisch') return from_parent.removeChild(it) to_parent.addChild(it) self.update_indexes() self.blockSignals(False) def sort(self, graph_item: QtWidgets.QTreeWidgetItem, mode: str = 'value'): graph_id = graph_item.data(0, QtCore.Qt.UserRole) sets = self.management.get_attributes(graph_id, mode) sets = [el[0] for el in sorted(sets.items(), key=lambda x: x[1])] self.management.move_sets(sets, graph_id, graph_id, pos=-1) self.blockSignals(True) children = graph_item.takeChildren() for s in sets: for c in children: if c.data(0, QtCore.Qt.UserRole) == s: graph_item.addChild(c) self.update_indexes() self.blockSignals(False) def update_indexes(self): graph_cnt = -1 set_cnt = 0 iterator = QtWidgets.QTreeWidgetItemIterator(self) self.blockSignals(True) while iterator.value(): item = iterator.value() if item is not None: if item.parent() is None: graph_cnt += 1 set_cnt = 0 item.setText(1, f'G[{graph_cnt}]') else: item.setText(1, f'.S[{set_cnt}]') set_cnt += 1 iterator += 1 self.resizeColumnToContents(1) self.blockSignals(False) def set_name(self, sid, name): iterator = QtWidgets.QTreeWidgetItemIterator(self) while iterator.value(): item = iterator.value() if item is not None: data = item.data(0, QtCore.Qt.UserRole) if data == sid: if name != item.text(0): item.setText(0, name) break iterator += 1 def keyPressEvent(self, evt: QtGui.QKeyEvent): if evt.key() == QtCore.Qt.Key_Delete: rm_sets = [] rm_graphs = [] for idx in self.selectedIndexes(): if idx.column() == 1: continue item = self.itemFromIndex(idx) if item.parent() is None: for c_i in range(item.childCount()): # add sets inside graph to removal child_data = item.child(c_i).data(0, QtCore.Qt.UserRole) if child_data not in rm_sets: rm_sets.append(child_data) rm_graphs.append(item.data(0, QtCore.Qt.UserRole)) else: item_data = item.data(0, QtCore.Qt.UserRole) if item_data not in rm_sets: rm_sets.append(item_data) # self.deleteItem.emit(rm_sets+rm_graphs) self.management.delete_sets(rm_sets+rm_graphs) elif evt.key() == QtCore.Qt.Key_Space: sets = [] from_parent = [] for idx in self.selectedIndexes(): if idx.column() != 0: continue item = self.itemFromIndex(idx) if item.parent() is None: is_selected = item.checkState(0) self.blockSignals(True) for i in range(item.childCount()): child = item.child(i) from_parent.append(child) self.blockSignals(False) if is_selected == QtCore.Qt.Checked: item.setCheckState(0, QtCore.Qt.Unchecked) else: item.setCheckState(0, QtCore.Qt.Checked) else: sets.append(item) for it in sets: if it in from_parent: continue it.setCheckState(0, QtCore.Qt.Unchecked if it.checkState(0) == QtCore.Qt.Checked else QtCore.Qt.Checked) else: super().keyPressEvent(evt) def mousePressEvent(self, evt: QtGui.QMouseEvent): # disable drag-and-drop when column 1 ('header') is clicked idx = self.indexAt(evt.pos()) self.setDragEnabled(idx.column() == 0) super().mousePressEvent(evt) def remove_item(self, ids: list[str]): iterator = QtWidgets.QTreeWidgetItemIterator(self) toberemoved = [] graph_removal = [] # find all items that have to be removed while iterator.value(): item = iterator.value() _id = item.data(0, QtCore.Qt.UserRole) if _id in ids: try: item_parent = item.parent() if item_parent is None: raise AttributeError idx = item_parent.indexOfChild(item) # item.parent().takeChild(idx) toberemoved.append((item_parent, idx)) if _id in self._checked_sets: self._checked_sets.remove(_id) except AttributeError: idx = self.invisibleRootItem().indexOfChild(item) # self.invisibleRootItem().takeChild(idx) graph_removal.append(idx) if _id in self._checked_graphs: self._checked_graphs.remove(_id) iterator += 1 for (item, set_idx) in sorted(toberemoved, key=lambda x: x[1], reverse=True): item.takeChild(set_idx) for graph_idx in sorted(graph_removal, reverse=True): self.invisibleRootItem().takeChild(graph_idx) self.update_indexes() def contextMenuEvent(self, evt): menu = QtWidgets.QMenu() _ = menu.addAction('Hello') _.setEnabled(False) menu.addSeparator() if self.invisibleRootItem().childCount() == 0 and len(self.selectedIndexes()) == 0: rdn_action = menu.addAction('Randomness') action = menu.exec(evt.globalPos()) if action == rdn_action: import webbrowser webbrowser.open('https://en.wikipedia.org/wiki/Special:Random') elif all([self.itemFromIndex(i).parent() is None for i in self.selectedIndexes()]): # only graphs selected self.ctx_graphs(evt, menu) else: self.ctx_sets(evt, menu) def ctx_graphs(self, evt, menu): del_action = menu.addAction('Exterminate graph!') sort_menu = menu.addMenu('Sort sets') for label in ['By name', 'By value']: sort_menu.addAction(label) col_menu = menu.addMenu('Color cycle') for c in available_cycles.keys(): col_menu.addAction(c) action = menu.exec(evt.globalPos()) if action is None: return graphs = [] items = [] for i in self.selectedIndexes(): if i.column() == 0: continue items.append(self.itemFromIndex(i)) graphs.append(self.itemFromIndex(i).data(0, QtCore.Qt.UserRole)) if action == del_action: for gid in graphs: self.management.delete_graph(gid) elif action.parent() == col_menu: for gid in graphs: self.management.set_cycle(self.management.graphs[gid].sets, action.text()) elif action.parent() == sort_menu: for i in items: self.sort(i, mode=action.text().split()[1]) evt.accept() def ctx_sets(self, evt, menu): del_action = menu.addAction('Exterminate sets') cp_action = menu.addAction('Replicate sets') cat_action = menu.addAction('Join us!') plt_action = save_action = extend_action = None menu.addSeparator() col_menu = menu.addMenu('Color cycle') for c in available_cycles.keys(): col_menu.addAction(c) idx = {} has_fits = False for i in self.selectedIndexes(): if i.column() == 0: continue item = self.itemFromIndex(i) parent = item.parent() if parent is None: continue else: graph_id = parent.data(0, QtCore.Qt.UserRole) if graph_id not in idx: idx[graph_id] = [] # collect sets in their graph idx[graph_id].append(item.data(0, QtCore.Qt.UserRole)) data = self.management[item.data(0, QtCore.Qt.UserRole)] if data.mode == 'fit': has_fits = True if has_fits: menu.addSeparator() plt_action = menu.addAction('Plot fit parameter') save_action = menu.addAction('Save fit parameter') extend_action = menu.addAction('Extrapolate fit') action = menu.exec(evt.globalPos()) s = [] for gid, sets in idx.items(): s.extend(sets) if action == del_action: self.management.delete_sets(s) elif action == cp_action: for gid, sets in idx.items(): self.management.copy_sets(sets, gid) elif action == cat_action: self.management.cat(s) elif action == plt_action: self.management.make_fit_parameter(s) elif action == save_action: self.saveFits.emit(s) elif action == extend_action: self.extendFits.emit(s) elif action.parent() == col_menu: self.management.set_cycle(s, action.text()) evt.accept() def highlight(self, gid: str): iterator = QtWidgets.QTreeWidgetItemIterator(self) while iterator.value(): item = iterator.value() if item is not None: if item.data(0, QtCore.Qt.UserRole) == gid: item.setBackground(0, QtGui.QBrush(QtGui.QColor('gray'))) else: item.setBackground(0, QtGui.QBrush()) iterator += 1 def uncheck_sets(self, sets: list[str]): self.blockSignals(True) iterator = QtWidgets.QTreeWidgetItemIterator(self) while iterator.value(): item = iterator.value() if item is not None: if item.data(0, QtCore.Qt.UserRole) in sets: item.setCheckState(0, QtCore.Qt.Unchecked) iterator += 1 self.blockSignals(False) class DataWidget(QtWidgets.QWidget, Ui_DataWidget): keyChanged = QtCore.pyqtSignal(str, str) deleteItem = QtCore.pyqtSignal(list) startShowProperty = QtCore.pyqtSignal(list) propertyChanged = QtCore.pyqtSignal(list, str, str, object) def __init__(self, parent=None): super().__init__(parent=parent) self.setupUi(self) self.tree = DataTree(self) self.verticalLayout.addWidget(self.tree) # noinspection PyUnresolvedReferences self.tree.selectionModel().selectionChanged.connect(lambda x, y: self.show_property(x)) self.tree.keyChanged.connect(lambda x, y: self.keyChanged.emit(x, y)) self.proptable = PropWidget(self) self.propwidget.addWidget(self.proptable) self.propwidget.setText('Properties') self.propwidget.expansionChanged.connect(self.show_property) self.proptable.propertyChanged.connect(self.change_property) make_action_icons(self) def add_graph(self, idd: str, name: str): self.tree.blockSignals(True) self.tree.add_graph(idd, name) self.tree.blockSignals(False) def add_item(self, idd: str, name: str, value: str, gid: str): self.tree.blockSignals(True) self.tree.add_item((idd, name, value), gid) self.tree.blockSignals(False) def add_item_list(self, loi: list, gid: str): self.tree.blockSignals(True) self.tree.add_item(loi, gid) self.tree.blockSignals(False) def remove_item(self, key: list[str]): self.tree.remove_item(key) def show_property(self, _: QtCore.QModelIndex = None): if not self.propwidget.isExpanded(): return sid = [] for i in self.tree.selectedIndexes(): if i.column() == 0: sid.append(i.data(role=QtCore.Qt.UserRole)) self.startShowProperty.emit(sid) @QtCore.pyqtSlot(dict) def set_properties(self, props: dict): self.proptable.populate(props) def change_property(self, key1, key2, value): ids = [item.data(0, QtCore.Qt.UserRole) for item in self.tree.selectedItems()] if key2 == 'Value': try: value = float(value) except ValueError: QtWidgets.QMessageBox.warning(self, 'Invalid entry', 'Value %r is not a valid number for `value`.' % value) return self.propertyChanged.emit(ids, key1, key2, value) def uncheck_sets(self, sets: list[str]): self.tree.uncheck_sets(sets) def set_name(self, sid, value): self.tree.set_name(sid, value) @property def management(self): return self.tree.management @management.setter def management(self, value): self.tree.management = value