# -*- coding: utf-8 -*- # # AWL simulator - Project tree widget # # Copyright 2017-2018 Michael Buesch # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # from __future__ import division, absolute_import, print_function, unicode_literals from awlsim.common.compat import * from awlsim.core.symbolparser import SymTabParser, Symbol from awlsim.library.libselection import AwlLibEntrySelection from awlsim.gui.cpuwidget import RunState from awlsim.gui.util import * from awlsim.gui.icons import * __all__ = [ "ProjectTreeModel", "ProjectTreeView", ] class ProjectTreeModel(QAbstractItemModel): """Main project tree model. """ # Signal: Project has just been loaded. projectLoaded = Signal() # Signal: Some project data changed. projectContentChanged = Signal() EnumGen.start INDEXID_SRCS = EnumGen.item INDEXID_SRCS_AWL = EnumGen.item INDEXID_SRCS_FUP = EnumGen.item INDEXID_SRCS_KOP = EnumGen.item INDEXID_SRCS_SYMTAB = EnumGen.item INDEXID_SRCS_LIBSEL = EnumGen.item INDEXID_CPU = EnumGen.item INDEXID_CONN = EnumGen.item INDEXID_HW = EnumGen.item INDEXID_GUICONF = EnumGen.item EnumGen.end # Base ID mask for dynamic elements INDEXID_BASE_MASK = 0xFF0000 # ID bases for dynamic elements INDEXID_SRCS_AWL_BASE = 0x010000 INDEXID_SRCS_FUP_BASE = 0x020000 INDEXID_SRCS_KOP_BASE = 0x030000 INDEXID_SRCS_SYMTAB_BASE = 0x040000 row2id_toplevel = { 0 : INDEXID_SRCS, 1 : INDEXID_CPU, 2 : INDEXID_CONN, 3 : INDEXID_HW, 4 : INDEXID_GUICONF, } id2row_toplevel = pivotDict(row2id_toplevel) row2id_srcs = { 0 : INDEXID_SRCS_AWL, 1 : INDEXID_SRCS_FUP, # 2 : INDEXID_SRCS_KOP, 2 : INDEXID_SRCS_SYMTAB, 3 : INDEXID_SRCS_LIBSEL, } id2row_srcs = pivotDict(row2id_srcs) id2childBase = { INDEXID_SRCS_AWL : INDEXID_SRCS_AWL_BASE, INDEXID_SRCS_FUP : INDEXID_SRCS_FUP_BASE, INDEXID_SRCS_KOP : INDEXID_SRCS_KOP_BASE, INDEXID_SRCS_SYMTAB : INDEXID_SRCS_SYMTAB_BASE, } base2parentId = pivotDict(id2childBase) EnumGen.start COLUMN_NAME = EnumGen.item EnumGen.end def __init__(self, mainWidget, project=None, parent=None): QAbstractItemModel.__init__(self, parent) self.mainWidget = mainWidget self.__reset(project) self.mainWidget.dirtyChanged.connect(self.__handleProjectDirtyChanged) self.dataChanged.connect(self.__handleDataChanged) self.modelReset.connect(self.__projectContentChanged) self.rowsInserted.connect(self.__projectContentChanged) self.rowsMoved.connect(self.__projectContentChanged) self.rowsRemoved.connect(self.__projectContentChanged) @property def editMdiArea(self): """Get EditMdiArea instance. """ return self.mainWidget.editMdiArea def handleIdentsMsg(self, identsMsg): """Handle a new identifier message from AwlSimClient(). identsMsg: AwlSimMessage_IDENTS() instance. """ identsMsgAwlSources = identsMsg.awlSources if identsMsg else [] identsMsgFupSources = identsMsg.fupSources if identsMsg else [] identsMsgKopSources = identsMsg.kopSources if identsMsg else [] identsMsgSymTabSources = identsMsg.symTabSources if identsMsg else [] awlIdentHashes = [ s.identHash for s in identsMsgAwlSources ] fupIdentHashes = [ s.identHash for s in identsMsgFupSources ] kopIdentHashes = [ s.identHash for s in identsMsgKopSources ] symTabIdentHashes = [ s.identHash for s in identsMsgSymTabSources ] # Iterate over all sources in the project and check if the # identHash matches what's in the CPU. project = self.getProject() roles = (Qt.DecorationRole, Qt.ToolTipRole) anyChanged = False for getSources, identHashes, idxId in ( (project.getAwlSources, awlIdentHashes, self.INDEXID_SRCS_AWL), #TODO (project.getFupSources, fupIdentHashes, self.INDEXID_SRCS_FUP), (project.getKopSources, kopIdentHashes, self.INDEXID_SRCS_KOP), (project.getSymTabSources, symTabIdentHashes, self.INDEXID_SRCS_SYMTAB)): anySourceChanged = False for i, source in enumerate(getSources()): # Update the match state in the source user data. oldMatchState = source.userData.get("gui-cpu-idents-match") newMatchState = source.identHash in identHashes source.userData["gui-cpu-idents-match"] = newMatchState # If the match state changed, emit the dataChanged signal. if oldMatchState != newMatchState: anyChanged = anySourceChanged = True parentIndex = self.idToIndex(idxId) if parentIndex.isValid(): index = self.index(i, 0, parentIndex) self.dataChanged.emit(index, index, roles) # Mark the parent source container as changed. if anySourceChanged: parentIndex = self.idToIndex(idxId) if parentIndex.isValid(): self.dataChanged.emit(parentIndex, parentIndex, roles) # Mark the program container as changed. if anyChanged: index = self.idToIndex(self.INDEXID_SRCS) self.dataChanged.emit(index, index, roles) def setCpuRunState(self, cpuRunState): """Handle a CPU run state change. cpuRunState: RunState() instance. """ # Store the run state object. self.__cpuRunState = cpuRunState # Emit the dataChanged signal for all sources # and all parent source containers. project = self.getProject() roles = (Qt.DecorationRole, Qt.ToolTipRole) for getSources, idxId in ( (project.getAwlSources, self.INDEXID_SRCS_AWL), (project.getFupSources, self.INDEXID_SRCS_FUP), (project.getKopSources, self.INDEXID_SRCS_KOP), (project.getSymTabSources, self.INDEXID_SRCS_SYMTAB)): parentIndex = self.idToIndex(idxId) if parentIndex.isValid(): for i, source in enumerate(getSources()): index = self.index(i, 0, parentIndex) self.dataChanged.emit(index, index, roles) self.dataChanged.emit(parentIndex, parentIndex, roles) index = self.idToIndex(self.INDEXID_SRCS) self.dataChanged.emit(index, index, roles) self.headerDataChanged.emit(Qt.Horizontal, 0, 0) def entryActivate(self, index, parentWidget=None): if not index or not index.isValid(): return False idxIdBase, idxId, itemNr = self.indexToId(index) editMdiArea = self.editMdiArea project = self.getProject() def connectMdiSubWinSignals(mdiSubWin): # Connect signals from newly created mdiSubWin # to this project tree. mdiSubWin.sourceChanged.connect( lambda: self.__handleMdiSubWinSourceChanged(mdiSubWin)) if idxId == self.INDEXID_CPU: self.mainWidget.cpuConfig() return True elif idxId == self.INDEXID_CONN: self.mainWidget.linkConfig() return True elif idxId == self.INDEXID_HW: self.mainWidget.hwmodConfig() return True elif idxId == self.INDEXID_GUICONF: self.mainWidget.guiConfig() return True elif idxId == self.INDEXID_SRCS_LIBSEL: libSelMdiSubWin = self.__libSelMdiSubWin if libSelMdiSubWin: editMdiArea.setActiveSubWindow(libSelMdiSubWin) else: libSelections = project.getLibSelections() def removeLibSelMdiSubWin(w): self.__libSelMdiSubWin = None libSelMdiSubWin = editMdiArea.newWin_Libsel(libSelections) libSelMdiSubWin.closed.connect(removeLibSelMdiSubWin) self.__libSelMdiSubWin = libSelMdiSubWin connectMdiSubWinSignals(libSelMdiSubWin) return True def handleSourceWindowActivation(sources, makeNewWin): source = sources[itemNr] mdiSubWin = source.userData.get("gui-edit-window") if mdiSubWin: editMdiArea.setActiveSubWindow(mdiSubWin) else: # Create a new MDI window. mdiSubWin = makeNewWin(source) mdiSubWin.closed.connect(lambda w: source.userData.pop("gui-edit-window", None)) source.userData["gui-edit-window"] = mdiSubWin connectMdiSubWinSignals(mdiSubWin) if idxIdBase == self.INDEXID_SRCS_AWL_BASE: handleSourceWindowActivation( sources=project.getAwlSources(), makeNewWin=lambda source: editMdiArea.newWin_AWL(source)) return True elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: handleSourceWindowActivation( sources=project.getFupSources(), makeNewWin=lambda source: editMdiArea.newWin_FUP(source)) return True elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: handleSourceWindowActivation( sources=project.getKopSources(), makeNewWin=lambda source: editMdiArea.newWin_KOP(source)) return True elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: handleSourceWindowActivation( sources=project.getSymTabSources(), makeNewWin=lambda source: editMdiArea.newWin_SymTab(source)) return True # If an empty source container is activated, # we add a new source to that container and activate it. addNew = False if idxId == self.INDEXID_SRCS_AWL: if not project.getAwlSources(): addNew = True elif idxId == self.INDEXID_SRCS_FUP: if not project.getFupSources(): addNew = True elif idxId == self.INDEXID_SRCS_KOP: if not project.getKopSources(): addNew = True elif idxId == self.INDEXID_SRCS_SYMTAB: if not project.getSymTabSources(): addNew = True if addNew: newIndex = self.entryAdd(self.id2childBase[idxId], parentWidget=parentWidget) self.entryActivate(newIndex, parentWidget=parentWidget) return False def entryDelete(self, index, force=False, parentWidget=None): if not index or not index.isValid(): return False idxIdBase, idxId, itemNr = self.indexToId(index) getter, setter = self.sourceGetter(idxIdBase) if not getter or not setter: return False sourceList = getter() source = sourceList[itemNr] if not force: res = QMessageBox.question(parentWidget or self.mainWidget, "Remove selected source?", "Remove the selected source '%s' from the project?\n" "This can't be undone.\n" % ( source.name), QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if res != QMessageBox.Yes: return False self.beginRemoveRows(index.parent(), itemNr, itemNr) try: # Close the MDI edit window, if one is open. mdiSubWin = source.userData.get("gui-edit-window") if mdiSubWin: mdiSubWin.forceClose() del mdiSubWin # Remove source from project. sourceList.pop(itemNr) setter(sourceList) finally: self.endRemoveRows() return True def entryAdd(self, idxIdBase, source=None, pos=-1, parentWidget=None): if idxIdBase == self.INDEXID_SRCS_AWL_BASE: parentIdxId = self.INDEXID_SRCS_AWL if not source: source = AwlSource() elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: parentIdxId = self.INDEXID_SRCS_FUP if not source: source = FupSource() elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: parentIdxId = self.INDEXID_SRCS_KOP if not source: source = KopSource() elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: parentIdxId = self.INDEXID_SRCS_SYMTAB if not source: source = SymTabSource() else: return None if not source.name: source.name = ">>> New source <<<" getter, setter = self.sourceGetter(idxIdBase) parentIndex = self.idToIndex(parentIdxId) sources = getter() if pos < 0: pos = len(sources) pos = min(pos, len(sources)) self.beginInsertRows(parentIndex, pos, pos) try: sources.insert(pos, source) setter(sources) finally: self.endInsertRows() return self.index(pos, 0, parentIndex) def entryRename(self, index, newName=None, parentWidget=None): idxIdBase, idxId, itemNr = self.indexToId(index) getter, setter = self.sourceGetter(idxIdBase) if getter and setter: sources = getter() source = sources[itemNr] if newName is None: newName, ok = QInputDialog.getText(parentWidget, "Name for %s source" % source.SRCTYPE, "Name for %s source" % source.SRCTYPE, QLineEdit.Normal, source.name) if not ok: newName = None if newName is not None: # Set the new name in the project. source.name = newName setter(sources) # Update the MDI window, if any. mdiSubWin = source.userData.get("gui-edit-window") if mdiSubWin: mdiSubWin.getSource().name = newName mdiSubWin.updateTitle() self.dataChanged.emit(index, index, (Qt.EditRole,)) return True return False def entryIntegrate(self, index, parentWidget=None): idxIdBase, idxId, itemNr = self.indexToId(index) getter, setter = self.sourceGetter(idxIdBase) if getter: # Remove the file backing reference. source = getter()[itemNr] source.forceNonFileBacked(source.name) # Update the MDI window, if any. mdiSubWin = source.userData.get("gui-edit-window") if mdiSubWin: mdiSubWin.setSource(source) self.__projectContentChanged() return True return False def entryEnable(self, index, enable=True, parentWidget=None): idxIdBase, idxId, itemNr = self.indexToId(index) getter, setter = self.sourceGetter(idxIdBase) if getter: # Set the new enable status in the project. source = getter()[itemNr] source.enabled = enable # Update the MDI window, if any. mdiSubWin = source.userData.get("gui-edit-window") if mdiSubWin: mdiSubWin.getSource().enabled = enable mdiSubWin.updateTitle() self.__projectContentChanged() return True return False def entryImport(self, idxIdBase, parentWidget=None): """Import an entry from an external file. """ # Create a new entry that we import the data to. newIndex = self.entryAdd(idxIdBase, parentWidget=parentWidget) if not newIndex: return False def error(): self.entryDelete(newIndex, force=True, parentWidget=parentWidget) return False # Activate the new entry. if not self.entryActivate(newIndex, parentWidget=parentWidget): return error() # Get the source of the new item. source = self.indexToSource(newIndex) if not source: return error() # Get the MDI sub window of the new entry. mdiSubWin = source.userData.get("gui-edit-window") if not mdiSubWin: return error() # Call import of the new entry. if not mdiSubWin.importSource(): return error() self.dataChanged.emit(newIndex, newIndex) return True def entryExport(self, index, filePath=None, parentWidget=None): """Export an entry to an external file. """ # Activate the export entry. if not self.entryActivate(index, parentWidget=parentWidget): return False # Get the source of the export item. source = self.indexToSource(index) if not source: return False # Get the MDI sub window of the export entry. mdiSubWin = source.userData.get("gui-edit-window") if not mdiSubWin: return False # Call export of the new entry. if not mdiSubWin.exportSource(): return False return True def entryClipboardCopy(self, index, parentWidget=None): """Copy the item at 'index' into the global clipboard. """ mimeData = self.mimeData((index,)) if not mimeData: return False # Write the data to the clipboard. clipboard = QGuiApplication.clipboard() if not clipboard: return False clipboard.setMimeData(mimeData, QClipboard.Clipboard) clipboard.setMimeData(mimeData, QClipboard.Selection) return True def entryClipboardCut(self, index, parentWidget=None): """Cut the item at 'index' into the global clipboard. """ if self.entryClipboardCopy(index, parentWidget=parentWidget): if self.entryDelete(index, force=True, parentWidget=parentWidget): return True return False def entryClipboardPaste(self, parentIndex, parentWidget=None): """Try to paste an entry from the global clipboard. Returns the index of the pasted item, or None. """ # Get the data from the clipboard. clipboard = QGuiApplication.clipboard() if not clipboard: return None mimeData = clipboard.mimeData(QClipboard.Clipboard) parentIdxIdBase, parentIdxId, parentItemNr = self.indexToId(parentIndex) if parentIdxIdBase == 0: # Append to the end of the container. row = -1 else: # Paste it after this element. row = parentIndex.row() + 1 parentIndex = self.parent(parentIndex) return self.__dropMimeData(mimeData, Qt.CopyAction, row, 0, parentIndex) def symbolAdd(self, symbol, parentWidget=None): """Try to add the symbol to a symbol table. symbol: The Symbol() instance to add. Returns True, if adding was successfull. """ assert(isinstance(symbol, Symbol)) project = self.getProject() mnemonics = project.getCpuConf().getConfiguredMnemonics() symTabSources = project.getSymTabSources() # Check if we already have this symbol. for symTabSource in symTabSources: try: symTab = SymTabParser.parseSource(symTabSource, mnemonics=mnemonics) except AwlSimError as e: MessageBox.handleAwlSimError(parentWidget, "Failed to parse symbol table", e) if symbol in symTab: # We already have this symbol. #TODO modify it, if needed return True if not symTabSources: # We don't have a symbol table. #TODO add one return False if len(symTabSources) == 1: # We only have one table. Add the symbol to that table. symTabIndex = 0 else: # Ask which table to add the symbol to. entries = [ "%d: %s" % (i + 1, symTabSource.name) for i, symTabSource in enumerate(symTabSources) ] entry, ok = QInputDialog.getItem(parentWidget, "Select symbol table", "Please select the symbol table "\ "where to add the symbol to:" "\n%s \"%s\"" %\ (symbol.getOperatorString(), symbol.getName()), entries, 0, False) if not ok or not entry: return False symTabIndex = int(entry.split(":")[0]) - 1 # Add the symbol to the sym tab source. symTabSource = symTabSources[symTabIndex] try: symTab = SymTabParser.parseSource(symTabSource, mnemonics=mnemonics) except AwlSimError as e: MessageBox.handleAwlSimError(parentWidget, "Failed to parse symbol table", e) symTab.add(symbol, True) # Update the sym tab source. try: symTab.toSource(symTabSource) except AwlSimError as e: MessageBox.handleAwlSimError(parentWidget, "Failed to update symbol table", e) # If an MDI edit window is open for this source, update it. mdiSubWin = symTabSource.userData.get("gui-edit-window") if mdiSubWin: mdiSubWin.setSource(symTabSource) self.__projectContentChanged() return True def libSelectionAdd(self, libSelection, parentWidget=None): """Try to add the library selection to the library selection table. libSelection: The AwlLibEntrySelection() to add. Returns True, if adding was successfull. """ assert(isinstance(libSelection, AwlLibEntrySelection)) project = self.getProject() libSelections = project.getLibSelections() # Check if we already have this selection. if libSelection in libSelections: return True # We already have this lib. # Add the library selection to the table. libSelections.append(libSelection) project.setLibSelections(libSelections) # If an MDI edit window is open, update it. if self.__libSelMdiSubWin: self.__libSelMdiSubWin.setLibSelections(libSelections) self.__projectContentChanged() return True def sourceGetter(self, idxIdBase): project = self.getProject() if idxIdBase == self.INDEXID_SRCS_AWL_BASE: getter = project.getAwlSources setter = project.setAwlSources elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: getter = project.getFupSources setter = project.setFupSources elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: getter = project.getKopSources setter = project.setKopSources elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: getter = project.getSymTabSources setter = project.setSymTabSources else: getter = None setter = None return getter, setter def __handleMdiSubWinSourceChanged(self, mdiSubWin): # Handle sourceChanged signal of an edit area MDI window. self.__projectContentChanged() self.__setProjectNeedRefresh() def __handleDataChanged(self, topLeftIndex, bottomRightIndex, roles=()): # Handle the self.dataChanged signal. if not roles or\ Qt.EditRole in roles or\ Qt.DisplayRole in roles: self.__projectContentChanged() def __projectContentChanged(self): self.projectContentChanged.emit() def refreshProject(self): """Copy the modified sources from the edit widgets. """ ret = True # Temporarily set the refresh flag to false to avoid recursions. self.__projectNeedRefresh = False # Refresh the sources in the project from # the edit widgets (if any). for source in self.__project.getAllSources(): mdiSubWin = source.userData.get("gui-edit-window") if mdiSubWin: newSource = mdiSubWin.getSource() if newSource is not None: source.copyFrom(newSource, copyUserData=False, updateUserData=True) else: ret = False # Refresh the library selections. libSelMdiSubWin = self.__libSelMdiSubWin if libSelMdiSubWin: libSelections = libSelMdiSubWin.getLibSelections() if libSelections is not None: self.__project.setLibSelections(libSelections) else: ret = False # Set the refresh-flag to False, if we succeeded. self.__projectNeedRefresh = not ret return ret def __setProjectNeedRefresh(self): self.__projectNeedRefresh = True def getProject(self): """Get the Project() instance that is being represented by this tree. """ if self.__projectNeedRefresh: self.refreshProject() return self.__project def __reset(self, project=None): if not project: project = Project(None) # Empty project self.__project = project self.__setProjectNeedRefresh() self.__libSelMdiSubWin = None self.__isAdHocProject = False self.__warnedFileBacked = False self.__cpuRunState = RunState() self.handleIdentsMsg(None) def reset(self, project=None): """Completely reset the data model. """ self.beginResetModel() self.__reset(project) self.endResetModel() self.projectLoaded.emit() self.__projectContentChanged() def __loadProject(self, project): self.beginResetModel() try: self.__project = project # Close all possibly open edit windows. editMdiArea = self.editMdiArea editMdiArea.resetArea() editMdiArea.setGuiSettings(project.getGuiSettings()) self.__warnedFileBacked = False self.__isAdHocProject = False except Exception as e: self.__reset() finally: self.endResetModel() def __loadPlainAwlSourceFile(self, filename, parentWidget): project = Project(None) # Create an ad-hoc project source = AwlSource.fromFile(name=filename, filepath=filename, compatReEncode=True) project.setAwlSources([ source, ]) self.__loadProject(project) self.__isAdHocProject = True QMessageBox.information(parentWidget, "Opened plain AWL/STL file", "The plain AWL/STL source file \n'%s'\n has sucessfully " "been opened.\n\n" "If you click on 'save', you will be asked to create " "a project file for this source." % filename) def loadProjectFile(self, filename, parentWidget): if Project.fileIsProject(filename): self.__loadProject(Project.fromFile(filename)) else: # This might be a plain AWL-file. # Try to load it. self.__loadPlainAwlSourceFile(filename, parentWidget) self.projectLoaded.emit() def saveProjectFile(self, filename, parentWidget): if self.__isAdHocProject: srcs = self.__project.getAwlSources() assert(len(srcs) == 1) if filename == srcs[0].filepath: # This is an ad-hoc project, that was created from # a plain AWL file. Do not overwrite the AWL file. # Ask the user to create an .awlpro file. res = QMessageBox.question(parentWidget, "Create Awlsim project file?", "The current project was created ad-hoc from a " "plain AWL/STL file.\n" "Can not save without creating a project file.\n\n" "Do you want to create a project file?", QMessageBox.Yes, QMessageBox.No) if res != QMessageBox.Yes: return 0 # The user has to choose a new project file name. # Signal this to our caller. return -1 if not self.refreshProject(): # Failed to generate some sources return 0 project = self.__project if (any(src.isFileBacked() for src in project.getAwlSources()) or\ any(src.isFileBacked() for src in project.getSymTabSources())) and\ not self.__warnedFileBacked: QMessageBox.information(parentWidget, "Project contains external sources", "The project contains external sources.\n" "It is strongly recommended to integrate " "external sources into the project.\n" "Click on 'integrate source into project' " "in the source menu.") self.__warnedFileBacked = True project.setProjectFile(filename) project.toFile() self.__isAdHocProject = False return 1 def idToIndex(self, idxId, column = 0): for table in (self.id2row_toplevel, self.id2row_srcs): if idxId in table: return self.createIndex(table[idxId], column, idxId) return QModelIndex() def indexToId(self, index): idxId = index.internalId() idxIdBase = idxId & self.INDEXID_BASE_MASK itemNr = idxId - idxIdBase return idxIdBase, idxId, itemNr def indexToSource(self, index): """Get the source that corresponds to an index, if any. """ idxIdBase, idxId, itemNr = self.indexToId(index) sources = None if idxIdBase == self.INDEXID_SRCS_AWL_BASE: sources = self.getProject().getAwlSources() elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: sources = self.getProject().getFupSources() elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: sources = self.getProject().getKopSources() elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: sources = self.getProject().getSymTabSources() if sources is None or itemNr >= len(sources): return None return sources[itemNr] def flags(self, index): if not index.isValid(): return Qt.NoItemFlags idxIdBase, idxId, itemNr = self.indexToId(index) flags = Qt.ItemIsEnabled if idxIdBase in {self.INDEXID_SRCS_AWL_BASE, self.INDEXID_SRCS_FUP_BASE, self.INDEXID_SRCS_KOP_BASE, self.INDEXID_SRCS_SYMTAB_BASE,}: flags |= Qt.ItemIsEditable |\ Qt.ItemIsDragEnabled |\ Qt.ItemIsSelectable if idxId in {self.INDEXID_SRCS_AWL, self.INDEXID_SRCS_FUP, self.INDEXID_SRCS_KOP, self.INDEXID_SRCS_SYMTAB,}: flags |= Qt.ItemIsDropEnabled return flags def supportedDragActions(self): return Qt.CopyAction | Qt.MoveAction def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction def columnCount(self, parentIndex=QModelIndex()): return 1 def rowCount(self, parentIndex=QModelIndex()): if parentIndex.isValid(): parentIdBase, parentId, parentItemNr, = self.indexToId(parentIndex) project = self.getProject() if parentId == self.INDEXID_SRCS: return len(self.row2id_srcs) elif parentId == self.INDEXID_SRCS_AWL: return len(project.getAwlSources()) elif parentId == self.INDEXID_SRCS_FUP: return len(project.getFupSources()) elif parentId == self.INDEXID_SRCS_KOP: return len(project.getKopSources()) elif parentId == self.INDEXID_SRCS_SYMTAB: return len(project.getSymTabSources()) else: return len(self.row2id_toplevel) return 0 def index(self, row, column, parentIndex=QModelIndex()): if row < 0 or column < 0: return QModelIndex() if parentIndex.isValid(): parentIdBase, parentId, parentItemNr = self.indexToId(parentIndex) if parentId == self.INDEXID_SRCS: row2idTable = self.row2id_srcs elif parentId == self.INDEXID_SRCS_AWL: return self.createIndex(row, column, self.INDEXID_SRCS_AWL_BASE + row) elif parentId == self.INDEXID_SRCS_FUP: return self.createIndex(row, column, self.INDEXID_SRCS_FUP_BASE + row) elif parentId == self.INDEXID_SRCS_KOP: return self.createIndex(row, column, self.INDEXID_SRCS_KOP_BASE + row) elif parentId == self.INDEXID_SRCS_SYMTAB: return self.createIndex(row, column, self.INDEXID_SRCS_SYMTAB_BASE + row) else: return QModelIndex() else: row2idTable = self.row2id_toplevel try: idxId = row2idTable[row] except KeyError as e: return QModelIndex() return self.createIndex(row, column, idxId) def parent(self, index): if not index.isValid(): return QModelIndex() idxIdBase, idxId, itemNr = self.indexToId(index) if idxId in self.id2row_toplevel: return QModelIndex() elif idxId in self.id2row_srcs: return self.idToIndex(self.INDEXID_SRCS) elif idxIdBase == self.INDEXID_SRCS_AWL_BASE: return self.idToIndex(self.INDEXID_SRCS_AWL) elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: return self.idToIndex(self.INDEXID_SRCS_FUP) elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: return self.idToIndex(self.INDEXID_SRCS_KOP) elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: return self.idToIndex(self.INDEXID_SRCS_SYMTAB) return QModelIndex() def __data_columnName(self, index, idxId, idxIdBase, itemNr): source = self.indexToSource(index) if source: name = source.name if not source.enabled: name += " (DISABLED)" return name names = { self.INDEXID_SRCS : "Program", self.INDEXID_SRCS_AWL : "AWL / STL", self.INDEXID_SRCS_FUP : "FUP / FBD", self.INDEXID_SRCS_KOP : "KOP / LAD", self.INDEXID_SRCS_SYMTAB : "Symbols", self.INDEXID_SRCS_LIBSEL : "Library selections", self.INDEXID_CPU : "CPU", self.INDEXID_CONN : "Connection", self.INDEXID_HW : "Hardware", self.INDEXID_GUICONF : "Editor settings", } return names.get(idxId) def __data_icon(self, index, idxId, idxIdBase, itemNr): """Get the QIcon for displaying in the tree. """ def getSourceContainerIcon(sourceList, okIconName): # If the identHash of any source does not match what's on the CPU, # use a warning icon. But only do that, if the CPU is # in RUN state. if self.__cpuRunState == RunState.STATE_RUN and\ any(not source.userData.get("gui-cpu-idents-match", True) for source in sourceList): return getIcon("warning") return getIcon(okIconName) def getSourceIcon(sourceList, okIconName): if itemNr >= len(sourceList): return False return getSourceContainerIcon((sourceList[itemNr],), okIconName) def getProgramContainerIcon(okIconName): project = self.getProject() return getSourceContainerIcon( itertools.chain( project.getAwlSources(), project.getFupSources(), project.getKopSources(), project.getSymTabSources()), okIconName) if idxId == self.INDEXID_SRCS: return getProgramContainerIcon("textsource") elif idxId == self.INDEXID_SRCS_AWL: return getSourceContainerIcon(self.getProject().getAwlSources(), "textsource") elif idxIdBase == self.INDEXID_SRCS_AWL_BASE: return getSourceIcon(self.getProject().getAwlSources(), "textsource") elif idxId == self.INDEXID_SRCS_FUP: return getSourceContainerIcon(self.getProject().getFupSources(), "fup") elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: return getSourceIcon(self.getProject().getFupSources(), "fup") elif idxId == self.INDEXID_SRCS_KOP: return getSourceContainerIcon(self.getProject().getKopSources(), "kop") elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: return getSourceIcon(self.getProject().getKopSources(), "kop") elif idxId == self.INDEXID_SRCS_SYMTAB: return getSourceContainerIcon(self.getProject().getSymTabSources(), "tag") elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: return getSourceIcon(self.getProject().getSymTabSources(), "tag") elif idxId == self.INDEXID_SRCS_LIBSEL: return getIcon("stdlib") elif idxId == self.INDEXID_CPU: return getIcon("cpu") elif idxId == self.INDEXID_CONN: return getIcon("network") elif idxId == self.INDEXID_HW: return getIcon("hwmod") elif idxId == self.INDEXID_GUICONF: return getIcon("prefs") return None def __data_toolTip(self, index, idxId, idxIdBase, itemNr): """Get the tool-tip for displaying in the tree. """ def getSourceContainerTip(sourceList, okToolTip): # If the identHash of any source does not match what's on the CPU, # use a warning tool tip. But only do that, if the CPU is # in RUN state. if self.__cpuRunState == RunState.STATE_RUN and\ any(not source.userData.get("gui-cpu-idents-match", True) for source in sourceList): return "WARNING: The source contained in the project\n"\ "does not match the source on the CPU.\n"\ "That means the CPU is currenly running an outdated program.\n"\ "Please re-download the sources to the CPU\n"\ "by clicking the download button in the tool bar." return okToolTip def getSourceTip(sourceList, okToolTip): if itemNr >= len(sourceList): return False return getSourceContainerTip((sourceList[itemNr],), okToolTip) def getProgramContainerTip(okToolTip): project = self.getProject() return getSourceContainerTip( itertools.chain( project.getAwlSources(), project.getFupSources(), project.getKopSources(), project.getSymTabSources()), okToolTip) if idxId == self.INDEXID_SRCS: return getProgramContainerTip(None) elif idxId == self.INDEXID_SRCS_AWL: return getSourceContainerTip(self.getProject().getAwlSources(), "Right click here to add new AWL source.") elif idxIdBase == self.INDEXID_SRCS_AWL_BASE: return getSourceTip(self.getProject().getAwlSources(), "Double click here to edit this AWL source.") elif idxId == self.INDEXID_SRCS_FUP: return getSourceContainerTip(self.getProject().getFupSources(), "Right click here to add new FUP source.") elif idxIdBase == self.INDEXID_SRCS_FUP_BASE: return getSourceTip(self.getProject().getFupSources(), "Double click here to edit this FUP diagram.") elif idxId == self.INDEXID_SRCS_KOP: return getSourceContainerTip(self.getProject().getKopSources(), "Right click here to add new KOP source.") elif idxIdBase == self.INDEXID_SRCS_KOP_BASE: return getSourceTip(self.getProject().getKopSources(), "Double click here to edit this KOP diagram.") elif idxId == self.INDEXID_SRCS_SYMTAB: return getSourceContainerTip(self.getProject().getSymTabSources(), "Right click here to add new symbol table.") elif idxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: return getSourceTip(self.getProject().getSymTabSources(), "Double click here to edit this symbol table.") elif idxId == self.INDEXID_SRCS_LIBSEL: return "Double click here to edit system library selections." elif idxId == self.INDEXID_CPU: return "Double click here to edit CPU settings." elif idxId == self.INDEXID_CONN: return "Double click here to edit core server connection settings." elif idxId == self.INDEXID_HW: return "Double click here to edit hardware module settings." elif idxId == self.INDEXID_GUICONF: return "Double click here to edit GUI settings." return None def data(self, index, role=Qt.DisplayRole): column = index.column() idxIdBase, idxId, itemNr = self.indexToId(index) if role in (Qt.DisplayRole, Qt.EditRole): if column == self.COLUMN_NAME: return self.__data_columnName(index, idxId, idxIdBase, itemNr) elif role == Qt.DecorationRole: if column == self.COLUMN_NAME: return self.__data_icon(index, idxId, idxIdBase, itemNr) elif role == Qt.ToolTipRole: if column == self.COLUMN_NAME: return self.__data_toolTip(index, idxId, idxIdBase, itemNr) return None def setData(self, index, value, role=Qt.EditRole): if not index or not index.isValid(): return False if role == Qt.EditRole: if not self.entryRename(index, newName=value): return False else: return False return True def setHeaderData(self, section, orientation, value, role=Qt.EditRole): return False def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: project = self.getProject() filename = project.getProjectFile() if filename: title = os.path.basename(filename) else: title = "Project" if self.mainWidget.isDirty(): title += "*" if self.__cpuRunState == RunState.STATE_OFFLINE: title += " (CPU: offline)" elif self.__cpuRunState == RunState.STATE_ONLINE: title += " (CPU: online / STOP)" elif self.__cpuRunState == RunState.STATE_LOAD: title += " (CPU: loading)" elif self.__cpuRunState == RunState.STATE_RUN: title += " (CPU: RUN)" elif self.__cpuRunState == RunState.STATE_EXCEPTION: title += " (CPU: EXCEPTION)" return title return None def __handleProjectDirtyChanged(self, dirty): self.headerDataChanged.emit(Qt.Horizontal, 0, 0) def mimeData(self, indexes): if not indexes: return None mimeData = QMimeData() for index in indexes: if not index.isValid(): continue idxIdBase, idxId, itemNr = self.indexToId(index) getter, _ = self.sourceGetter(idxIdBase) if not getter: return False sources = getter() source = sources[itemNr] # Squash the file backing (if any) source = source.dup() source.forceNonFileBacked(source.filepath) # Generate XML data try: mimeType = { self.INDEXID_SRCS_AWL_BASE : "application/x-awlsim-xml-awl", self.INDEXID_SRCS_FUP_BASE : "application/x-awlsim-xml-fup", self.INDEXID_SRCS_KOP_BASE : "application/x-awlsim-xml-kop", self.INDEXID_SRCS_SYMTAB_BASE : "application/x-awlsim-xml-symtab", }[idxIdBase] factory = source.factory(project=self.getProject(), source=source) # Store the data as Awlsim object. dataBytes = factory.compose() mimeData.setData(mimeType, dataBytes) # Store the data as plain XML. mimeData.setData("application/xml", dataBytes) # Store the source bytes as plain text. mimeData.setText(source.sourceText) except (XmlFactory.Error, KeyError) as e: return None return mimeData def __getExpectedMimeFormat(self, parentIndex): parentIdxIdBase, parentIdxId, parentItemNr = self.indexToId(parentIndex) if parentIdxId == self.INDEXID_SRCS_AWL or\ parentIdxIdBase == self.INDEXID_SRCS_AWL_BASE: return "application/x-awlsim-xml-awl" if parentIdxId == self.INDEXID_SRCS_FUP or\ parentIdxIdBase == self.INDEXID_SRCS_FUP_BASE: return "application/x-awlsim-xml-fup" if parentIdxId == self.INDEXID_SRCS_KOP or\ parentIdxIdBase == self.INDEXID_SRCS_KOP_BASE: return "application/x-awlsim-xml-kop" if parentIdxId == self.INDEXID_SRCS_SYMTAB or\ parentIdxIdBase == self.INDEXID_SRCS_SYMTAB_BASE: return "application/x-awlsim-xml-symtab" return None def canDropMimeData(self, mimeData, action, row, column, parentIndex): if not parentIndex.isValid(): return False if action not in {Qt.MoveAction, Qt.CopyAction}: return False expectedMimeFormat = self.__getExpectedMimeFormat(parentIndex) if not expectedMimeFormat: return False if expectedMimeFormat not in mimeData.formats(): return False return True def __dropMimeData(self, mimeData, action, row, column, parentIndex): if not self.canDropMimeData(mimeData, action, row, column, parentIndex): return None parentIdxIdBase, parentIdxId, parentItemNr = self.indexToId(parentIndex) # Parse the MIME data. mimeFormat = self.__getExpectedMimeFormat(parentIndex) data = bytes(mimeData.data(mimeFormat)) try: if mimeFormat == "application/x-awlsim-xml-awl": source = AwlSource() elif mimeFormat == "application/x-awlsim-xml-fup": source = FupSource() elif mimeFormat == "application/x-awlsim-xml-kop": source = KopSource() return None # Not yet elif mimeFormat == "application/x-awlsim-xml-symtab": source = SymTabSource() else: return None factory = source.factory(project=self.getProject(), source=source) if not factory.parse(data): return None except XmlFactory.Error as e: return None if action == Qt.CopyAction: source.name += " (Copy)" # Insert the new element return self.entryAdd(idxIdBase=self.id2childBase[parentIdxId], source=source, pos=row) def dropMimeData(self, mimeData, action, row, column, parentIndex): if self.__dropMimeData(mimeData, action, row, column, parentIndex): return True return False def mimeTypes(self): return [ "application/x-awlsim-xml-awl", "application/x-awlsim-xml-fup", "application/x-awlsim-xml-kop", "application/x-awlsim-xml-symtab", ] def removeRows(self, row, count, parentIndex): for i in range(count): index = self.index(row, 0, parentIndex) if not self.entryDelete(index, force=True): return False return True class SourceContextMenu(QMenu): # Signal: Opem the edit for the selected source edit = Signal() # Signal: Add new source add = Signal() # Signal: Delete current source delete = Signal() # Signal: Rename current source rename = Signal() # Signal: Copy source copy = Signal() # Signal: Cut source cut = Signal() # Signal: Paste source paste = Signal() # Signal: Integrate source integrate = Signal() # Signal: Import source import_ = Signal() # Signal: Export source export = Signal() # Signal: Enable/disable source enable = Signal(bool) def __init__(self, itemCategoryName, itemName, withEditButton=False, withAddButton=False, withDeleteButton=False, withRenameButton=False, withIntegrateButton=False, withCopyButton=False, withCutButton=False, withPasteButton=False, withImportButton=False, withExportButton=False, withEnableButton=False, withDisableButton=False, parent=None): QMenu.__init__(self, parent) self.itemCategoryName = itemCategoryName self.itemName = itemName if withEditButton: self.addAction(getIcon("doc_edit"), "&Edit '%s'..." % itemName, self.__edit) self.addSeparator() if withAddButton: self.addAction(getIcon("doc_new"), "&Add %s" % itemCategoryName, self.__add) if withDeleteButton: self.addAction(getIcon("doc_close"), "&Delete '%s'..." % itemName, self.__delete) if withRenameButton: self.addAction(getIcon("doc_edit"), "&Rename '%s'..." % itemName, self.__rename) self.addSeparator() if withCopyButton: self.addAction(getIcon("copy"), "&Copy '%s'..." % itemName, self.__copy) if withCutButton: self.addAction(getIcon("cut"), "&Cut '%s'..." % itemName, self.__cut) if withPasteButton: self.addAction(getIcon("paste"), "&Paste '%s'..." % itemCategoryName, self.__paste) self.addSeparator() if withImportButton: self.addAction(getIcon("doc_import"), "&Import %s..." % itemCategoryName, self.__import) if withExportButton: self.addAction(getIcon("doc_export"), "&Export '%s'..." % itemName, self.__export) if withIntegrateButton: self.addAction("&Integrate '%s' into project..." % itemName, self.__integrate) self.addSeparator() if withEnableButton: self.addAction("E&nable '%s'" % itemName, self.__enable) if withDisableButton: self.addAction("D&isable '%s'" % itemName, self.__disable) def __edit(self): self.edit.emit() def __add(self): self.add.emit() def __delete(self): self.delete.emit() def __rename(self): self.rename.emit() def __copy(self): self.copy.emit() def __cut(self): self.cut.emit() def __paste(self): self.paste.emit() def __import(self): self.import_.emit() def __export(self): self.export.emit() def __integrate(self): res = QMessageBox.question(self, "Integrate current %s" % self.itemName, "The current %s is stored in an external file.\n" "Do you want to integrate this file info " "the awlsim project file (.awlpro)?" %\ self.itemName, QMessageBox.Yes, QMessageBox.No) if res == QMessageBox.Yes: self.integrate.emit() def __enable(self): self.enable.emit(True) def __disable(self): self.enable.emit(False) class ProjectTreeView(QTreeView): def __init__(self, model, parent=None): QTreeView.__init__(self, parent) self.setModel(model) self.setSelectionMode(QAbstractItemView.SingleSelection) # Disable double-click-edit self.setEditTriggers(self.editTriggers() & ~QTreeView.DoubleClicked) self.setDragDropMode(QAbstractItemView.DragDrop) self.setDefaultDropAction(Qt.MoveAction) self.setAcceptDrops(True) self.setDragEnabled(True) self.setDropIndicatorShown(True) self.__currentIndex = None self.pressed.connect(self.__mouseBtnPressed) self.doubleClicked.connect(self.__mouseBtnDoubleClicked) def setModel(self, model): oldModel = self.model() if oldModel: oldModel.projectLoaded.disconnect(self.__handleProjectLoaded) QTreeView.setModel(self, model) if model: model.projectLoaded.connect(self.__handleProjectLoaded) def __handleProjectLoaded(self): model = self.model() if model: self.expand(model.idToIndex(model.INDEXID_SRCS)) self.expand(model.idToIndex(model.INDEXID_SRCS_AWL)) self.expand(model.idToIndex(model.INDEXID_SRCS_FUP)) baseToCatName = { ProjectTreeModel.INDEXID_SRCS_AWL_BASE : "AWL source", ProjectTreeModel.INDEXID_SRCS_FUP_BASE : "FUP source", ProjectTreeModel.INDEXID_SRCS_KOP_BASE : "KOP source", ProjectTreeModel.INDEXID_SRCS_SYMTAB_BASE : "Symbol Table", } idToCatName = { ProjectTreeModel.INDEXID_SRCS_AWL : baseToCatName[ProjectTreeModel.INDEXID_SRCS_AWL_BASE], ProjectTreeModel.INDEXID_SRCS_FUP : baseToCatName[ProjectTreeModel.INDEXID_SRCS_FUP_BASE], ProjectTreeModel.INDEXID_SRCS_KOP : baseToCatName[ProjectTreeModel.INDEXID_SRCS_KOP_BASE], ProjectTreeModel.INDEXID_SRCS_SYMTAB : baseToCatName[ProjectTreeModel.INDEXID_SRCS_SYMTAB_BASE], } def __showSourceContextMenu(self, index, catName=None, itemName=None, onContainer=False, onSource=False, onStaticItem=False): model = self.model() idxIdBase, idxId, itemNr = model.indexToId(index) # Get the source, if any. source = None if onSource: getter, setter = model.sourceGetter(idxIdBase) if getter: source = getter()[itemNr] # Extract source information itemIsEnabled = bool(source and source.enabled) itemIsFileBacked = bool(source and source.isFileBacked()) if not itemName: itemName = source.name if source else None # Source-edit handler def handleEdit(): model.entryActivate(index, parentWidget=self) # Source-add handler def handleAdd(): base = idxIdBase if base == 0: base = model.id2childBase[idxId] newIndex = model.entryAdd(base, parentWidget=self) if newIndex: self.expand(index) model.entryRename(newIndex, parentWidget=self) model.entryActivate(newIndex, parentWidget=self) # Source-remove handler def handleDelete(): model.entryDelete(index, parentWidget=self) # Source-rename handler def handleRename(): self.edit(index) # Source-copy handler def handleCopy(): model.entryClipboardCopy(index, parentWidget=self) # Source-cut handler def handleCut(): model.entryClipboardCut(index, parentWidget=self) # Source-paste handler def handlePaste(): pastedIndex = model.entryClipboardPaste(index, parentWidget=self) if pastedIndex: self.expand(index) self.setCurrentIndex(pastedIndex) else: QMessageBox.critical(self, "Clipboard paste failed", "The clipboard does not seem to contain data " "that can be pasted here.") # Source-integrate handler def handleIntegrate(): model.entryIntegrate(index, parentWidget=self) # Source-import handler def handleImport(): base = idxIdBase if base == 0: base = model.id2childBase[idxId] model.entryImport(base, parentWidget=self) # Source-export handler def handleExport(): model.entryExport(index, parentWidget=self) # Source-enable/disable handler def handleEnable(enable): model.entryEnable(index, enable, parentWidget=self) # Construct the context menu and show it modally. menu = SourceContextMenu( itemCategoryName=catName, itemName=itemName, withEditButton=(onSource or onStaticItem), withAddButton=(onContainer or onSource), withDeleteButton=onSource, withRenameButton=onSource, withCopyButton=onSource, withCutButton=onSource, withPasteButton=(onSource or onContainer), withIntegrateButton=(onSource and itemIsFileBacked), withImportButton=(onContainer or onSource), withExportButton=onSource, withEnableButton=(onSource and not itemIsEnabled), withDisableButton=(onSource and itemIsEnabled), parent=self) menu.edit.connect(handleEdit) menu.add.connect(handleAdd) menu.delete.connect(handleDelete) menu.rename.connect(handleRename) menu.copy.connect(handleCopy) menu.cut.connect(handleCut) menu.paste.connect(handlePaste) menu.integrate.connect(handleIntegrate) menu.import_.connect(handleImport) menu.export.connect(handleExport) menu.enable.connect(handleEnable) menu.exec_(QCursor.pos()) def __mouseBtnPressed(self, index): model, buttons = self.model(), QApplication.mouseButtons() if not model: return try: self.__currentIndex = index if buttons & Qt.RightButton: idxIdBase, idxId, itemNr = model.indexToId(index) # Open the context menu. onContainer, onSource = False, True catName = self.baseToCatName.get(idxIdBase) if not catName: catName = self.idToCatName.get(idxId) if catName: onContainer, onSource = True, False if catName: self.__showSourceContextMenu(index=index, catName=catName, onContainer=onContainer, onSource=onSource) elif idxId in {ProjectTreeModel.INDEXID_CPU, ProjectTreeModel.INDEXID_CONN, ProjectTreeModel.INDEXID_HW, ProjectTreeModel.INDEXID_GUICONF, ProjectTreeModel.INDEXID_SRCS_LIBSEL}: itemName = model.data(index) self.__showSourceContextMenu(index=index, itemName=itemName, onStaticItem=True) finally: self.__currentIndex = None def __mouseBtnDoubleClicked(self, index): model = self.model() if model: self.__currentIndex = index model.entryActivate(index, self) def keyPressEvent(self, ev): QTreeView.keyPressEvent(self, ev) model = self.model() if model: index = self.currentIndex() if ev.matches(QKeySequence.Delete): model.entryDelete(index, parentWidget=self) elif ev.matches(QKeySequence.Copy): model.entryClipboardCopy(index, parentWidget=self) elif ev.matches(QKeySequence.Cut): model.entryClipboardCut(index, parentWidget=self) elif ev.matches(QKeySequence.Paste): pastedIndex = model.entryClipboardPaste(index, parentWidget=self) if pastedIndex: self.expand(index) self.setCurrentIndex(pastedIndex) def __removeSource(self): model = self.model() if model: index = self.__currentIndex if index is None: index = self.currentIndex() model.entryDelete(index, parentWidget=self)