def get_selected_recs_ids(selection_model: QItemSelectionModel, ndx_col: int = 0) -> Optional[Tuple[int, ...]]: """ Get integer ids from selected records. Unless explicitely defines, it assumes that id is stored in column 0. :param selection_model: the selection model. :type selection_model: QItemSelectionModel. :param ndx_col: the index of the searched columns. :type ndx_col: int. :return: the sequence of ids. :rtype: tuple of integers. """ # get selected records attitudes selected_records_ids = selection_model.selectedRows(column=ndx_col) if selected_records_ids: return tuple( map(lambda qmodel_ndx: qmodel_ndx.data(), selected_records_ids)) else: return None
class ListModel(QAbstractTableModel): orderChanged = pyqtSignal() elementSelected = pyqtSignal(int) class ColumnID(object): """ Define how many column the model holds and their type """ ncols = 2 Name = 0 Delete = 1 def __init__(self, elements=None, parent=None): """ Common interface for the labelListModel, the boxListModel, and the cropListModel see concrete implementations for details :param elements: :param parent: """ QAbstractTableModel.__init__(self, parent) if elements is None: elements = [] self._elements = list(elements) self._selectionModel = QItemSelectionModel(self) def onSelectionChanged(selected, deselected): if selected: ind = selected[0].indexes() if len(ind) > 0: self.elementSelected.emit(ind[0].row()) self._selectionModel.selectionChanged.connect(onSelectionChanged) self._allowRemove = True self._toolTipSuffixes = {} self.unremovable_rows = [ ] # rows in this list cannot be removed from the gui, # to add to this list call self.makeRowPermanent(int) # to remove make the self.makeRowRemovable(int) def makeRowPermanent(self, rowIndex): """ The rowindex cannot be removed from gui to remove this index use self.makeRowRemovable """ self.unremovable_rows.append(rowIndex) def makeRowRemovable(self, rowIndex): """ :param rowIndex: is the index for the label of interest in the current gui setting """ self.unremovable_rows.remove(rowIndex) def __len__(self): return len(self._elements) def __getitem__(self, i): return self._elements[i] def selectedRow(self): selected = self._selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=None): return len(self._elements) def columnCount(self, parent): return self.ColumnID.ncols def _getToolTipSuffix(self, row): """ Get the middle column tooltip suffix """ suffix = "; Click to select" if row in self._toolTipSuffixes: suffix = self._toolTipSuffixes[row] return suffix def _setToolTipSuffix(self, row, text): """ Set the middle column tooltip suffix """ self._toolTipSuffixes[row] = text index = self.createIndex(row, 1) self.dataChanged.emit(index, index) class EntryToolTipAdapter(object): """This class can be used to make each row look like a separate widget with its own tooltip. In this case, the "tooltip" is the suffix appended to the tooltip of the middle column. """ def __init__(self, table, row): self._row = row self._table = table def toolTip(self): return self._table._getToolTipSuffix(self._row) def setToolTip(self, text): self._table._setToolTipSuffix(self._row, text) def insertRow(self, position, object, parent=QModelIndex()): self.beginInsertRows(parent, position, position) object.changed.connect(self.modelReset) self._elements.insert(position, object) self.endInsertRows() return True def removeRow(self, position, parent=QModelIndex()): if position in self.unremovable_rows: return False self.beginRemoveRows(parent, position, position) value = self._elements[position] logger.debug("removing row: " + str(value)) self._elements.remove(value) self.endRemoveRows() return True def allowRemove(self, check): # Allow removing of rows. Needed to be able to disallow it # in interactive mode self._allowRemove = check self.dataChanged.emit( self.createIndex(0, self.ColumnID.Delete), self.createIndex(self.rowCount(), self.ColumnID.Delete)) def data(self, index, role): """ Reimplement, see labelListModel or boxListModel for concrete example :param index: :param role: """ if role == Qt.EditRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Delete: s = "Delete {}".format(self._elements[index.row()].name) return s elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Name: suffix = self._getToolTipSuffix(index.row()) s = "{}\nDouble click to rename {}".format( self._elements[index.row()].name, suffix) return s elif role == Qt.DisplayRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name if role == Qt.DecorationRole and index.column( ) == self.ColumnID.Delete: if index.row() in self.unremovable_rows: return row = index.row() pixmap = QPixmap(_NPIXELS, _NPIXELS) pixmap.fill(Qt.transparent) painter = QPainter() painter.begin(pixmap) painter.setRenderHint(QPainter.Antialiasing) painter.setBrush(QColor("red")) painter.drawEllipse(1, 1, _NPIXELS - 2, _NPIXELS - 2) pen = QPen(QColor("black")) pen.setWidth(2) painter.setPen(pen) x = _XSTART y = _NPIXELS - x painter.drawLine(x, x, y, y) painter.drawLine(y, x, x, y) painter.end() icon = QIcon(pixmap) return icon def flags(self, index): """ Reimplement, see labelListModel or boxListModel for concrete example :param index: """ if index.column() == self.ColumnID.Delete: if self._allowRemove: return Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags elif index.column() == self.ColumnID.Name: return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags def setData(self, index, value, role=Qt.EditRole): """ Reimplement, see labelListModel or boxListModel for concrete example :param index: """ if role == Qt.EditRole and index.column() == self.ColumnID.Name: row = index.row() self._elements[row].name = value self.dataChanged.emit(index, index) return True return False def select(self, row): """ Reimplement, see labelListModel or boxListModel for concrete example :param row """ self._selectionModel.clear() self._selectionModel.select(self.index(row, self.ColumnID.Name), QItemSelectionModel.Select) def clearSelectionModel(self): self._selectionModel.clear()
class LayerStackModel(QAbstractListModel): canMoveSelectedUp = pyqtSignal("bool") canMoveSelectedDown = pyqtSignal("bool") canDeleteSelected = pyqtSignal("bool") orderChanged = pyqtSignal() layerAdded = pyqtSignal(Layer, int) # is now in row layerRemoved = pyqtSignal(Layer, int) # was in row stackCleared = pyqtSignal() def __init__(self, parent=None): QAbstractListModel.__init__(self, parent) self._layerStack = [] self.selectionModel = QItemSelectionModel(self) self.selectionModel.selectionChanged.connect(self.updateGUI) self.selectionModel.selectionChanged.connect(self._onSelectionChanged) self._movingRows = False QTimer.singleShot(0, self.updateGUI) def _handleRemovedLayer(layer): # Layerstacks *own* the layers they hold, and thus are # responsible for cleaning them up when they are removed: layer.clean_up() self.layerRemoved.connect(_handleRemovedLayer) #### ## High level API to manipulate the layerstack ### def __len__(self): return self.rowCount() def __repr__(self): return "<LayerStackModel: layerStack='%r'>" % (self._layerStack, ) def __getitem__(self, i): return self._layerStack[i] def __iter__(self): return self._layerStack.__iter__() def layerIndex(self, layer): #note that the 'index' function already has a different implementation #from Qt side return self._layerStack.index(layer) def findMatchingIndex(self, func): """Call the given function with each layer and return the index of the first layer for which f is True.""" for index, layer in enumerate(self._layerStack): if func(layer): return index raise ValueError("No matching layer in stack.") def append(self, data): self.insert(0, data) def clear(self): if len(self) > 0: self.removeRows(0, len(self)) self.stackCleared.emit() def insert(self, index, data): """ Insert a layer into this layer stack, which *takes ownership* of the layer. """ assert isinstance( data, Layer), "Only Layers can be added to a LayerStackModel" self.insertRow(index) self.setData(self.index(index), data) if self.selectedRow() >= 0: self.selectionModel.select(self.index(self.selectedRow()), QItemSelectionModel.Deselect) self.selectionModel.select(self.index(index), QItemSelectionModel.Select) data.changed.connect( functools.partial(self._onLayerChanged, self.index(index))) index = self._layerStack.index(data) self.layerAdded.emit(data, index) self.updateGUI() def selectRow(self, row): already_selected_rows = self.selectionModel.selectedRows() if len(already_selected_rows) == 1 and row == already_selected_rows[0]: # Nothing to do if this row is already selected return self.selectionModel.clear() self.selectionModel.setCurrentIndex(self.index(row), QItemSelectionModel.SelectCurrent) def deleteSelected(self): num_rows = len(self.selectionModel.selectedRows()) assert num_rows == 1, "Can't delete selected row: {} layers are currently selected.".format( num_rows) row = self.selectionModel.selectedRows()[0] layer = self._layerStack[row.row()] assert not layer._cleaned_up, "This layer ({}) has already been cleaned up. Shouldn't it already be removed from the layerstack?".format( layer.name) self.removeRow(row.row()) if self.rowCount() > 0: self.selectionModel.select(self.index(0), QItemSelectionModel.Select) self.layerRemoved.emit(layer, row.row()) self.updateGUI() def moveSelectedUp(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = oldRow - 1 self._moveToRow(oldRow, newRow) def moveSelectedDown(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = oldRow + 1 self._moveToRow(oldRow, newRow) def moveSelectedToTop(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = 0 self._moveToRow(oldRow, newRow) def moveSelectedToBottom(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = self.rowCount() - 1 self._moveToRow(oldRow, newRow) def moveSelectedToRow(self, newRow): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != newRow: oldRow = row.row() self._moveToRow(oldRow, newRow) def _moveToRow(self, oldRow, newRow): d = self._layerStack[oldRow] self.removeRow(oldRow) self.insertRow(newRow) self.setData(self.index(newRow), d) self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select) self.orderChanged.emit() self.updateGUI() #### ## Low level API. To add, remove etc. layers use the high level API from above. #### def updateGUI(self): self.canMoveSelectedUp.emit(self.selectedRow() > 0) self.canMoveSelectedDown.emit(self.selectedRow() < self.rowCount() - 1) self.canDeleteSelected.emit(self.rowCount() > 0) self.wantsUpdate() def selectedRow(self): selected = self.selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=QModelIndex()): if not parent.isValid(): return len(self._layerStack) return 0 def insertRows(self, row, count, parent=QModelIndex()): '''Insert empty rows in the stack. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Always use the insert() or append() method. ''' if parent.isValid(): return False oldRowCount = self.rowCount() #for some reason, row can be negative! beginRow = max(0, row) endRow = min(beginRow + count - 1, len(self._layerStack)) self.beginInsertRows(parent, beginRow, endRow) while (beginRow <= endRow): self._layerStack.insert(row, Layer(datasources=[])) beginRow += 1 self.endInsertRows() assert self.rowCount( ) == oldRowCount + 1, "oldRowCount = %d, self.rowCount() = %d" % ( oldRowCount, self.rowCount()) return True def removeRows(self, row, count, parent=QModelIndex()): '''Remove rows from the stack. DO NOT USE THIS METHOD TO REMOVE LAYERS! Use the deleteSelected() method instead. ''' if parent.isValid(): return False if row + count <= 0 or row >= len(self._layerStack): return False oldRowCount = self.rowCount() beginRow = max(0, row) endRow = min(row + count - 1, len(self._layerStack) - 1) self.beginRemoveRows(parent, beginRow, endRow) while (beginRow <= endRow): del self._layerStack[row] beginRow += 1 self.endRemoveRows() return True def flags(self, index): defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled if index.isValid(): return Qt.ItemIsDragEnabled | defaultFlags else: return Qt.ItemIsDropEnabled | defaultFlags def supportedDropActions(self): return Qt.MoveAction def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None if index.row() >= len(self._layerStack): return None if role == Qt.DisplayRole or role == Qt.EditRole: return self._layerStack[index.row()] elif role == Qt.ToolTipRole: return self._layerStack[index.row()].toolTip() else: return None def setData(self, index, value, role=Qt.EditRole): '''Replace one layer with another. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Use deleteSelected() followed by insert() or append(). ''' if role == Qt.EditRole: layer = value if not isinstance(value, Layer): layer = value self._layerStack[index.row()] = layer self.dataChanged.emit(index, index) return True elif role == Qt.ToolTipRole: self._layerStack[index.row()].setToolTip() return True return False def headerData(self, section, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return "Column %r" % section else: return "Row %r" % section def wantsUpdate(self): self.layoutChanged.emit() def _onLayerChanged(self, idx): self.dataChanged.emit(idx, idx) self.updateGUI() def _onSelectionChanged(self, selected, deselected): for idx in deselected.indexes(): self[idx.row()].setActive(False) for idx in selected.indexes(): self[idx.row()].setActive(True)
class Attributes(): def __init__(self, treeManipulate, tabnumber): self.treeManipulate = treeManipulate self.tabnumber = tabnumber self.tvattributesview = None self.lenameattributes = None self.levalueattributes = None self.battributesinsert = None self.battributesdelete = None self.battributeshelp = None self.setUiInit() self.helptext = self.treeManipulate.main.attribhelp #build empty model for data and the selection self.attribmodel = AttribStandardItemModel(self.tvattributesview) self.attribmodel.setHorizontalHeaderLabels(["Name", "Value", "var/fun", "comment"]) self.attribselectionmodel = QItemSelectionModel(self.attribmodel) #set model to tableview self.tvattributesview.setModel(self.attribmodel) self.tvattributesview.setSelectionModel(self.attribselectionmodel) #signals self.battributesinsert.clicked.connect(self.addAttrib) self.battributesdelete.clicked.connect(self.deleteAttrib) self.battributeshelp.clicked.connect(self.help) self.attribmodel.itemChanged.connect(self.changeAttrib) #resize self.resz() #variables self.changeOnce = True #prevent the changeAttrib() function from being executed twice (the second time by the model change) def setUiInit(self): if self.tabnumber == 0: self.tvattributesview = self.treeManipulate.main.attributefieldst1[0] self.lenameattributes = self.treeManipulate.main.attributefieldst1[1] self.levalueattributes = self.treeManipulate.main.attributefieldst1[2] self.battributesinsert = self.treeManipulate.main.attributefieldst1[3] self.battributesdelete = self.treeManipulate.main.attributefieldst1[4] self.battributeshelp = self.treeManipulate.main.attributefieldst1[5] if self.tabnumber == 1: self.tvattributesview = self.treeManipulate.main.attributefieldst2[0] self.lenameattributes = self.treeManipulate.main.attributefieldst2[1] self.levalueattributes = self.treeManipulate.main.attributefieldst2[2] self.battributesinsert = self.treeManipulate.main.attributefieldst2[3] self.battributesdelete = self.treeManipulate.main.attributefieldst2[4] self.battributeshelp = self.treeManipulate.main.attributefieldst2[5] if self.tabnumber == 2: self.tvattributesview = self.treeManipulate.main.attributefieldst3[0] self.lenameattributes = self.treeManipulate.main.attributefieldst3[1] self.levalueattributes = self.treeManipulate.main.attributefieldst3[2] self.battributesinsert = self.treeManipulate.main.attributefieldst3[3] self.battributesdelete = self.treeManipulate.main.attributefieldst3[4] self.battributeshelp = self.treeManipulate.main.attributefieldst3[5] if self.tabnumber == 3: self.tvattributesview = self.treeManipulate.main.attributefieldst4[0] self.lenameattributes = self.treeManipulate.main.attributefieldst4[1] self.levalueattributes = self.treeManipulate.main.attributefieldst4[2] self.battributesinsert = self.treeManipulate.main.attributefieldst4[3] self.battributesdelete = self.treeManipulate.main.attributefieldst4[4] self.battributeshelp = self.treeManipulate.main.attributefieldst4[5] if self.tabnumber == 4: self.tvattributesview = self.treeManipulate.main.attributefieldst5[0] self.lenameattributes = self.treeManipulate.main.attributefieldst5[1] self.levalueattributes = self.treeManipulate.main.attributefieldst5[2] self.battributesinsert = self.treeManipulate.main.attributefieldst5[3] self.battributesdelete = self.treeManipulate.main.attributefieldst5[4] self.battributeshelp = self.treeManipulate.main.attributefieldst5[5] if self.tabnumber == 5: self.tvattributesview = self.treeManipulate.main.attributefieldst6[0] self.lenameattributes = self.treeManipulate.main.attributefieldst6[1] self.levalueattributes = self.treeManipulate.main.attributefieldst6[2] self.battributesinsert = self.treeManipulate.main.attributefieldst6[3] self.battributesdelete = self.treeManipulate.main.attributefieldst6[4] self.battributeshelp = self.treeManipulate.main.attributefieldst6[5] if self.tabnumber == 6: self.tvattributesview = self.treeManipulate.main.attributefieldst7[0] self.lenameattributes = self.treeManipulate.main.attributefieldst7[1] self.levalueattributes = self.treeManipulate.main.attributefieldst7[2] self.battributesinsert = self.treeManipulate.main.attributefieldst7[3] self.battributesdelete = self.treeManipulate.main.attributefieldst7[4] self.battributeshelp = self.treeManipulate.main.attributefieldst7[5] if self.tabnumber == 7: self.tvattributesview = self.treeManipulate.main.attributefieldst8[0] self.lenameattributes = self.treeManipulate.main.attributefieldst8[1] self.levalueattributes = self.treeManipulate.main.attributefieldst8[2] self.battributesinsert = self.treeManipulate.main.attributefieldst8[3] self.battributesdelete = self.treeManipulate.main.attributefieldst8[4] self.battributeshelp = self.treeManipulate.main.attributefieldst8[5] if self.tabnumber == 8: self.tvattributesview = self.treeManipulate.main.attributefieldst9[0] self.lenameattributes = self.treeManipulate.main.attributefieldst9[1] self.levalueattributes = self.treeManipulate.main.attributefieldst9[2] self.battributesinsert = self.treeManipulate.main.attributefieldst9[3] self.battributesdelete = self.treeManipulate.main.attributefieldst9[4] self.battributeshelp = self.treeManipulate.main.attributefieldst9[5] if self.tabnumber == 9: self.tvattributesview = self.treeManipulate.main.attributefieldst10[0] self.lenameattributes = self.treeManipulate.main.attributefieldst10[1] self.levalueattributes = self.treeManipulate.main.attributefieldst10[2] self.battributesinsert = self.treeManipulate.main.attributefieldst10[3] self.battributesdelete = self.treeManipulate.main.attributefieldst10[4] self.battributeshelp = self.treeManipulate.main.attributefieldst10[5] def setSesVarsFunsInAttributes(self): if self.tabnumber == 0: self.sesVariablest1 = self.treeManipulate.main.modellist[0][1] self.sesFunctionst1 = self.treeManipulate.main.modellist[0][2] self.sesVariablest1.sesvarChangedSignal.connect(self.validate) self.sesFunctionst1.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 1: self.sesVariablest2 = self.treeManipulate.main.modellist[1][1] self.sesFunctionst2 = self.treeManipulate.main.modellist[1][2] self.sesVariablest2.sesvarChangedSignal.connect(self.validate) self.sesFunctionst2.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 2: self.sesVariablest3 = self.treeManipulate.main.modellist[2][1] self.sesFunctionst3 = self.treeManipulate.main.modellist[2][2] self.sesVariablest3.sesvarChangedSignal.connect(self.validate) self.sesFunctionst3.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 3: self.sesVariablest4 = self.treeManipulate.main.modellist[3][1] self.sesFunctionst4 = self.treeManipulate.main.modellist[3][2] self.sesVariablest4.sesvarChangedSignal.connect(self.validate) self.sesFunctionst4.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 4: self.sesVariablest5 = self.treeManipulate.main.modellist[4][1] self.sesFunctionst5 = self.treeManipulate.main.modellist[4][2] self.sesVariablest5.sesvarChangedSignal.connect(self.validate) self.sesFunctionst5.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 5: self.sesVariablest6 = self.treeManipulate.main.modellist[5][1] self.sesFunctionst6 = self.treeManipulate.main.modellist[5][2] self.sesVariablest6.sesvarChangedSignal.connect(self.validate) self.sesFunctionst6.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 6: self.sesVariablest7 = self.treeManipulate.main.modellist[6][1] self.sesFunctionst7 = self.treeManipulate.main.modellist[6][2] self.sesVariablest7.sesvarChangedSignal.connect(self.validate) self.sesFunctionst7.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 7: self.sesVariablest8 = self.treeManipulate.main.modellist[7][1] self.sesFunctionst8 = self.treeManipulate.main.modellist[7][2] self.sesVariablest8.sesvarChangedSignal.connect(self.validate) self.sesFunctionst8.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 8: self.sesVariablest9 = self.treeManipulate.main.modellist[8][1] self.sesFunctionst9 = self.treeManipulate.main.modellist[8][2] self.sesVariablest9.sesvarChangedSignal.connect(self.validate) self.sesFunctionst9.sesfunChangedSignal.connect(self.validate) if self.tabnumber == 9: self.sesVariablest10 = self.treeManipulate.main.modellist[9][1] self.sesFunctionst10 = self.treeManipulate.main.modellist[9][2] self.sesVariablest10.sesvarChangedSignal.connect(self.validate) self.sesFunctionst10.sesfunChangedSignal.connect(self.validate) """read -> called from tree manipulate""" def readAttribList(self, lst): for row in range(len(lst)): itemnme = QStandardItem(lst[row][0]) itemval = QStandardItem(lst[row][1]) itemvarfun = QStandardItem(lst[row][2]) itemcomment = QStandardItem(lst[row][3]) self.attribmodel.appendRow([itemnme, itemval, itemvarfun, itemcomment]) self.resz() self.validate() """write -> the entries of the list in the current node""" def writeAttribList(self): if not self.treeManipulate.isRestoringTree: # only, if it is not called due to a selection change during reading the tree from save attribList = [] # get current selected node nodeuid = self.treeManipulate.treeModel.getNode(self.treeManipulate.treeSelectionModel.currentIndex()).getUid() for row in range(self.attribmodel.rowCount()): indnme = self.attribmodel.item(row, 0) indval = self.attribmodel.item(row, 1) indvarfun = self.attribmodel.item(row, 2) indcomment = self.attribmodel.item(row, 3) var = [indnme.data(QtCore.Qt.DisplayRole), indval.data(QtCore.Qt.DisplayRole), indvarfun.data(QtCore.Qt.DisplayRole), indcomment.data(QtCore.Qt.DisplayRole)] attribList.append(var) self.treeManipulate.treeModel.insertNodeSpecProp(self.treeManipulate.treeSelectionModel.currentIndex(), attribList, "attriblist", nodeuid) # write into the node """resize""" """ def resz(self): self.tvattributesview.setColumnWidth(0, self.tvattributesview.width()*0.4) header = self.tvattributesview.horizontalHeader() header.setStretchLastSection(True) """ def resz(self): i = 0 while i < 2: self.tvattributesview.resizeColumnToContents(i) i += 1 header = self.tvattributesview.horizontalHeader() header.setStretchLastSection(True) """add an attribute to the model""" def addAttrib(self): name = self.lenameattributes.text() value = self.levalueattributes.text() cname, cnameb, deleten = self.checkReturnName(name, False) cvalue, cvalueb, deletev, varfun = self.checkReturnValue(value, False) if cnameb and cvalueb: itemnm = QStandardItem(cname) itemval = QStandardItem(str(cvalue)) itemvarfun = QStandardItem(varfun) itemcomment = QStandardItem("") self.attribmodel.appendRow([itemnm, itemval, itemvarfun, itemcomment]) self.lenameattributes.setText("") self.levalueattributes.setText("") self.writeAttribList() self.resz() self.validate() """model changed via double click""" def changeAttrib(self): if self.changeOnce: self.changeOnce = False index = self.tvattributesview.currentIndex() #cloumn 0 -> name if index.column() == 0: namedic = self.attribmodel.itemData(index) name = namedic[0] cname, cnameb, deleten = self.checkReturnName(name, True) if cnameb: # set data dict = {0: cname} self.attribmodel.setItemData(index, dict) if deleten: # remove row self.deleteAttrib(self.attribselectionmodel.currentIndex().row(), True) #column 1 -> value elif index.column() == 1: valuedic = self.attribmodel.itemData(index) value = valuedic[0] cvalue, cvalueb, deletev, varfun = self.checkReturnValue(value, True) if cvalueb: # set data dict = {0: str(cvalue)} self.attribmodel.setItemData(index, dict) #set the value for the varfun column index2 = self.attribmodel.index(index.row(), 2) dict2 = {0: varfun} self.attribmodel.setItemData(index2, dict2) if deletev: # remove row self.deleteAttrib(self.attribselectionmodel.currentIndex().row(), True) self.writeAttribList() self.resz() self.validate() self.changeOnce = True """check the name""" def checkReturnName(self, name, edited): if name != "" and not name == "''" and not name == '""' and not name == '\n': name = name.strip() #remove whitespaces before and after # check for two words as name namesplit = name.split(" ") if not edited and len(namesplit) > 1: QMessageBox.information(None, "Inserting not possible", "The variable name contains spaces. Please remove them.", QtWidgets.QMessageBox.Ok) return("", False, False) elif edited and len(namesplit) > 1: QMessageBox.information(None, "Changing not possible", "The variable name contains spaces. The variable is deleted.", QtWidgets.QMessageBox.Ok) return("", False, True) # check for duplicates of variable name if not edited and len(self.attribmodel.findItems(name)) != 0: QMessageBox.information(None, "Inserting not possible", "The variable name already exists. Please enter another variable name.", QtWidgets.QMessageBox.Ok) return ("", False, False) elif edited and len(self.attribmodel.findItems(self.attribmodel.item(self.attribselectionmodel.currentIndex().row(), 0).data(QtCore.Qt.DisplayRole))) > 1: QMessageBox.information(None, "Changing not possible", "The variable name already exists. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True) # check that the variable name is no Python keyword if not edited and keyword.iskeyword(name): QMessageBox.information(None, "Inserting not possible", "The variable name denotes a Python keyword. Please enter another variable name.", QtWidgets.QMessageBox.Ok) return ("", False, False) elif edited and keyword.iskeyword(name): QMessageBox.information(None, "Changing not possible", "The variable name denotes a Python keyword. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True) # check that the variable name is not 'PARENT', 'CHILDREN' or 'NUMREP' if not edited and (name == 'PARENT' or name == 'CHILDREN' or name == 'NUMREP'): QMessageBox.information(None, "Inserting not possible", "The variable name is 'PARENT', 'CHILDREN' or 'NUMREP'. Please enter another variable name.", QtWidgets.QMessageBox.Ok) return ("", False, False) elif edited and (name == 'PARENT' or name == 'CHILDREN' or name == 'NUMREP'): QMessageBox.information(None, "Changing not possible", "The variable name is 'PARENT', 'CHILDREN' or 'NUMREP'. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True) #check if the variable name is in Python syntax attribregex = re.compile('^([a-z]|[A-Z])(\w+)?$') attribregexcorrect = attribregex.match(name) if not edited and attribregexcorrect is None: QMessageBox.information(None, "Inserting not possible", "Please enter correct Python syntax for the variable name.", QtWidgets.QMessageBox.Ok) return ("", False, False) elif edited and attribregexcorrect is None: QMessageBox.information(None, "Changing not possible", "Please enter correct Python syntax for the variable name. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True) return (name, True, False) else: #empty name if not edited: QMessageBox.information(None, "The variable name is empty", "The variable name is empty. The variable can not be inserted.", QtWidgets.QMessageBox.Ok) return ("", False, False) else: QMessageBox.information(None, "The variable name is empty", "The variable name is empty. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True) """check the value""" def checkReturnValue(self, value, edited): if value != "" and not value == "''" and not value == '""' and not value == '\n': value = value.strip() # remove whitespaces before and after isString = False if (ord(value[0]) == 39 and ord(value[-1]) == 39) or (ord(value[0]) == 34 and ord(value[-1]) == 34): #check ascii code: ord() for getting the number and chr() for getting the character again, unichr() for getting the unicode character isString = True try: if not isString: #a string shall stay a string, try to interprete if not entered as string value = ast.literal_eval(value) return (value, True, False, "") except: #the value is no Python value so it could be the name of an SES variable or function #check if it can be the syntax of an SES variable or function attribregex = re.compile('^([a-z]|[A-Z])(\w+)?(\(([\"\']?\w+[\"\']?)?(,\s*?[\"\']?\w+[\"\']?)*\))?$') attribregexcorrect = attribregex.match(value) if attribregexcorrect is not None and not isString: return (value, True, False, "x") QMessageBox.information(None, "Inserting not possible", "Please enter a variable using Python syntax or the possible name of an SES variable or function. SES variables and functions may not be part of a list or other Python datatype.\n" "If edited the variable will be deleted.", QtWidgets.QMessageBox.Ok) if not edited: return ("", False, False, "") else: return ("", False, True, "") else: # empty value if not edited: QMessageBox.information(None, "The variable value is empty", "The variable value is empty. The variable can not be inserted.", QtWidgets.QMessageBox.Ok) return ("", False, False, "") else: QMessageBox.information(None, "The variable value is empty", "The variable value is empty. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True, "") """delete""" def deleteAttrib(self, rw=-1, rowsdelete=False): selectedrows = self.attribselectionmodel.selectedRows() if len(selectedrows) == 0 and rw == -1: QMessageBox.information(None, "Deleting not possible", "Please select at least one attribute to delete.", QtWidgets.QMessageBox.Ok) elif len(selectedrows) > 0 and not rowsdelete: deleteListRows = [] for rowind in selectedrows: deleteListRows.append(rowind.row()) deleteListRows.sort(reverse=True) for row in deleteListRows: self.attribmodel.removeRow(row, QtCore.QModelIndex()) elif rw != -1 and rowsdelete: self.attribmodel.removeRow(rw, QtCore.QModelIndex()) self.writeAttribList() self.resz() """color the value field if an SES variable or SES function can be interpreted""" def validate(self, sesvarl="", sesfunl="", nd=None): #sub function def validateAttr(nd, sviat, sesfunl): attrlist = nd.attributes #there can be several attributes, so we have to put the results of this sub function into lists attrlinel = [] isVarFunl = [] varFoundl = [] funFoundl = [] funVarFoundl = [] resl = [] for row in attrlist: data = row[1] datavarfun = row[2] attrline = row isVarFun = False varFound = False funFound = False funVarFound = True res = "" #check if it is an SES variable or function if datavarfun == "": #if type(data) is str: #it can only be a string res = data elif datavarfun == "x": isVarFun = True # evaluate the SES variable try: res = eval(data, globals(), sviat.__dict__) varFound = True except: pass # check if the expression is an SES function if isinstance(data, str) and "(" in data and ")" in data: funname = data.split("(") funname[1] = funname[1][0:-1] vars = funname[1].split(",") if vars[0] == '': del vars[0] for v in range(len(vars)): vars[v] = vars[v].strip() # check if the parameters are SES variables and get the values varvalues = [] for v in vars: try: vv = ast.literal_eval(v) varvalues.append(vv) except: # the value is no Python value so it could be the name of an SES variable or function # check if the expression is an SES variable try: ret = eval(v, globals(), sviat.__dict__) # replace the name with the value varvalues.append(ret) except: varvalues.append("") # now get the function from the sesFunctions and try to find a match with the entry for sesfunvalue in sesfunl: if sesfunvalue[0] == funname[0]: # get the vars of the found function match since the parameters in the function definition do not have to match the SES variable names funvarsfound = re.findall('def\s+' + re.escape(funname[0]) + '\(.*\)', sesfunvalue[1]) funvarsfound[0] = funvarsfound[0].replace("def", "") funvarsfound[0] = funvarsfound[0].replace(funname[0] + "(", "") funvarsfound[0] = funvarsfound[0].replace(")", "") funvarsfound[0] = funvarsfound[0].strip() vars2 = funvarsfound[0].split(",") if vars2[0] == '': del vars2[0] # remove existing default values for i in range(len(vars2)): if i < len(vars): vars2[i] = vars2[i].split("=")[0] # make variables to default variables by position for i in range(len(vars2)): if i < len(vars): if varvalues[i] != "": if isinstance(varvalues[i], str): vars2[i] = vars2[i] + " = '" + varvalues[i] + "'" else: vars2[i] = vars2[i] + " = " + str(varvalues[i]) else: funVarFound = False # build a string from the variables to pass for i in range(len(vars2)): vars2[i] = str(vars2[i]) varstring = ', '.join(vars2) # replace parameters in the function with the varstring sesfunvalue[1] = re.sub('def ' + re.escape(sesfunvalue[0]) + '\(.*\)', 'def ' + re.escape(sesfunvalue[0]) + '(' + varstring + ')', sesfunvalue[1]) # try to execute the function try: exec(sesfunvalue[1]) self.ret = None execute = "self.ret = " + sesfunvalue[0] + "()" if sesfunvalue[0] in locals(): try: exec(execute) funFound = True res = self.ret except: pass except: pass #always return a string res = str(res) attrlinel.append(attrline) isVarFunl.append(isVarFun) varFoundl.append(varFound) funFoundl.append(funFound) funVarFoundl.append(funVarFound) resl.append(res) return attrlinel, isVarFunl, varFoundl, funFoundl, funVarFoundl, resl #here the validate function begins # own class for SES variables class sesvarsinattr: pass # create an instance of the SES variables class sviat = sesvarsinattr #was the process started for coloring the lines or for pruning? if sesvarl == "" and sesfunl == "" and nd == None: # the validate process was started from the editor for coloring the lines # fill the instance of the sesvar class for sesvarvalue in self.treeManipulate.main.modellist[self.treeManipulate.main.activeTab][1].outputSesVarList(): try: sesvarvalue[1] = ast.literal_eval(sesvarvalue[1]) # interprete the type of the value except: pass # do nothing, it stays a string setattr(sviat, sesvarvalue[0], sesvarvalue[1]) #get the SES functions sesfunl = self.treeManipulate.main.modellist[self.treeManipulate.main.activeTab][2].outputSesFunList() #go through all nodes indices = self.treeManipulate.listAllIndices(self.treeManipulate.treeSelectionModel.currentIndex()) for ind in indices: #get the node nd = self.treeManipulate.treeModel.getNode(ind[0]) #only continue if the node has attributes if nd.typeInfo() == "Entity Node": attrlinel, isVarFunl, varFoundl, funFoundl, funVarFoundl, resl = validateAttr(nd, sviat, sesfunl) #update the model self.updateModel(attrlinel, isVarFunl, varFoundl, funFoundl, funVarFoundl) else: # the validate process was started for pruning # fill the instance of the sesvar class for sesvarvalue in sesvarl: try: sesvarvalue[1] = ast.literal_eval(sesvarvalue[1]) # interprete the type of the value except: pass # do nothing, it stays a string setattr(sviat, sesvarvalue[0], sesvarvalue[1]) #the SES functions are given in the pass list #only continue if the node has attributes if nd.typeInfo() == "Entity Node": attrlinel, isVarFunl, varFoundl, funFoundl, funVarFoundl, resl = validateAttr(nd, sviat, sesfunl) #return the evaluated results return resl def updateModel(self, attrlinel, isVarFunl, varFoundl, funFoundl, funVarFoundl): #the information how to color the rows is not in the node (no True/False), so we have to pass the lists for row in range(self.attribmodel.rowCount()): index0 = self.attribmodel.index(row, 0) index1 = self.attribmodel.index(row, 1) index2 = self.attribmodel.index(row, 2) index3 = self.attribmodel.index(row, 3) #if the attribute the parameters were calculated for fits the attribute of the line attrname = index0.data(QtCore.Qt.DisplayRole) attrvalue = index1.data(QtCore.Qt.DisplayRole) attrvarfun = index2.data(QtCore.Qt.DisplayRole) attrcomment = index3.data(QtCore.Qt.DisplayRole) for i in range(len(attrlinel)): if attrlinel[i][0] == attrname and attrlinel[i][1] == attrvalue and attrlinel[i][2] == attrvarfun and attrlinel[i][3] == attrcomment: if isVarFunl[i]: #color the rows according to the found result if varFoundl[i] or (funFoundl[i] and funVarFoundl[i]): color1 = QtGui.QColor(195, 255, 195, 255) else: color1 = QtGui.QColor(255, 195, 195, 255) else: #get the alternating colors if row % 2 == 0: color1 = QtGui.QColor(QtCore.Qt.white) else: color1 = QtGui.QColor(239, 240, 241, 255) self.attribmodel.setData(index1, color1, QtCore.Qt.BackgroundColorRole) self.attribmodel.setData(index2, color1, QtCore.Qt.BackgroundColorRole) """empty the model""" def emptyAttribModel(self): self.attribmodel.clear() self.attribmodel.setHorizontalHeaderLabels(["Name", "Value", "var/fun", "comment"]) """help""" def help(self): msgBox = QMessageBox(self.treeManipulate.main) msgBox.setIcon(QMessageBox.Information) msgBox.setWindowTitle("attributes: Help") msgBox.setText(self.helptext[0]) msgBox.setDetailedText(self.helptext[1]) msgBox.setWindowModality(Qt.NonModal) msgBox.setStandardButtons(QMessageBox.Ok) msgBox.setDefaultButton(QMessageBox.Ok) msgBox.setEscapeButton(QMessageBox.Ok) msgBox.show() #validate function where only the existence of the SES function and parameters is checked but not if it is interpretable """ def validate(self): self.changeOnce = False for row in range(self.attribmodel.rowCount()): data = self.attribmodel.item(row, 1).data(QtCore.Qt.DisplayRole) datavarfun = self.attribmodel.item(row, 2).data(QtCore.Qt.DisplayRole) index1 = self.attribmodel.index(row, 1) index2 = self.attribmodel.index(row, 2) #if it is an SES variable or function if datavarfun == "x": varFound = False funFound = False parameterFound = [] for sesvarvalue in self.treeManipulate.main.modellist[self.treeManipulate.main.activeTab][1].outputSesVarList(): if data == sesvarvalue[0]: varFound = True attribregex = re.compile('^([a-z]|[A-Z])(\w+)?(\(([\"\']?\w+[\"\']?)?(,\s*?[\"\']?\w+[\"\']?)*\))$') attribregexfun = attribregex.match(data) if attribregexfun is not None: funsplit = data.split("(") funsplit[1] = funsplit[1][0:len(funsplit[1])-1] for sesfunvalue in self.treeManipulate.main.modellist[self.treeManipulate.main.activeTab][2].outputSesFunList(): if funsplit[0] == sesfunvalue[0]: funFound = True #now interprete the parameters parameters = funsplit[1].split(",") for p in range(len(parameters)): parameters[p] = parameters[p].strip() if not parameters[p] == "": try: ast.literal_eval(parameters[p]) except: #the parameter is maybe an SES variable found = False for sesvarvalue in self.treeManipulate.main.modellist[self.treeManipulate.main.activeTab][1].outputSesVarList(): if parameters[p] == sesvarvalue[0]: found = True if found: parameterFound.append(True) else: parameterFound.append(False) #color the rows according to the found result parameterFoundRes = all(parameterFound) if varFound or (funFound and parameterFoundRes): color1 = QtGui.QColor(195, 255, 195, 255) else: color1 = QtGui.QColor(255, 195, 195, 255) else: #get the alternating colors if row % 2 == 0: color1 = QtGui.QColor(QtCore.Qt.white) else: color1 = QtGui.QColor(239, 240, 241, 255) self.attribmodel.setData(index1, color1, QtCore.Qt.BackgroundColorRole) self.attribmodel.setData(index2, color1, QtCore.Qt.BackgroundColorRole) self.changeOnce = True """ #functions when the syntax var() and fun() is used------------------------------------------------------------------ """check the value -> old function: with var as keyword for SES Variable and fun as keyword for SES function""" """ def checkReturnValue(self, value, edited): def checkQuotes(val): # it must be an even number of quotes and one type of quotes may only be twice in the string since it defines the string quote = val.count("'") doublequote = val.count('"') if quote % 2 == 1 or doublequote % 2 == 1: # if one number of quotetype is odd -> return False return False elif quote == 0 or doublequote == 0: # one number of quotetype must be 0 (since the string delimiters are not counted) return True else: return False if value != "": value = value.strip() # remove whitespaces before and after try: value = ast.literal_eval(value) sesvarfunkeyexists = False sesvarkeyfound = None sesfunkeyfound = None sesvarregex = re.compile('^var\([\"\']([a-z]|[A-Z])(\w+)?[\"\']\)$') # regular expression: var("abc") or var('abc'), not found: var("abc def") since SES variable names may not contain whitespaces sesfunregex = re.compile('^fun\([\"\']([a-z]|[A-Z])(\w+)?[\"\'](,\s?((\d+(\.\d+)?)|([\"\']var\([\"\']([a-z]|[A-Z])(\w+)?[\"\']\)[\"\'])))*\)$') # regular expression: fun("abc"[...]) or fun('abc'[...]) with [...] optional containing: , 4 or , "var("a")" with whitespace optional between , and text checkquotes = False if isinstance(value, str) and ("var(" in value or "fun(" in value): sesvarfunkeyexists = True sesvarkeyfound = sesvarregex.match(value) sesfunkeyfound = sesfunregex.match(value) checkquotes = checkQuotes(value) # if isinstance(value, list) and (("var(" in v for v in value) or ("fun(" in v for v in value)): #does not perfectly function if isinstance(value, list): for v in value: if isinstance(v, str) and ("var(" in v or "fun(" in v): sesvarfunkeyexists = True sesvarkeyfound = sesvarregex.match(v) sesfunkeyfound = sesfunregex.match(v) checkquotes = checkQuotes(v) # if isinstance(value, tuple) and (("var(" in v for v in value) or ("fun(" in v for v in value)): #does not perfectly function if isinstance(value, tuple): for v in value: if isinstance(v, str) and ("var(" in v or "fun(" in v): sesvarfunkeyexists = True sesvarkeyfound = sesvarregex.match(v) sesfunkeyfound = sesfunregex.match(v) checkquotes = checkQuotes(v) # if isinstance(value, dict) and (("var(" in v for v in list(value.values())) or ("fun(" in v for v in list(value.values()))): #does not perfectly function if isinstance(value, dict): for v in list(value.values()): if isinstance(v, str) and ("var(" in v or "fun(" in v): sesvarfunkeyexists = True sesvarkeyfound = sesvarregex.match(v) sesfunkeyfound = sesfunregex.match(v) checkquotes = checkQuotes(v) if not sesvarfunkeyexists: # all correct Python values which do not contain the keywords fun( or var( return (value, True, False) elif sesvarfunkeyexists and sesvarkeyfound is not None and checkquotes: # value contains var( keyword in correct syntax return (value, True, False) elif sesvarfunkeyexists and sesfunkeyfound is not None and checkquotes: # value contains fun( keyword in correct syntax return (value, True, False) elif sesvarfunkeyexists and (sesvarkeyfound is not None or sesfunkeyfound is not None) and not checkquotes: QMessageBox.information(None, "Inserting not possible", "It seems you want to reference an SES variable or function. Please watch your quotes. " "If edited the variable will be deleted.", QtWidgets.QMessageBox.Ok) if not edited: return (value, False, False) else: return (value, False, True) elif sesvarfunkeyexists and sesvarkeyfound is None and sesfunkeyfound is None: # value contains var( or fun( keywords not in correct syntax QMessageBox.information(None, "Inserting not possible", "It seems you want to reference an SES variable or function.\n" "Please use the syntax\n\"var('sesvarname')\" or\n\"fun('sesfunname'[, 4.5])\" or\n\"fun('sesfunname'[, 'var('sesvarname')'])\" .\n" "The expression in square brackets is optional, do not type the square brackets. Use it if you want to pass parameters.\n" "sesvarname and sesfunname must be alphanumeric not beginning with a number.\n" "If edited the variable will be deleted.", QtWidgets.QMessageBox.Ok) if not edited: return (value, False, False) else: return (value, False, True) else: return (value, False, False) # if it is none of the above cases -> do nothing except: QMessageBox.information(None, "Inserting not possible", "Please enter a variable using Python syntax.\n" "If edited the variable will be deleted.", QtWidgets.QMessageBox.Ok) if not edited: return ("", False, False) else: return ("", False, True) else: # empty value if not edited: QMessageBox.information(None, "The variable value is empty", "The variable value is empty. The variable can not be inserted.", QtWidgets.QMessageBox.Ok) return ("", False, False) else: QMessageBox.information(None, "The variable value is empty", "The variable value is empty. The variable is deleted.", QtWidgets.QMessageBox.Ok) return ("", False, True) """ """color the value field if a var() or fun() can be interpreted -> old function: with var as keyword for SES Variable and fun as keyword for SES function""" """ def validate(self): self.changeOnce = False for row in range(self.attribmodel.rowCount()): data = self.attribmodel.item(row, 1).data(QtCore.Qt.DisplayRole) #find all occurences of var matches = re.findall('var\([\"\']([a-z]|[A-Z])(\w+)?[\"\']\)', data) sesvarfound = [] for ma in matches: #connect the tuple of each resulting group m = ma[0] + ma[1] #check if SES variable with this found name exists nameexists = False for sesvarvalue in self.sesVariables.outputSesVarList(): if m == sesvarvalue[0]: nameexists = True break sesvarfound.append(nameexists) #find all occurrences of fun matches = re.findall('fun\([\"\']([a-z]|[A-Z])(\w+)?[\"\'].*\)', data) sesfunfound = [] for ma in matches: #connect the tuple of each resulting group m = ma[0] + ma[1] #check if SES function with this found name exists nameexists = False for sesfunvalue in self.sesFunctions.outputSesFunList(): if m == sesfunvalue[0]: nameexists = True break sesfunfound.append(nameexists) #color the rows according to the found result if not (not sesvarfound and not sesfunfound): #if both lists are empty -> line does not contain fun(...) or var(...) -> do not color sesvarallfound = all(sesvarfound) sesfunallfound = all(sesfunfound) index1 = self.attribmodel.index(row, 1) if sesvarallfound and sesfunallfound: color1 = QtGui.QColor(195, 255, 195, 255) else: color1 = QtGui.QColor(255, 195, 195, 255) self.attribmodel.setData(index1, color1, QtCore.Qt.BackgroundColorRole) self.changeOnce = True """ """new function: validate (commented function above)""" """
class ListModel(QAbstractTableModel): orderChanged = pyqtSignal() elementSelected = pyqtSignal(int) class ColumnID(object): ''' Define how many column the model holds and their type ''' ncols=2 Name=0 Delete=1 def __init__(self, elements=None, parent=None): ''' Common interface for the labelListModel, the boxListModel, and the cropListModel see concrete implementations for details :param elements: :param parent: ''' QAbstractTableModel.__init__(self, parent) if elements is None: elements = [] self._elements = list(elements) self._selectionModel = QItemSelectionModel(self) def onSelectionChanged(selected, deselected): if selected: ind = selected[0].indexes() if len(ind)>0: self.elementSelected.emit(ind[0].row()) self._selectionModel.selectionChanged.connect(onSelectionChanged) self._allowRemove = True self._toolTipSuffixes = {} self.unremovable_rows=[] #rows in this list cannot be removed from the gui, # to add to this list call self.makeRowPermanent(int) # to remove make the self.makeRowRemovable(int) def makeRowPermanent(self, rowIndex): """ The rowindex cannot be removed from gui to remove this index use self.makeRowRemovable """ self.unremovable_rows.append(rowIndex) def makeRowRemovable(self, rowIndex): """ :param rowIndex: is the index for the label of interest in the current gui setting """ self.unremovable_rows.remove(rowIndex) def __len__(self): return len(self._elements) def __getitem__(self, i): return self._elements[i] def selectedRow(self): selected = self._selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=None): return len(self._elements) def columnCount(self, parent): return self.ColumnID.ncols def _getToolTipSuffix(self, row): """ Get the middle column tooltip suffix """ suffix = "; Click to select" if row in self._toolTipSuffixes: suffix = self._toolTipSuffixes[row] return suffix def _setToolTipSuffix(self, row, text): """ Set the middle column tooltip suffix """ self._toolTipSuffixes[row] = text index = self.createIndex(row, 1) self.dataChanged.emit(index, index) class EntryToolTipAdapter(object): """This class can be used to make each row look like a separate widget with its own tooltip. In this case, the "tooltip" is the suffix appended to the tooltip of the middle column. """ def __init__(self, table, row): self._row = row self._table = table def toolTip(self): return self._table._getToolTipSuffix(self._row) def setToolTip(self, text): self._table._setToolTipSuffix(self._row, text) def insertRow(self, position, object, parent=QModelIndex()): self.beginInsertRows(parent, position, position) object.changed.connect(self.modelReset) self._elements.insert(position, object) self.endInsertRows() return True def removeRow(self, position, parent=QModelIndex()): if position in self.unremovable_rows: return False self.beginRemoveRows(parent, position, position) value = self._elements[position] logger.debug("removing row: " + str(value)) self._elements.remove(value) self.endRemoveRows() return True def allowRemove(self, check): #Allow removing of rows. Needed to be able to disallow it #in interactive mode self._allowRemove = check self.dataChanged.emit(self.createIndex(0, self.ColumnID.Delete), self.createIndex(self.rowCount(), self.ColumnID.Delete)) def data(self, index, role): ''' Reimplement, see labelListModel or boxListModel for concrete example :param index: :param role: ''' if role == Qt.EditRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Delete: s = "Delete {}".format(self._elements[index.row()].name) return s elif role == Qt.ToolTipRole and index.column() == self.ColumnID.Name: suffix = self._getToolTipSuffix(index.row()) s = "{}\nDouble click to rename {}".format( self._elements[index.row()].name, suffix) return s elif role == Qt.DisplayRole and index.column() == self.ColumnID.Name: name = self._elements[index.row()].name return name if role == Qt.DecorationRole and index.column() == self.ColumnID.Delete: if index.row() in self.unremovable_rows: return row = index.row() pixmap = QPixmap(_NPIXELS, _NPIXELS) pixmap.fill(Qt.transparent) painter = QPainter() painter.begin(pixmap) painter.setRenderHint(QPainter.Antialiasing) painter.setBrush(QColor("red")) painter.drawEllipse(1, 1, _NPIXELS - 2, _NPIXELS - 2) pen = QPen(QColor("black")) pen.setWidth(2) painter.setPen(pen) x = _XSTART y = _NPIXELS - x painter.drawLine(x, x, y, y) painter.drawLine(y, x, x, y) painter.end() icon = QIcon(pixmap) return icon def flags(self, index): ''' Reimplement, see labelListModel or boxListModel for concrete example :param index: ''' if index.column() == self.ColumnID.Delete: if self._allowRemove: return Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags elif index.column() == self.ColumnID.Name: return Qt.ItemIsEditable | Qt.ItemIsEnabled | Qt.ItemIsSelectable else: return Qt.NoItemFlags def setData(self, index, value, role=Qt.EditRole): ''' Reimplement, see labelListModel or boxListModel for concrete example :param index: ''' if role == Qt.EditRole and index.column() == self.ColumnID.Name: row = index.row() self._elements[row].name = value self.dataChanged.emit(index, index) return True return False def select(self, row): ''' Reimplement, see labelListModel or boxListModel for concrete example :param row ''' self._selectionModel.clear() self._selectionModel.select(self.index(row, self.ColumnID.Name), QItemSelectionModel.Select) def clearSelectionModel(self): self._selectionModel.clear()
class LayerStackModel(QAbstractListModel): canMoveSelectedUp = pyqtSignal("bool") canMoveSelectedDown = pyqtSignal("bool") canDeleteSelected = pyqtSignal("bool") orderChanged = pyqtSignal() layerAdded = pyqtSignal(Layer, int) # is now in row layerRemoved = pyqtSignal(Layer, int) # was in row stackCleared = pyqtSignal() def __init__(self, parent=None): QAbstractListModel.__init__(self, parent) self._layerStack = [] self.selectionModel = QItemSelectionModel(self) self.selectionModel.selectionChanged.connect(self.updateGUI) self.selectionModel.selectionChanged.connect(self._onSelectionChanged) self._movingRows = False QTimer.singleShot(0, self.updateGUI) def _handleRemovedLayer(layer): # Layerstacks *own* the layers they hold, and thus are # responsible for cleaning them up when they are removed: layer.clean_up() self.layerRemoved.connect(_handleRemovedLayer) #### ## High level API to manipulate the layerstack ### def __len__(self): return self.rowCount() def __repr__(self): return "<LayerStackModel: layerStack='%r'>" % (self._layerStack,) def __getitem__(self, i): return self._layerStack[i] def __iter__(self): return self._layerStack.__iter__() def layerIndex(self, layer): # note that the 'index' function already has a different implementation # from Qt side return self._layerStack.index(layer) def findMatchingIndex(self, func): """Call the given function with each layer and return the index of the first layer for which f is True.""" for index, layer in enumerate(self._layerStack): if func(layer): return index raise ValueError("No matching layer in stack.") def append(self, data): self.insert(0, data) def clear(self): if len(self) > 0: self.removeRows(0, len(self)) self.stackCleared.emit() def insert(self, index, data): """ Insert a layer into this layer stack, which *takes ownership* of the layer. """ assert isinstance(data, Layer), "Only Layers can be added to a LayerStackModel" self.insertRow(index) self.setData(self.index(index), data) if self.selectedRow() >= 0: self.selectionModel.select(self.index(self.selectedRow()), QItemSelectionModel.Deselect) self.selectionModel.select(self.index(index), QItemSelectionModel.Select) data.changed.connect(functools.partial(self._onLayerChanged, self.index(index))) index = self._layerStack.index(data) self.layerAdded.emit(data, index) self.updateGUI() def selectRow(self, row): already_selected_rows = self.selectionModel.selectedRows() if len(already_selected_rows) == 1 and row == already_selected_rows[0]: # Nothing to do if this row is already selected return self.selectionModel.clear() self.selectionModel.setCurrentIndex(self.index(row), QItemSelectionModel.SelectCurrent) def deleteSelected(self): num_rows = len(self.selectionModel.selectedRows()) assert num_rows == 1, "Can't delete selected row: {} layers are currently selected.".format(num_rows) row = self.selectionModel.selectedRows()[0] layer = self._layerStack[row.row()] assert ( not layer._cleaned_up ), "This layer ({}) has already been cleaned up. Shouldn't it already be removed from the layerstack?".format( layer.name ) self.removeRow(row.row()) if self.rowCount() > 0: self.selectionModel.select(self.index(0), QItemSelectionModel.Select) self.layerRemoved.emit(layer, row.row()) self.updateGUI() def moveSelectedUp(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = oldRow - 1 self._moveToRow(oldRow, newRow) def moveSelectedDown(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = oldRow + 1 self._moveToRow(oldRow, newRow) def moveSelectedToTop(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != 0: oldRow = row.row() newRow = 0 self._moveToRow(oldRow, newRow) def moveSelectedToBottom(self): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != self.rowCount() - 1: oldRow = row.row() newRow = self.rowCount() - 1 self._moveToRow(oldRow, newRow) def moveSelectedToRow(self, newRow): assert len(self.selectionModel.selectedRows()) == 1 row = self.selectionModel.selectedRows()[0] if row.row() != newRow: oldRow = row.row() self._moveToRow(oldRow, newRow) def _moveToRow(self, oldRow, newRow): d = self._layerStack[oldRow] self.removeRow(oldRow) self.insertRow(newRow) self.setData(self.index(newRow), d) self.selectionModel.select(self.index(newRow), QItemSelectionModel.Select) self.orderChanged.emit() self.updateGUI() #### ## Low level API. To add, remove etc. layers use the high level API from above. #### def updateGUI(self): self.canMoveSelectedUp.emit(self.selectedRow() > 0) self.canMoveSelectedDown.emit(self.selectedRow() < self.rowCount() - 1) self.canDeleteSelected.emit(self.rowCount() > 0) self.wantsUpdate() def selectedRow(self): selected = self.selectionModel.selectedRows() if len(selected) == 1: return selected[0].row() return -1 def selectedIndex(self): row = self.selectedRow() if row >= 0: return self.index(self.selectedRow()) else: return QModelIndex() def rowCount(self, parent=QModelIndex()): if not parent.isValid(): return len(self._layerStack) return 0 def insertRows(self, row, count, parent=QModelIndex()): """Insert empty rows in the stack. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Always use the insert() or append() method. """ if parent.isValid(): return False oldRowCount = self.rowCount() # for some reason, row can be negative! beginRow = max(0, row) endRow = min(beginRow + count - 1, len(self._layerStack)) self.beginInsertRows(parent, beginRow, endRow) while beginRow <= endRow: self._layerStack.insert(row, Layer(datasources=[])) beginRow += 1 self.endInsertRows() assert self.rowCount() == oldRowCount + 1, "oldRowCount = %d, self.rowCount() = %d" % ( oldRowCount, self.rowCount(), ) return True def removeRows(self, row, count, parent=QModelIndex()): """Remove rows from the stack. DO NOT USE THIS METHOD TO REMOVE LAYERS! Use the deleteSelected() method instead. """ if parent.isValid(): return False if row + count <= 0 or row >= len(self._layerStack): return False oldRowCount = self.rowCount() beginRow = max(0, row) endRow = min(row + count - 1, len(self._layerStack) - 1) self.beginRemoveRows(parent, beginRow, endRow) while beginRow <= endRow: del self._layerStack[row] beginRow += 1 self.endRemoveRows() return True def flags(self, index): defaultFlags = Qt.ItemIsSelectable | Qt.ItemIsEditable | Qt.ItemIsEnabled if index.isValid(): return Qt.ItemIsDragEnabled | defaultFlags else: return Qt.ItemIsDropEnabled | defaultFlags def supportedDropActions(self): return Qt.MoveAction def data(self, index, role=Qt.DisplayRole): if not index.isValid(): return None if index.row() >= len(self._layerStack): return None if role == Qt.DisplayRole or role == Qt.EditRole: return self._layerStack[index.row()] elif role == Qt.ToolTipRole: return self._layerStack[index.row()].toolTip() else: return None def setData(self, index, value, role=Qt.EditRole): """Replace one layer with another. DO NOT USE THIS METHOD TO INSERT NEW LAYERS! Use deleteSelected() followed by insert() or append(). """ if role == Qt.EditRole: layer = value if not isinstance(value, Layer): layer = value self._layerStack[index.row()] = layer self.dataChanged.emit(index, index) return True elif role == Qt.ToolTipRole: self._layerStack[index.row()].setToolTip() return True return False def headerData(self, section, orientation, role=Qt.DisplayRole): if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return "Column %r" % section else: return "Row %r" % section def wantsUpdate(self): self.layoutChanged.emit() def _onLayerChanged(self, idx): self.dataChanged.emit(idx, idx) self.updateGUI() def _onSelectionChanged(self, selected, deselected): for idx in deselected.indexes(): self[idx.row()].setActive(False) for idx in selected.indexes(): self[idx.row()].setActive(True)