def showPathLabelContextMenu(self, pos): """Triggered when a context menu is requested for the path label""" contextMenu = QMenu(self) contextMenu.addAction(getIcon('copymenu.png'), 'Copy full path to clipboard (double click)', self.onPathLabelDoubleClick) contextMenu.addSeparator() contextMenu.addAction(getIcon(''), 'Copy directory path to clipboard', self.onCopyDirToClipboard) contextMenu.addAction(getIcon(''), 'Copy file name to clipboard', self.onCopyFileNameToClipboard) contextMenu.popup(self.__fileLabel.mapToGlobal(pos))
class BreakPointView(QTreeView): """Breakpoint viewer widget""" sigSelectionChanged = pyqtSignal(QModelIndex) def __init__(self, parent, bpointsModel): QTreeView.__init__(self, parent) self.__model = None self.setModel(bpointsModel) self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.setUniformRowHeights(True) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setItemDelegate(NoOutlineHeightDelegate(4)) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) self.doubleClicked.connect(self.__doubleClicked) self.__createPopupMenus() def setModel(self, model): """Sets the breakpoint model""" self.__model = model self.sortingModel = QSortFilterProxyModel() self.sortingModel.setSourceModel(self.__model) QTreeView.setModel(self, self.sortingModel) header = self.header() header.setSortIndicator(COLUMN_LOCATION, Qt.AscendingOrder) header.setSortIndicatorShown(True) header.setSectionsClickable(True) self.setSortingEnabled(True) self.layoutDisplay() def layoutDisplay(self): """Performs the layout operation""" self.__resizeColumns() self.__resort() def __resizeColumns(self): """Resizes the view when items get added, edited or deleted""" self.header().setStretchLastSection(True) self.header().resizeSections(QHeaderView.ResizeToContents) self.header().resizeSection(COLUMN_TEMPORARY, 22) self.header().resizeSection(COLUMN_ENABLED, 22) def __resort(self): """Resorts the tree""" self.model().sort(self.header().sortIndicatorSection(), self.header().sortIndicatorOrder()) def toSourceIndex(self, index): """Converts an index to a source index""" return self.sortingModel.mapToSource(index) def __fromSourceIndex(self, sindex): """Convert a source index to an index""" return self.sortingModel.mapFromSource(sindex) def __setRowSelected(self, index, selected=True): """Selects a row""" if not index.isValid(): return if selected: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) else: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.Deselect | QItemSelectionModel.Rows) self.selectionModel().select(index, flags) def __createPopupMenus(self): """Generate the popup menu""" self.menu = QMenu() self.__editAct = self.menu.addAction(getIcon('bpprops.png'), "Edit...", self.__editBreak) self.__jumpToCodeAct = self.menu.addAction(getIcon('gotoline.png'), "Jump to code", self.__showSource) self.menu.addSeparator() self.__enableAct = self.menu.addAction(getIcon('bpenable.png'), "Enable", self.enableBreak) self.__enableAllAct = self.menu.addAction(getIcon('bpenableall.png'), "Enable all", self.enableAllBreaks) self.menu.addSeparator() self.__disableAct = self.menu.addAction(getIcon('bpdisable.png'), "Disable", self.disableBreak) self.__disableAllAct = self.menu.addAction(getIcon('bpdisableall.png'), "Disable all", self.disableAllBreaks) self.menu.addSeparator() self.__delAct = self.menu.addAction(getIcon('bpdel.png'), "Delete", self.deleteBreak) self.__delAllAct = self.menu.addAction(getIcon('bpdelall.png'), "Delete all", self.deleteAllBreaks) def __showContextMenu(self, _): """Shows the context menu""" index = self.currentIndex() if not index.isValid(): return sindex = self.toSourceIndex(index) if not sindex.isValid(): return bpoint = self.__model.getBreakPointByIndex(sindex) if not bpoint: return enableCount, disableCount = self.__model.getCounts() self.__editAct.setEnabled(True) self.__enableAct.setEnabled(not bpoint.isEnabled()) self.__disableAct.setEnabled(bpoint.isEnabled()) self.__jumpToCodeAct.setEnabled(True) self.__delAct.setEnabled(True) self.__enableAllAct.setEnabled(disableCount > 0) self.__disableAllAct.setEnabled(enableCount > 0) self.__delAllAct.setEnabled(enableCount + disableCount > 0) self.menu.popup(QCursor.pos()) def __doubleClicked(self, index): """Handles the double clicked signal""" if not index.isValid(): return sindex = self.toSourceIndex(index) if not sindex.isValid(): return # Jump to the code bpoint = self.__model.getBreakPointByIndex(sindex) fileName = bpoint.getAbsoluteFileName() line = bpoint.getLineNumber() self.jumpToCode(fileName, line) @staticmethod def jumpToCode(fileName, line): """Jumps to the source code""" editorsManager = GlobalData().mainWindow.editorsManager() editorsManager.openFile(fileName, line) editor = editorsManager.currentWidget().getEditor() editor.gotoLine(line) editorsManager.currentWidget().setFocus() def __editBreak(self): """Handle the edit breakpoint context menu entry""" index = self.currentIndex() if index.isValid(): self.__editBreakpoint(index) def __editBreakpoint(self, index): """Edits a breakpoint""" sindex = self.toSourceIndex(index) if sindex.isValid(): bpoint = self.__model.getBreakPointByIndex(sindex) if not bpoint: return dlg = BreakpointEditDialog(bpoint) if dlg.exec_() == QDialog.Accepted: newBpoint = dlg.getData() if newBpoint == bpoint: return self.__model.setBreakPointByIndex(sindex, newBpoint) self.layoutDisplay() def __setBpEnabled(self, index, enabled): """Sets the enabled status of a breakpoint""" sindex = self.toSourceIndex(index) if sindex.isValid(): self.__model.setBreakPointEnabledByIndex(sindex, enabled) def enableBreak(self): """Handles the enable breakpoint context menu entry""" index = self.currentIndex() self.__setBpEnabled(index, True) self.__resizeColumns() self.__resort() def enableAllBreaks(self): """Handles the enable all breakpoints context menu entry""" index = self.model().index(0, 0) while index.isValid(): self.__setBpEnabled(index, True) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def disableBreak(self): """Handles the disable breakpoint context menu entry""" index = self.currentIndex() self.__setBpEnabled(index, False) self.__resizeColumns() self.__resort() def disableAllBreaks(self): """Handles the disable all breakpoints context menu entry""" index = self.model().index(0, 0) while index.isValid(): self.__setBpEnabled(index, False) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def deleteBreak(self): """Handles the delete breakpoint context menu entry""" index = self.currentIndex() sindex = self.toSourceIndex(index) if sindex.isValid(): self.__model.deleteBreakPointByIndex(sindex) def deleteAllBreaks(self): """Handles the delete all breakpoints context menu entry""" self.__model.deleteAll() def __showSource(self): """Handles the goto context menu entry""" index = self.currentIndex() self.__doubleClicked(index) def highlightBreakpoint(self, fname, lineno): """Handles the clientLine signal""" sindex = self.__model.getBreakPointIndex(fname, lineno) if sindex.isValid(): return index = self.__fromSourceIndex(sindex) if index.isValid(): self.__clearSelection() self.__setRowSelected(index, True) def __getSelectedItemsCount(self): """Provides the count of items selected""" count = len(self.selectedIndexes()) / (self.__model.columnCount() - 1) # column count is 1 greater than selectable return count def selectionChanged(self, selected, deselected): """The slot is called when the selection has changed""" if selected.indexes(): self.sigSelectionChanged.emit(selected.indexes()[0]) else: self.sigSelectionChanged.emit(QModelIndex()) QTreeView.selectionChanged(self, selected, deselected)
class ClientExceptionsViewer(QWidget): """Implements the client exceptions viewer for a debugger""" sigClientExceptionsCleared = pyqtSignal() def __init__(self, parent, ignoredExceptionsViewer): QWidget.__init__(self, parent) self.__ignoredExceptionsViewer = ignoredExceptionsViewer self.__currentItem = None self.__createPopupMenu() self.__createLayout() GlobalData().project.sigProjectChanged.connect(self.__onProjectChanged) def setFocus(self): """Sets the widget focus""" self.exceptionsList.setFocus() def __createPopupMenu(self): """Creates the popup menu""" self.__excptMenu = QMenu() self.__addToIgnoreMenuItem = self.__excptMenu.addAction( "Add to ignore list", self.__onAddToIgnore) self.__jumpToCodeMenuItem = self.__excptMenu.addAction( "Jump to code", self.__onJumpToCode) def __createLayout(self): """Creates the widget layout""" verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(0, 0, 0, 0) verticalLayout.setSpacing(0) self.__excptLabel = QLabel("Exceptions", self) self.headerFrame = QFrame() self.headerFrame.setObjectName('excpt') self.headerFrame.setStyleSheet('QFrame#excpt {' + getLabelStyle(self.__excptLabel) + '}') self.headerFrame.setFixedHeight(HEADER_HEIGHT) headerLayout = QHBoxLayout() headerLayout.setContentsMargins(0, 0, 0, 0) headerLayout.addSpacing(3) headerLayout.addWidget(self.__excptLabel) self.headerFrame.setLayout(headerLayout) self.exceptionsList = QTreeWidget(self) self.exceptionsList.setSortingEnabled(False) self.exceptionsList.setAlternatingRowColors(True) self.exceptionsList.setRootIsDecorated(True) self.exceptionsList.setItemsExpandable(True) self.exceptionsList.setUniformRowHeights(True) self.exceptionsList.setSelectionMode(QAbstractItemView.SingleSelection) self.exceptionsList.setSelectionBehavior(QAbstractItemView.SelectRows) self.exceptionsList.setItemDelegate(NoOutlineHeightDelegate(4)) self.exceptionsList.setContextMenuPolicy(Qt.CustomContextMenu) self.__addToIgnoreButton = QAction( getIcon('add.png'), "Add exception to the list of ignored", self) self.__addToIgnoreButton.triggered.connect(self.__onAddToIgnore) self.__addToIgnoreButton.setEnabled(False) expandingSpacer = QWidget() expandingSpacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.__jumpToCodeButton = QAction( getIcon('gotoline.png'), "Jump to the code", self) self.__jumpToCodeButton.triggered.connect(self.__onJumpToCode) self.__jumpToCodeButton.setEnabled(False) self.__delAllButton = QAction( getIcon('trash.png'), "Delete all the client exceptions", self) self.__delAllButton.triggered.connect(self.__onDelAll) self.__delAllButton.setEnabled(False) self.toolbar = QToolBar() self.toolbar.setOrientation(Qt.Horizontal) self.toolbar.setMovable(False) self.toolbar.setAllowedAreas(Qt.TopToolBarArea) self.toolbar.setIconSize(QSize(16, 16)) self.toolbar.setFixedHeight(28) self.toolbar.setContentsMargins(0, 0, 0, 0) self.toolbar.addAction(self.__addToIgnoreButton) self.toolbar.addAction(self.__jumpToCodeButton) self.toolbar.addWidget(expandingSpacer) self.toolbar.addAction(self.__delAllButton) self.exceptionsList.itemDoubleClicked.connect( self.__onExceptionDoubleClicked) self.exceptionsList.customContextMenuRequested.connect( self.__showContextMenu) self.exceptionsList.itemSelectionChanged.connect( self.__onSelectionChanged) self.exceptionsList.setHeaderLabels(["Exception", "Function", "Arguments"]) verticalLayout.addWidget(self.headerFrame) verticalLayout.addWidget(self.toolbar) verticalLayout.addWidget(self.exceptionsList) def clear(self): """Clears the content""" self.exceptionsList.clear() self.__updateExceptionsLabel() self.__addToIgnoreButton.setEnabled(False) self.__jumpToCodeButton.setEnabled(False) self.__delAllButton.setEnabled(False) self.__currentItem = None self.sigClientExceptionsCleared.emit() def __onExceptionDoubleClicked(self, item, column): """Triggered when an exception is double clicked""" del item # unused argument del column # unused argument if self.__currentItem is not None: if self.__currentItem.getType() == STACK_FRAME_ITEM: self.__onJumpToCode() return # This is an exception item itself. # Open a separate dialog window with th detailed info. def __showContextMenu(self, coord): """Shows the frames list context menu""" self.__currentItem = self.exceptionsList.itemAt(coord) self.__addToIgnoreMenuItem.setEnabled( self.__addToIgnoreButton.isEnabled()) self.__jumpToCodeMenuItem.setEnabled( self.__jumpToCodeButton.isEnabled()) if self.__currentItem is not None: self.__excptMenu.popup(QCursor.pos()) def __onAddToIgnore(self): """Adds an exception into the ignore list""" if self.__currentItem is not None: self.__ignoredExceptionsViewer.addExceptionFilter( str(self.__currentItem.getExceptionType())) self.__addToIgnoreButton.setEnabled(False) def __onJumpToCode(self): """Jumps to the corresponding source code line""" if self.__currentItem is not None: if self.__currentItem.getType() == STACK_FRAME_ITEM: fileName = self.__currentItem.getFileName() if '<' not in fileName and '>' not in fileName: lineNumber = self.__currentItem.getLineNumber() editorsManager = GlobalData().mainWindow.editorsManager() editorsManager.openFile(fileName, lineNumber) editor = editorsManager.currentWidget().getEditor() editor.gotoLine(lineNumber) editorsManager.currentWidget().setFocus() def __onDelAll(self): """Triggered when all the exceptions should be deleted""" self.clear() def addException(self, exceptionType, exceptionMessage, stackTrace): """Adds the exception to the view""" for index in range(self.exceptionsList.topLevelItemCount()): item = self.exceptionsList.topLevelItem(index) if item.equal(exceptionType, exceptionMessage, stackTrace): item.incrementCounter() self.exceptionsList.clearSelection() self.exceptionsList.setCurrentItem(item) self.__updateExceptionsLabel() return item = ExceptionItem(self.exceptionsList, exceptionType, exceptionMessage, stackTrace) self.exceptionsList.clearSelection() self.exceptionsList.setCurrentItem(item) self.__updateExceptionsLabel() self.__delAllButton.setEnabled(True) def __updateExceptionsLabel(self): """Updates the exceptions header label""" total = self.getTotalCount() if total > 0: self.__excptLabel.setText("Exceptions (total: " + str(total) + ")") else: self.__excptLabel.setText("Exceptions") def getTotalCount(self): """Provides the total number of exceptions""" count = 0 for index in range(self.exceptionsList.topLevelItemCount()): count += self.exceptionsList.topLevelItem(index).getCount() return count def __onProjectChanged(self, what): """Triggered when a project is changed""" if what == CodimensionProject.CompleteProject: self.clear() def __onSelectionChanged(self): """Triggered when the current item is changed""" selected = list(self.exceptionsList.selectedItems()) if selected: self.__currentItem = selected[0] if self.__currentItem.getType() == STACK_FRAME_ITEM: fileName = self.__currentItem.getFileName() if '<' in fileName or '>' in fileName: self.__jumpToCodeButton.setEnabled(False) else: self.__jumpToCodeButton.setEnabled(True) self.__addToIgnoreButton.setEnabled(False) else: self.__jumpToCodeButton.setEnabled(False) excType = str(self.__currentItem.getExceptionType()) if self.__ignoredExceptionsViewer.isIgnored(excType) or \ " " in excType or excType.startswith("unhandled"): self.__addToIgnoreButton.setEnabled(False) else: self.__addToIgnoreButton.setEnabled(True) else: self.__currentItem = None self.__addToIgnoreButton.setEnabled(False) self.__jumpToCodeButton.setEnabled(False)
class EditorContextMenuMixin: """Encapsulates the context menu handling""" def __init__(self): self.encodingReloadMenu = QMenu("Set &encoding and reload") self.encodingReloadActGrp = QActionGroup(self) self.encodingWriteMenu = QMenu("Set encodin&g") self.encodingWriteActGrp = QActionGroup(self) mainWindow = GlobalData().mainWindow editorsManager = mainWindow.editorsManagerWidget.editorsManager self._menu = QMenu(self) self.__menuUndo = self._menu.addAction(getIcon('undo.png'), '&Undo', self.onUndo, "Ctrl+Z") self.__menuRedo = self._menu.addAction(getIcon('redo.png'), '&Redo', self.onRedo, "Ctrl+Y") self._menu.addSeparator() self.__menuCut = self._menu.addAction(getIcon('cutmenu.png'), 'Cu&t', self.onShiftDel, "Ctrl+X") self.__menuCopy = self._menu.addAction(getIcon('copymenu.png'), '&Copy', self.onCtrlC, "Ctrl+C") self.__menuPaste = self._menu.addAction(getIcon('pastemenu.png'), '&Paste', self.paste, "Ctrl+V") self.__menuSelectAll = self._menu.addAction( getIcon('selectallmenu.png'), 'Select &all', self.selectAll, "Ctrl+A") self._menu.addSeparator() self.__initReloadEncodingMenu() self.encodingReloadMenu.setIcon(getIcon('textencoding.png')) self._menu.addMenu(self.encodingReloadMenu) self.__initWriteEncodingMenu() self.encodingWriteMenu.setIcon(getIcon('textencoding.png')) menu = self._menu.addMenu(self.encodingWriteMenu) self.__menuClearEncoding = self._menu.addAction( getIcon('clearmenu.png'), 'Clear explicit encoding', self.__onClearEncoding) self._menu.addSeparator() menu = self._menu.addMenu(self.__initToolsMenu()) menu.setIcon(getIcon('toolsmenu.png')) self._menu.addSeparator() menu = self._menu.addMenu(self.__initDiagramsMenu()) menu.setIcon(getIcon('diagramsmenu.png')) self._menu.addSeparator() self.__menuOpenAsFile = self._menu.addAction(getIcon('filemenu.png'), 'O&pen as file', self.openAsFile) self.__menuDownloadAndShow = self._menu.addAction( getIcon('filemenu.png'), 'Do&wnload and show', self.downloadAndShow) self.__menuOpenInBrowser = self._menu.addAction( getIcon('homepagemenu.png'), 'Open in browser', self.openInBrowser) self._menu.addSeparator() self.__menuHighlightInPrj = self._menu.addAction( getIcon("highlightmenu.png"), "&Highlight in project browser", editorsManager.onHighlightInPrj) self.__menuHighlightInFS = self._menu.addAction( getIcon("highlightmenu.png"), "H&ighlight in file system browser", editorsManager.onHighlightInFS) self._menuHighlightInOutline = self._menu.addAction( getIcon("highlightmenu.png"), "Highlight in &outline browser", self.highlightInOutline, 'Ctrl+B') # Plugins support self.__pluginMenuSeparator = self._menu.addSeparator() mainWindow = GlobalData().mainWindow editorsManager = mainWindow.editorsManagerWidget.editorsManager registeredMenus = editorsManager.getPluginMenus() if registeredMenus: for path in registeredMenus: self._menu.addMenu(registeredMenus[path]) else: self.__pluginMenuSeparator.setVisible(False) editorsManager.sigPluginContextMenuAdded.connect( self.__onPluginMenuAdded) editorsManager.sigPluginContextMenuRemoved.connect( self.__onPluginMenuRemoved) def __initReloadEncodingMenu(self): """Creates the encoding menu for reloading the existing file""" for encoding in sorted(SUPPORTED_CODECS): act = self.encodingReloadMenu.addAction(encoding) act.setCheckable(True) act.setData(encoding) self.encodingReloadActGrp.addAction(act) self.encodingReloadMenu.triggered.connect(self.__onReloadWithEncoding) def __initWriteEncodingMenu(self): """Creates the encoding menu for further read/write operations""" for encoding in sorted(SUPPORTED_CODECS): act = self.encodingWriteMenu.addAction(encoding) act.setCheckable(True) act.setData(encoding) self.encodingWriteActGrp.addAction(act) self.encodingWriteMenu.triggered.connect(self.__onReadWriteEncoding) def __initToolsMenu(self): """Creates the tools menu""" self.toolsMenu = QMenu('Python too&ls') self.runAct = self.toolsMenu.addAction(getIcon('run.png'), 'Run script', self._parent.onRunScript) self.runParamAct = self.toolsMenu.addAction( getIcon('paramsmenu.png'), 'Set parameters and run', self._parent.onRunScriptDlg) self.toolsMenu.addSeparator() self.profileAct = self.toolsMenu.addAction( getIcon('profile.png'), 'Profile script', self._parent.onProfileScript) self.profileParamAct = self.toolsMenu.addAction( getIcon('paramsmenu.png'), 'Set parameters and profile', self._parent.onProfileScriptDlg) self.toolsMenu.addSeparator() self.disasmMenu = QMenu('Disassembly') self.disasmMenu.setIcon(getIcon('disassembly.png')) self.disasmAct0 = self.disasmMenu.addAction( getIcon(''), 'Disassembly (no optimization)', self._onDisasm0) self.disasmAct1 = self.disasmMenu.addAction( getIcon(''), 'Disassembly (optimization level 1)', self._onDisasm1) self.disasmAct2 = self.disasmMenu.addAction( getIcon(''), 'Disassembly (optimization level 2)', self._onDisasm2) self.toolsMenu.addMenu(self.disasmMenu) return self.toolsMenu def __initDiagramsMenu(self): """Creates the diagrams menu""" self.diagramsMenu = QMenu("&Diagrams") self.importsDgmAct = self.diagramsMenu.addAction( getIcon('importsdiagram.png'), 'Imports diagram', self._parent.onImportDgm) self.importsDgmParamAct = self.diagramsMenu.addAction( getIcon('paramsmenu.png'), 'Fine tuned imports diagram', self._parent.onImportDgmTuned) return self.diagramsMenu def contextMenuEvent(self, event): """Called just before showing a context menu""" # Accepting needs to suppress the native menu event.accept() isPython = self.isPythonBuffer() readOnly = self.isReadOnly() self.__menuUndo.setEnabled(self.document().isUndoAvailable()) self.__menuRedo.setEnabled(self.document().isRedoAvailable()) self.__menuCut.setEnabled(not readOnly) self.__menuPaste.setEnabled(QApplication.clipboard().text() != "" and not readOnly) fileName = self._parent.getFileName() absFileName = os.path.isabs(fileName) self.__menuOpenAsFile.setEnabled(self.openAsFileAvailable()) self.__menuDownloadAndShow.setEnabled(self.downloadAndShowAvailable()) self.__menuOpenInBrowser.setEnabled(self.downloadAndShowAvailable()) self.__menuHighlightInPrj.setEnabled( absFileName and GlobalData().project.isProjectFile(fileName)) self.__menuHighlightInFS.setEnabled(absFileName) self._menuHighlightInOutline.setEnabled(isPython) self._menuHighlightInOutline.setEnabled(isPython) self.toolsMenu.setEnabled(isPython) if isPython: runEnabled = self._parent.runScriptButton.isEnabled() self.runAct.setEnabled(runEnabled) self.runParamAct.setEnabled(runEnabled) self.profileAct.setEnabled(runEnabled) self.profileParamAct.setEnabled(runEnabled) if absFileName: self.__menuClearEncoding.setEnabled( getFileEncoding(fileName) is not None) else: self.__menuClearEncoding.setEnabled( self.explicitUserEncoding is not None) # Check the proper encoding in the menu encoding = 'undefined' if absFileName: enc = getFileEncoding(fileName) if enc: encoding = enc else: if self.explicitUserEncoding: encoding = self.explicitUserEncoding encoding = getNormalizedEncoding(encoding, False) if absFileName: for act in self.encodingReloadActGrp.actions(): act.setChecked(encoding == getNormalizedEncoding(act.data())) else: self.encodingReloadMenu.setEnabled(False) for act in self.encodingWriteActGrp.actions(): act.setChecked(encoding == getNormalizedEncoding(act.data())) # Show the menu self._menu.popup(event.globalPos()) def __isSameEncodingAsCurrent(self, enc): """True if the same encoding has already been set""" fileName = self._parent.getFileName() if not os.path.isabs(fileName): # New unsaved yet file if not self.explicitUserEncoding: return False return getNormalizedEncoding(enc) == getNormalizedEncoding( self.explicitUserEncoding) # Existed before or just saved new file currentEnc = getFileEncoding(fileName) if not currentEnc: return False return getNormalizedEncoding(currentEnc) == getNormalizedEncoding(enc) def __onReloadWithEncoding(self, act): """Triggered when encoding is selected""" # The method is called only for the existing disk files encoding = act.data() if self.__isSameEncodingAsCurrent(encoding): return if self.document().isModified(): res = QMessageBox.warning( self, 'Continue loosing changes', '<p>The buffer has unsaved changes. Are you sure to continue ' 'reloading the content using ' + encoding + ' encoding and ' 'loosing the changes?</p>', QMessageBox.StandardButtons(QMessageBox.Cancel | QMessageBox.Yes), QMessageBox.Cancel) if res == QMessageBox.Cancel: return # Do the reload fileName = self._parent.getFileName() setFileEncoding(fileName, encoding) self.__updateFilePosition() self.readFile(fileName) self.__restoreFilePosition() self.__updateMainWindowStatusBar() def __onReadWriteEncoding(self, act): """Sets explicit encoding for further read/write ops""" encoding = act.data() if self.__isSameEncodingAsCurrent(encoding): return # fileName = self._parent.getFileName() # absFileName = os.path.isabs(fileName) self.document().setModified(True) self.explicitUserEncoding = encoding self.__updateMainWindowStatusBar() def __onClearEncoding(self): """Clears the explicitly set encoding""" self.explicitUserEncoding = None fileName = self._parent.getFileName() absFileName = os.path.isabs(fileName) if absFileName: setFileEncoding(fileName, None) self.encoding = detectEncodingOnClearExplicit(fileName, self.text) self.__updateMainWindowStatusBar() def onUndo(self): """Undo implementation""" if self.document().isUndoAvailable(): self.undo() self._parent.modificationChanged() def onRedo(self): """Redo implementation""" if self.document().isRedoAvailable(): self.redo() self._parent.modificationChanged() def __onPluginMenuAdded(self, menu, count): """Triggered when a new menu was added""" del count # unused argument self._menu.addMenu(menu) self.__pluginMenuSeparator.setVisible(True) def __onDisasm(self, optimization): """Common implementation""" if self.isPythonBuffer(): if os.path.isabs(self._parent.getFileName()): if not self._parent.isModified(): GlobalData().mainWindow.showFileDisassembly( self._parent.getFileName(), optimization) return fileName = self._parent.getFileName() if not fileName: fileName = self._parent.getShortName() encoding = self.encoding if not encoding: encoding = detectNewFileWriteEncoding(self, fileName) GlobalData().mainWindow.showBufferDisassembly( self.text, encoding, fileName, optimization) def _onDisasm0(self): """Triggered to disassemble the buffer without optimization""" self.__onDisasm(OPT_NO_OPTIMIZATION) def _onDisasm1(self): """Triggered to disassemble the buffer with optimization level 1""" self.__onDisasm(OPT_OPTIMIZE_ASSERT) def _onDisasm2(self): """Triggered to disassemble the buffer with optimization level 2""" self.__onDisasm(OPT_OPTIMIZE_DOCSTRINGS) def __onPluginMenuRemoved(self, menu, count): """Triggered when a menu was deleted""" self._menu.removeAction(menu.menuAction()) self.__pluginMenuSeparator.setVisible(count != 0) def highlightInOutline(self): """Triggered when highlight in outline browser is requested""" if self.isPythonBuffer(): info = getBriefModuleInfoFromMemory(self.text) context = getContext(self, info, True, False) line, _ = self.cursorPosition GlobalData().mainWindow.highlightInOutline(context, int(line) + 1) self.setFocus() @staticmethod def __updateMainWindowStatusBar(): """Updates the main window status bar""" mainWindow = GlobalData().mainWindow editorsManager = mainWindow.editorsManagerWidget.editorsManager editorsManager.updateStatusBar() @staticmethod def __updateFilePosition(): """Updates the position in a file""" mainWindow = GlobalData().mainWindow editorsManager = mainWindow.editorsManagerWidget.editorsManager editorsManager.updateFilePosition(None) @staticmethod def __restoreFilePosition(): """Restores the position in a file""" mainWindow = GlobalData().mainWindow editorsManager = mainWindow.editorsManagerWidget.editorsManager editorsManager.restoreFilePosition(None)
class CFSceneContextMenuMixin: """Encapsulates the context menu handling""" def __init__(self): self.menu = None self.individualMenus = {} # Scene menu preparation self.sceneMenu = QMenu() self.sceneMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...', self.parent().onSaveAsSVG) self.sceneMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...', self.parent().onSaveAsPDF) self.sceneMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...', self.parent().onSaveAsPNG) self.sceneMenu.addSeparator() self.sceneMenu.addAction(getIcon('copymenu.png'), 'Copy image to clipboard', self.parent().copyToClipboard) # Common menu for all the individually selected items self.commonMenu = QMenu() self.__ccSubmenuAction = self.commonMenu.addMenu( self.__initCustomColorsContextMenu()) self.__rtSubmenuAction = self.commonMenu.addMenu( self.__initReplaceTextContextMenu()) self.__docSubmenuAction = self.commonMenu.addMenu( self.__initDocContextMenu()) self.__groupAction = self.commonMenu.addAction( getIcon("cfgroup.png"), "Group...", self.onGroup) #self.commonMenu.addSeparator() #self.__cutAction = self.commonMenu.addAction( # getIcon("cutmenu.png"), "Cut (specific for graphics pane)", # self.onCut) #self.__copyAction = self.commonMenu.addAction( # getIcon("copymenu.png"), "Copy (specific for graphics pane)", # self.onCopy) #self.commonMenu.addSeparator() #self.commonMenu.addAction( # getIcon("trash.png"), "Delete", self.onDelete) # Individual items specific menu: begin ifContextMenu = QMenu() ifContextMenu.addAction( getIcon("switchbranches.png"), "Switch branch layout", self.onSwitchIfBranch) self.individualMenus[IfCell] = ifContextMenu self.individualMenus[OpenedGroupBegin] = self.__initOpenGroupContextMenu() self.individualMenus[CollapsedGroup] = self.__initCloseGroupContextMenu() self.individualMenus[EmptyGroup] = self.__initEmptyGroupContextMenu() # Individual items specific menu: end # Menu for a group of selected items self.groupMenu = QMenu() def __initOpenGroupContextMenu(self): """Creates the open group context menu""" ogMenu = QMenu() ogMenu.addAction(getIcon("collapse.png"), "Collapse", self.onGroupCollapse) ogMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle) ogMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup) return ogMenu def __initCloseGroupContextMenu(self): """Creates the closed group context menu""" cgMenu = QMenu() cgMenu.addAction(getIcon("expand.png"), "Expand", self.onGroupExpand) cgMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle) cgMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup) return cgMenu def __initEmptyGroupContextMenu(self): """Creates the empty group context menu""" egMenu = QMenu() egMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle) egMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup) return egMenu def __initCustomColorsContextMenu(self): """Create the custom colors submenu""" self.__customColorsSubmenu = QMenu('Custom colors') self.__customColorsSubmenu.setIcon(getIcon('customcolorsmenu.png')) self.__ccAction = self.__customColorsSubmenu.addAction( getIcon("customcolors.png"), "Custom colors...", self.onCustomColors) self.__customColorsSubmenu.addSeparator() self.__removeCCAction = self.__customColorsSubmenu.addAction( getIcon('trash.png'), 'Remove custom colors', self.onRemoveCustomColors) return self.__customColorsSubmenu def __initReplaceTextContextMenu(self): """Create the Replace text submenu""" self.__replaceTextSubmenu = QMenu('Replace text') self.__replaceTextSubmenu.setIcon(getIcon('replacetextmenu.png')) self.__rtAction = self.__replaceTextSubmenu.addAction( getIcon("replacetitle.png"), "Replace text...", self.onReplaceText) self.__replaceTextSubmenu.addSeparator() self.__removeRTAction = self.__replaceTextSubmenu.addAction( getIcon('trash.png'), 'Remove replacement text', self.onRemoveReplacementText) return self.__replaceTextSubmenu def __initDocContextMenu(self): """Create the Documentation submenu""" self.__docSubmenu = QMenu('Documentation') self.__docSubmenu.setIcon(getIcon('markdown.png')) self.__editDocAction = self.__docSubmenu.addAction( getIcon('replacetitle.png'), 'Add/edit doc link/anchor...', self.onEditDoc) self.__autoDocActon = self.__docSubmenu.addAction( getIcon('createdoc.png'), 'Create doc file, add link and open for editing', self.onAutoAddDoc) self.__docSubmenu.addSeparator() self.__removeDocAction = self.__docSubmenu.addAction( getIcon('trash.png'), 'Remove doc link/anchor', self.onRemoveDoc) return self.__docSubmenu def onContextMenu(self, event): """Triggered when a context menu should be shown""" selectedItems = self.selectedItems() selectionCount = len(selectedItems) if selectionCount == 0: self.sceneMenu.popup(event.screenPos()) return if selectionCount == 1: self.__buildIndividualMenu(selectedItems[0]) else: self.__buildGroupMenu(selectedItems) self.menu.popup(event.screenPos()) def __buildIndividualMenu(self, item): """Builds a context menu for the given item""" self.menu = QMenu() if type(item) in self.individualMenus: individualPart = self.individualMenus[type(item)] self.menu.addActions(individualPart.actions()) self.menu.addSeparator() self.menu.addActions(self.commonMenu.actions()) # Note: if certain items need to be disabled then it should be done # here self.__disableMenuItems() def __buildGroupMenu(self, items): """Builds a context menu for the group of items""" self.menu = QMenu() if type(items[0]) in self.individualMenus: if self.areSelectedOfTypes([[items[0].kind, items[0].subKind]]): individualPart = self.individualMenus[type(items[0])] self.menu.addActions(individualPart.actions()) self.menu.addSeparator() self.menu.addActions(self.commonMenu.actions()) if not self.groupMenu.isEmpty(): self.menu.addSeparator() self.menu.addActions(self.groupMenu.actions()) # Note: if certain items need to be disabled then it should be done # here self.__disableMenuItems() def __disableMenuItems(self): """Disables the common menu items as needed""" totalComments = self.countComments() hasComment = totalComments > 0 hasDocstring = self.isDocstringInSelection() hasMinimizedExcepts = self.isInSelected([(CellElement.EXCEPT_MINIMIZED, None)]) # Doc links are considered comments as well totalDocLinks = self.countInSelected([(CellElement.INDEPENDENT_DOC, None), (CellElement.LEADING_DOC, None), (CellElement.ABOVE_DOC, None)]) totalNonDocComments = totalComments - totalDocLinks totalGroups = sum(self.countGroups()) count = len(self.selectedItems()) totalCCGroups = sum(self.countGroupsWithCustomColors()) totalCCDocs = self.countDocWithCustomColors() self.__ccAction.setEnabled(totalNonDocComments == 0 and not hasMinimizedExcepts) self.__removeCCAction.setEnabled( self.countItemsWithCML(CMLcc) + totalCCGroups + totalCCDocs == count) self.__customColorsSubmenu.setEnabled(self.__ccAction.isEnabled() or self.__removeCCAction.isEnabled()) self.__rtAction.setEnabled(not hasComment and not hasDocstring and not hasMinimizedExcepts and totalDocLinks == 0 and totalGroups == 0) self.__removeRTAction.setEnabled( self.countItemsWithCML(CMLrt) == count) self.__replaceTextSubmenu.setEnabled(self.__rtAction.isEnabled() or self.__removeRTAction.isEnabled()) self.__groupAction.setEnabled(self.__canBeGrouped()) itemsWithDocCML = self.countItemsWithCML(CMLdoc) self.__removeDocAction.setEnabled(totalDocLinks + itemsWithDocCML == count) if count != 1 or totalNonDocComments != 0 or hasDocstring or totalGroups != 0: self.__editDocAction.setEnabled(False) self.__autoDocActon.setEnabled(False) else: self.__editDocAction.setEnabled(True) fileName = None editor = self.selectedItems()[0].getEditor() if editor: fileName = editor._parent.getFileName() self.__autoDocActon.setEnabled( fileName and totalDocLinks + itemsWithDocCML == 0) self.__docSubmenu.setEnabled(self.__removeDocAction.isEnabled() or self.__editDocAction.isEnabled() or self.__autoDocActon.isEnabled()) #self.__cutAction.setEnabled(count == 1) #self.__copyAction.setEnabled(count == 1) def __actionPrerequisites(self): """True if an editor related action can be done""" selectedItems = self.selectedItems() if not selectedItems: return False editor = selectedItems[0].getEditor() if editor is None: return False return True def onSwitchIfBranch(self): """If primitive should switch the branches""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.IF: cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLsw) if cmlComment is None: # Did not exist, so needs to be generated line = CMLsw.generate(item.ref.body.beginPos) lineNo = item.getFirstLine() editor.insertLines(line, lineNo) else: # Existed, so it just needs to be deleted cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onCustomColors(self): """Custom background and foreground colors""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() bgcolor, fgcolor, bordercolor = self.selectedItems()[0].getColors() hasDocstring = self.isDocstringInSelection() dlg = CustomColorsDialog(bgcolor, fgcolor, None if hasDocstring else bordercolor, self.parent()) if dlg.exec_(): bgcolor = dlg.backgroundColor() fgcolor = dlg.foregroundColor() bordercolor = dlg.borderColor() editor = self.selectedItems()[0].getEditor() with editor: # Add colors is done via delete/insert for the Doc and group # items. So it is safer to do first because the cc comment may be # in a set of selected which is inserted before the doc cml and # thus breaks the line numbering for item in self.selectedItems(): if item.isCMLDoc(): # The doc always exists so just add/change the colors item.cmlRef.updateCustomColors(editor, bgcolor, fgcolor, bordercolor) continue if item.isGroupItem(): # The group always exists so just add/change the colors item.groupBeginCMLRef.updateCustomColors(editor, bgcolor, fgcolor, bordercolor) for item in self.sortSelectedReverse(): if item.isCMLDoc() or item.isGroupItem(): continue if item.isDocstring(): cmlComment = CMLVersion.find( item.ref.docstring.leadingCMLComments, CMLcc) else: cmlComment = CMLVersion.find( item.ref.leadingCMLComments, CMLcc) if cmlComment is not None: # Existed, so remove the old one first lineNo = cmlComment.ref.beginLine cmlComment.removeFromText(editor) else: lineNo = item.getFirstLine() pos = item.ref.body.beginPos if item.isDocstring(): pos = item.ref.docstring.beginPos line = CMLcc.generate(bgcolor, fgcolor, bordercolor, pos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onReplaceText(self): """Replace the code with a title""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() dlg = ReplaceTextDialog('Replace text', 'Item caption:', self.parent()) # If it was one item selection and there was a previous text then # set it for editing if len(self.selectedItems()) == 1: cmlComment = CMLVersion.find( self.selectedItems()[0].ref.leadingCMLComments, CMLrt) if cmlComment is not None: dlg.setText(cmlComment.getText()) if dlg.exec_(): replacementText = dlg.text() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find( item.ref.leadingCMLComments, CMLrt) if cmlComment is not None: # Existed, so remove the old one first lineNo = cmlComment.ref.beginLine cmlComment.removeFromText(editor) else: lineNo = item.getFirstLine() line = CMLrt.generate(replacementText, item.ref.body.beginPos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onGroupCollapse(self): """Collapses the selected group""" if not self.__actionPrerequisites(): return # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.OPENED_GROUP_BEGIN: fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() addCollapsedGroup(fileName, item.getGroupId()) QApplication.processEvents() self.parent().redrawNow() def onGroupExpand(self): """Expands the selected group""" if not self.__actionPrerequisites(): return # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.COLLAPSED_GROUP: fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() removeCollapsedGroup(fileName, item.getGroupId()) QApplication.processEvents() self.parent().redrawNow() def onGroupEditTitle(self): """Edit (or view) the group title""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent()) # If it was one item selection and there was a previous text then # set it for editing if len(self.selectedItems()) == 1: title = self.selectedItems()[0].getTitle() if title: dlg.setText(title) if dlg.exec_(): newTitle = dlg.text() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): item.groupBeginCMLRef.updateTitle(editor, newTitle) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onGroupUngroup(self): """Ungroups the items""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): item.groupEndCMLRef.removeFromText(editor) item.groupBeginCMLRef.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByTooltip(selection) def onDelete(self): """Delete the item""" print("Delete") def onGroup(self): """Groups items into a single one""" dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent()) if dlg.exec_(): groupTitle = dlg.text() selected = self.__extendSelectionForGrouping() selected = self.sortSelected(selected) editor = selected[0].getEditor() firstLine, lastLine, pos = self.__getLineRange(selected) groupid = self.parent().generateNewGroupId() beginComment = CMLgb.generate(groupid, groupTitle, None, None, None, pos) endComment = CMLge.generate(groupid, pos) with editor: editor.insertLines(endComment, lastLine + 1) editor.insertLines(beginComment, firstLine) # Redraw the group collapsed fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() addCollapsedGroup(fileName, groupid) QApplication.processEvents() self.parent().redrawNow() def onCopy(self): """Copying...""" selectedItems = self.selectedItems() if selectedItems: if len(selectedItems) > 1: print('Copying multiple items has not been implemented yet') return selectedItems[0].copyToClipboard() def onCut(self): """Cutting...""" print("Cut") def onRemoveCustomColors(self): """Removing the previously set custom colors""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() editor = self.selectedItems()[0].getEditor() with editor: # Remove colors is done via delete/insert for the Doc and group # items. So it is safer to do first because the cc comment may be # in a set of selected which is inserted before the doc cml and # thus breaks the line numbering for item in self.selectedItems(): # The doc always exists if item.isCMLDoc(): item.cmlRef.removeCustomColors(editor) continue # The group always exists if item.isGroupItem(): item.groupBeginCMLRef.removeCustomColors(editor) # Now handle the rest of items for item in self.sortSelectedReverse(): if item.isCMLDoc() or item.isGroupItem(): continue if item.isDocstring(): cmlComment = CMLVersion.find( item.ref.docstring.leadingCMLComments, CMLcc) else: cmlComment = CMLVersion.find( item.ref.leadingCMLComments, CMLcc) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onRemoveReplacementText(self): """Removing replacement text""" if self.__actionPrerequisites(): # Memorize the current selection selection = self.serializeSelection() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLrt) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def areSelectedOfTypes(self, matchList): """Checks if the selected items belong to the match""" # match is a list of pairs [kind, subKind] # None would mean 'match any' selectedItems = self.selectedItems() if selectedItems: for selectedItem in selectedItems: for kind, subKind in matchList: match = True if kind is not None: if kind != selectedItem.kind: match = False if subKind is not None: if subKind != selectedItem.subKind: match = False if match: break else: return False return True return False def __createDocFile(self, link, fromFile): """Creates the doc file if needed""" fName, _, errMsg = preResolveLinkPath(link, fromFile, True) if errMsg: logging.error(errMsg) return None if os.path.exists(fName): return fName try: os.makedirs(os.path.dirname(fName), exist_ok=True) with open(fName, 'w') as f: pass except Exception as exc: logging.error('Error creating the documentation file ' + fName + ': ' + str(exc)) return None return fName def onEditDoc(self): """Editing the CML doc comment""" if not self.__actionPrerequisites(): return selectedItem = self.selectedItems()[0] # Exactly one is selected editor = selectedItem.getEditor() fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() # It could be a CML doc or an item which has a CML doc if selectedItem.isComment(): cmlRef = selectedItem.cmlRef else: # If not found then it means the doc link needs to be created cmlRef = self.__findCMLinItem(selectedItem, CMLdoc) dlg = DocLinkAnchorDialog('Add' if cmlRef is None else 'Edit', cmlRef, fileName, self.parent()) if dlg.exec_(): link = dlg.linkEdit.text().strip() anchor = dlg.anchorEdit.text().strip() title = dlg.title() needToCreate = dlg.needToCreate() # First create a file if asked if needToCreate: docFileName = self.__createDocFile(link, fileName) if not docFileName: return selection = self.serializeSelection() with editor: # Now insert a new cml comment or update existing if cmlRef: # It is editing, the comment exists lineNo = cmlRef.ref.beginLine pos = cmlRef.ref.beginPos cmlRef.removeFromText(editor) bgColor = cmlRef.bgColor fgColor = cmlRef.fgColor border = cmlRef.border else: # It is a new doc link lineNo = selectedItem.getFirstLine() pos = selectedItem.ref.body.beginPos bgColor = None fgColor = None border = None line = CMLdoc.generate(link, anchor, title, bgColor, fgColor, border, pos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) @staticmethod def __getAutoDocFileName(fileName): """Forms the auto doc file name""" # Markdown is used as a default documentation format fBaseName = os.path.basename(fileName) if '.' in fBaseName: fileExtension = fBaseName.split('.')[-1] fBaseName = fBaseName[:-len(fileExtension)] + 'md' else: fBaseName += '.md' project = GlobalData().project if project.isProjectFile(fileName): projectDir = project.getProjectDir() relativePath = fileName[len(projectDir):] projectName = project.getProjectName() if relativePath.startswith(projectName): relativePath = relativePath.replace(projectName, '', 1) return os.path.normpath( os.path.sep.join([projectDir + 'doc', os.path.dirname(relativePath), fBaseName])) return os.path.normpath( os.path.sep.join([os.path.dirname(fileName), 'doc', fBaseName])) def onAutoAddDoc(self): """Create a doc file, add a link and open for editing""" if not self.__actionPrerequisites(): return selectedItem = self.selectedItems()[0] # Exactly one is selected editor = selectedItem.getEditor() fileName = editor._parent.getFileName() if not fileName: logging.error('Save file before invoking auto doc') return needContent = False newAnchor = 'doc' + str(uuid.uuid4().fields[-1])[-6:] docFileName = self.__getAutoDocFileName(fileName) if not os.path.exists(docFileName): # Create file and populate with the default content try: os.makedirs(os.path.dirname(docFileName), exist_ok=True) with open(docFileName, 'w') as f: pass except Exception as exc: logging.error('Error creating the documentation file ' + docFileName + ': ' + str(exc)) return needContent = True project = GlobalData().project if project.isProjectFile(docFileName): link = project.getRelativePath(docFileName) else: link = os.path.relpath(docFileName, fileName) # Insert a doc link with editor: lineNo = selectedItem.getFirstLine() line = CMLdoc.generate(link, newAnchor, 'See documentation', None, None, None, selectedItem.ref.body.beginPos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() # Open the file if GlobalData().mainWindow.openFile(docFileName, -1): if needContent: widget = GlobalData().mainWindow.em.getWidgetForFileName(docFileName) editor = widget.getEditor() editor.text = getDefaultFileDoc(fileName, newAnchor) editor.document().setModified(False) def onRemoveDoc(self): """Removing the CML doc comment""" if not self.__actionPrerequisites(): return editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLdoc) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() def countInSelected(self, matchList): """Counts the number of matching items in selection""" # match is a list of pairs [kind, subKind] # None would mean 'match any' count = 0 for selectedItem in self.selectedItems(): for kind, subKind in matchList: match = True if kind is not None: if kind != selectedItem.kind: match = False if subKind is not None: if subKind != selectedItem.subKind: match = False if match: count += 1 return count def isInSelected(self, matchList): """Checks if any if the match list items is in the selection""" return self.countInSelected(matchList) > 0 def isDocstringInSelection(self): """True if a docstring item in the selection""" for item in self.selectedItems(): if item.isDocstring(): return True return False def countComments(self): """Count comments in selection""" count = 0 for item in self.selectedItems(): if item.isComment(): count += 1 return count def isCommentInSelection(self): """True if a comment item in the selection""" return self.countComments() > 0 def countItemsWithCML(self, cmlType): """Counts items with have a certain type of a CML comment""" count = 0 for item in self.selectedItems(): if self.__findCMLinItem(item, cmlType) is not None: count += 1 return count def __findCMLinItem(self, item, cmlType): """Finds a related CML item""" if item.isComment(): # Doc links are comments so they are skipped here return None if item.isDocstring(): # Side comments for docstrings? Nonesense! So they are ignored # even if they are collected cml = CMLVersion.find(item.ref.docstring.leadingCMLComments, cmlType) if cml is not None: return cml if hasattr(item.ref, 'leadingCMLComments'): cml = CMLVersion.find(item.ref.leadingCMLComments, cmlType) if cml is not None: return cml if hasattr(item.ref, 'sideCMLComments'): cml = CMLVersion.find(item.ref.sideCMLComments, cmlType) if cml is not None: return cml return None def countGroups(self): """Counts empty, close and open groups""" emptyCount = 0 closeCount = 0 openCount = 0 for item in self.selectedItems(): if item.kind == CellElement.EMPTY_GROUP: emptyCount += 1 elif item.kind == CellElement.COLLAPSED_GROUP: closeCount += 1 elif item.kind == CellElement.OPENED_GROUP_BEGIN: openCount += 1 return emptyCount, closeCount, openCount def countGroupsWithCustomColors(self): """Counts the groups with any color defined""" emptyCount = 0 closeCount = 0 openCount = 0 for item in self.selectedItems(): if item.kind in [CellElement.EMPTY_GROUP, CellElement.COLLAPSED_GROUP, CellElement.OPENED_GROUP_BEGIN]: if item.groupBeginCMLRef.bgColor is not None or \ item.groupBeginCMLRef.fgColor is not None or \ item.groupBeginCMLRef.border is not None: if item.kind == CellElement.EMPTY_GROUP: emptyCount += 1 elif item.kind == CellElement.COLLAPSED_GROUP: closeCount += 1 else: openCount += 1 return emptyCount, closeCount, openCount def countDocWithCustomColors(self): count = 0 for item in self.selectedItems(): if item.isCMLDoc(): if item.cmlRef.bgColor is not None or \ item.cmlRef.fgColor is not None or \ item.cmlRef.border is not None: count += 1 return count def sortSelectedReverse(self): """Sorts the selected items in reverse order""" result = [] for item in self.selectedItems(): itemBegin = item.getAbsPosRange()[0] for index in range(len(result)): if itemBegin > result[index].getAbsPosRange()[0]: result.insert(index, item) break else: result.append(item) return result def sortSelected(self, selected): """Sorts the selected items in direct order""" result = [] for item in selected: itemBegin = item.getAbsPosRange()[0] for index in range(len(result)): if itemBegin < result[index].getAbsPosRange()[0]: result.insert(index, item) break else: result.append(item) return result def __canBeGrouped(self): """True if the selected items can be grouped""" # Cannot import it at the top... from .flowuiwidget import SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT if Settings()['smartZoom'] not in [SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT]: return False if self.__areAllSelectedComments(): return False if self.__areScopeDocstringOrCommentSelected(): return False if self.__isModuleSelected(): return False # Extend the selection with all the selected items comments selected = self.__extendSelectionForGrouping() if self.__areLoneCommentsSelected(selected): return False if self.__areIncompleteScopeSelected(selected): return False scopeCoveredRegions = self.__getSelectedScopeRegions(selected) # The __areIfFullySelected() also updates the regions with # fully selected if regions if not self.__areIfFullySelected(selected, scopeCoveredRegions): return False selected = self.sortSelected(selected) begin = selected[0].getAbsPosRange()[0] end = selected[-1].getAbsPosRange()[1] if not self.__isSelectionContinuous(selected, scopeCoveredRegions, begin, end): return False if self.__moreThanOneIfBranchSelected(selected, scopeCoveredRegions): return False return True def __areAllSelectedComments(self): """True if all selected items are comments""" for item in self.selectedItems(): if not item.isComment(): return False return True def __areScopeDocstringOrCommentSelected(self): for item in self.selectedItems(): if item.scopedItem(): if item.subKind in [ScopeCellElement.SIDE_COMMENT, ScopeCellElement.DOCSTRING]: return True return False def __isModuleSelected(self): """True if the whole module is selected""" for item in self.selectedItems(): if item.kind == CellElement.FILE_SCOPE: return True return False def __areIncompleteScopeSelected(self, selected): """True if an incomplete scope selected""" for item in selected: if item.kind in [CellElement.FOR_SCOPE, CellElement.WHILE_SCOPE]: if item.ref.elsePart: for relatedItem in self.findItemsForRef(item.ref.elsePart): if relatedItem not in selected: return True elif item.kind in [CellElement.TRY_SCOPE]: # It could be that the exception blocks are hidden, so there # will be exactly one more item instead of many and that item # will have a ref which matches the try scope. exceptPartCount = 0 for exceptPart in item.ref.exceptParts: for relatedItem in self.findItemsForRef(exceptPart): exceptPartCount += 1 if relatedItem not in selected: return True if exceptPartCount == 0: # here: no except blocks on the diagram, they are collapsed tryItems = self.findItemsForRef(item.ref) for tryItem in tryItems: if tryItem.kind == CellElement.EXCEPT_MINIMIZED: if not tryItem.isSelected(): return True break else: # The minimized except is not selected return True if item.ref.elsePart: for relatedItem in self.findItemsForRef(item.ref.elsePart): if relatedItem not in selected: return True if item.ref.finallyPart: for relatedItem in self.findItemsForRef(item.ref.finallyPart): if relatedItem not in selected: return True elif item.kind in [CellElement.ELSE_SCOPE, CellElement.EXCEPT_SCOPE, CellElement.FINALLY_SCOPE]: for relatedItem in self.findItemsForRef(item.leaderRef): if relatedItem not in selected: return True elif item.kind == CellElement.EXCEPT_MINIMIZED: # here: no except blocks on the diagram, they are collapsed tryItems = self.findItemsForRef(item.ref) for tryItem in tryItems: if tryItem.kind == CellElement.TRY_SCOPE: if tryItem.subKind == ScopeCellElement.TOP_LEFT: if not tryItem.isSelected(): return True break else: # The try is not selected return True return False def __extendSelectionForGrouping(self): """Extends the selection with the leading and side comments""" boundComments = [] selected = self.selectedItems() for item in selected: if not item.isComment() and not self.isOpenGroupItem(item): for relatedItem in self.findItemsForRef(item.ref): if relatedItem not in selected: boundComments.append(relatedItem) return selected + boundComments def __areLoneCommentsSelected(self, selected): """True if there are comments selected which have no main item selected""" for item in selected: if item.isComment(): if item.kind in [CellElement.SIDE_COMMENT, CellElement.LEADING_COMMENT, CellElement.ABOVE_COMMENT]: for relatedItem in self.findItemsForRef(item.ref): if relatedItem not in selected: return True return False def __getLineRange(self, selected): first = selected[0] last = selected[-1] if first.kind == CellElement.OPENED_GROUP_BEGIN: firstLine = first.groupBeginCMLRef.ref.parts[0].beginLine pos = first.groupBeginCMLRef.ref.parts[0].beginPos else: firstLine = first.getLineRange()[0] pos = first.ref.beginPos if last.scopedItem(): lastLine = last.ref.endLine elif last.kind == CellElement.OPENED_GROUP_BEGIN: lastLine = last.groupEndCMLRef.ref.parts[-1].endLine else: lastLine = last.getLineRange()[1] return firstLine, lastLine, pos def __getSelectedScopeRegions(self, selected): """Provides the regions of the selected scope items""" coveredRegions = [] for item in selected: if item.scopedItem(): if item.subKind in [ScopeCellElement.TOP_LEFT]: if item.ref.leadingComment: coveredRegions.append((item.ref.leadingComment.begin, item.ref.end)) else: coveredRegions.append((item.ref.begin, item.ref.end)) elif item.kind == CellElement.OPENED_GROUP_BEGIN: coveredRegions.append(item.getAbsPosRange()) return coveredRegions def __areIfFullySelected(self, selected, regions): """Checks if selected IFs are fully selected""" for item in selected: if item.kind == CellElement.IF: ifBegin = item.ref.begin ifEnd = item.ref.end for item in self.items(): if item.isProxyItem(): continue if item.scopedItem(): if item.subKind not in [ScopeCellElement.TOP_LEFT, ScopeCellElement.DOCSTRING, ScopeCellElement.SIDE_COMMENT]: continue if item in selected: continue itemRange = item.getAbsPosRange() if self.isInRegion(itemRange[0], itemRange[1], regions): continue if itemRange[0] > ifBegin and itemRange[0] < ifEnd: return False if itemRange[1] > ifBegin and itemRange[1] < ifEnd: return False regions.append([ifBegin, ifEnd]) return True @staticmethod def isInRegion(start, finish, regions): for region in regions: if start >= region[0] and finish <= region[1]: return True return False def __isSelectionContinuous(self, selected, regions, begin, end): """Checks if the selection is continuous""" for item in self.items(): if item.isProxyItem(): continue if item.scopedItem(): if item.subKind not in [ScopeCellElement.TOP_LEFT, ScopeCellElement.DOCSTRING, ScopeCellElement.SIDE_COMMENT]: continue if item in selected: continue itemRange = item.getAbsPosRange() if self.isInRegion(itemRange[0], itemRange[1], regions): continue # It is important to keep < and > instead of <= and >= # This is because the scopes start with the first statement if itemRange[0] > begin and itemRange[0] < end: return False if itemRange[1] > begin and itemRange[1] < end: return False return True def __moreThanOneIfBranchSelected(self, selected, regions): """Checks that the continuous selected items belong to more than one not selected IF statements """ ifRef = None for item in selected: if item.kind != CellElement.IF: itemRange = item.getAbsPosRange() if item.kind != CellElement.OPENED_GROUP_BEGIN: if self.isInRegion(itemRange[0], itemRange[1], regions): # Here: an item is in a selected scope item, in a selected # open group or in a fully selected if continue # Test if an item belongs to an if statement branch if item.kind in [CellElement.OPENED_GROUP_BEGIN, CellElement.EMPTY_GROUP, CellElement.COLLAPSED_GROUP]: branchId = item.groupBeginCMLRef.ref.getParentIfID() elif item.kind in [CellElement.INDEPENDENT_DOC, CellElement.LEADING_DOC, CellElement.ABOVE_DOC]: branchId = item.cmlRef.ref.getParentIfID() else: branchId = item.ref.getParentIfID() if branchId is not None: if ifRef is None: ifRef = branchId else: if branchId != ifRef: # Selected items belong to more than one branch return True return False
class CFSceneContextMenuMixin: """Encapsulates the context menu handling""" def __init__(self): self.menu = None self.individualMenus = {} # Scene menu preparation self.sceneMenu = QMenu() self.sceneMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...', self.parent().onSaveAsSVG) self.sceneMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...', self.parent().onSaveAsPDF) self.sceneMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...', self.parent().onSaveAsPNG) self.sceneMenu.addSeparator() self.sceneMenu.addAction(getIcon('copymenu.png'), 'Copy image to clipboard', self.parent().copyToClipboard) # Common menu for all the individually selected items self.commonMenu = QMenu() self.__ccAction = self.commonMenu.addAction( getIcon("customcolors.png"), "Custom colors...", self.onCustomColors) self.__rtAction = self.commonMenu.addAction( getIcon("replacetitle.png"), "Replace text...", self.onReplaceText) self.__groupAction = self.commonMenu.addAction(getIcon("cfgroup.png"), "Group...", self.onGroup) self.commonMenu.addSeparator() self.__removeCCAction = self.commonMenu.addAction( getIcon('trash.png'), 'Remove custom colors', self.onRemoveCustomColors) self.__removeRTAction = self.commonMenu.addAction( getIcon('trash.png'), 'Remove replacement text', self.onRemoveReplacementText) #self.commonMenu.addSeparator() #self.__cutAction = self.commonMenu.addAction( # getIcon("cutmenu.png"), "Cut (specific for graphics pane)", # self.onCut) #self.__copyAction = self.commonMenu.addAction( # getIcon("copymenu.png"), "Copy (specific for graphics pane)", # self.onCopy) #self.commonMenu.addSeparator() #self.commonMenu.addAction( # getIcon("trash.png"), "Delete", self.onDelete) # Individual items specific menu: begin ifContextMenu = QMenu() ifContextMenu.addAction(getIcon("switchbranches.png"), "Switch branch layout", self.onSwitchIfBranch) openGroupContextMenu = QMenu() openGroupContextMenu.addAction(getIcon("collapse.png"), "Collapse", self.onGroupCollapse) openGroupContextMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle) openGroupContextMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup) closeGroupContextMenu = QMenu() closeGroupContextMenu.addAction(getIcon("expand.png"), "Expand", self.onGroupExpand) closeGroupContextMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle) closeGroupContextMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup) emptyGroupContextMenu = QMenu() emptyGroupContextMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle) emptyGroupContextMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup) self.individualMenus[IfCell] = ifContextMenu self.individualMenus[OpenedGroupBegin] = openGroupContextMenu self.individualMenus[CollapsedGroup] = closeGroupContextMenu self.individualMenus[EmptyGroup] = emptyGroupContextMenu # Individual items specific menu: end # Menu for a group of selected items self.groupMenu = QMenu() def onContextMenu(self, event): """Triggered when a context menu should be shown""" selectedItems = self.selectedItems() selectionCount = len(selectedItems) if selectionCount == 0: self.sceneMenu.popup(event.screenPos()) return if selectionCount == 1: self.__buildIndividualMenu(selectedItems[0]) else: self.__buildGroupMenu(selectedItems) self.menu.popup(event.screenPos()) def __buildIndividualMenu(self, item): """Builds a context menu for the given item""" self.menu = QMenu() if type(item) in self.individualMenus: individualPart = self.individualMenus[type(item)] self.menu.addActions(individualPart.actions()) self.menu.addSeparator() self.menu.addActions(self.commonMenu.actions()) # Note: if certain items need to be disabled then it should be done # here self.__disableMenuItems() def __buildGroupMenu(self, items): """Builds a context menu for the group of items""" self.menu = QMenu() if type(items[0]) in self.individualMenus: if self.areSelectedOfTypes([[items[0].kind, items[0].subKind]]): individualPart = self.individualMenus[type(items[0])] self.menu.addActions(individualPart.actions()) self.menu.addSeparator() self.menu.addActions(self.commonMenu.actions()) if not self.groupMenu.isEmpty(): self.menu.addSeparator() self.menu.addActions(self.groupMenu.actions()) # Note: if certain items need to be disabled then it should be done # here self.__disableMenuItems() def __disableMenuItems(self): """Disables the common menu items as needed""" hasComment = self.isCommentInSelection() hasDocstring = self.isDocstringInSelection() hasMinimizedExcepts = self.isInSelected([(CellElement.EXCEPT_MINIMIZED, None)]) totalGroups = sum(self.countGroups()) count = len(self.selectedItems()) self.__ccAction.setEnabled(not hasComment and not hasMinimizedExcepts) self.__rtAction.setEnabled(not hasComment and not hasDocstring and not hasMinimizedExcepts and totalGroups == 0) totalCCGroups = sum(self.countGroupsWithCustomColors()) self.__removeCCAction.setEnabled( self.countItemsWithCML(CMLcc) + totalCCGroups == count) self.__removeRTAction.setEnabled( self.countItemsWithCML(CMLrt) == count) self.__groupAction.setEnabled(self.__canBeGrouped()) #self.__cutAction.setEnabled(count == 1) #self.__copyAction.setEnabled(count == 1) def __actionPrerequisites(self): """True if an editor related action can be done""" selectedItems = self.selectedItems() if not selectedItems: return False editor = selectedItems[0].getEditor() if editor is None: return False return True def onSwitchIfBranch(self): """If primitive should switch the branches""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.IF: cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLsw) if cmlComment is None: # Did not exist, so needs to be generated line = CMLsw.generate(item.ref.body.beginPos) lineNo = item.getFirstLine() editor.insertLines(line, lineNo) else: # Existed, so it just needs to be deleted cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onCustomColors(self): """Custom background and foreground colors""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() bgcolor, fgcolor, bordercolor = self.selectedItems()[0].getColors() hasDocstring = self.isDocstringInSelection() dlg = CustomColorsDialog(bgcolor, fgcolor, None if hasDocstring else bordercolor, self.parent()) if dlg.exec_(): bgcolor = dlg.backgroundColor() fgcolor = dlg.foregroundColor() bordercolor = dlg.borderColor() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind in [ CellElement.OPENED_GROUP_BEGIN, CellElement.COLLAPSED_GROUP, CellElement.EMPTY_GROUP ]: # The group always exists so just add/change the colors item.groupBeginCMLRef.updateCustomColors( editor, bgcolor, fgcolor, bordercolor) continue if item.isDocstring(): cmlComment = CMLVersion.find( item.ref.docstring.leadingCMLComments, CMLcc) else: cmlComment = CMLVersion.find( item.ref.leadingCMLComments, CMLcc) if cmlComment is not None: # Existed, so remove the old one first lineNo = cmlComment.ref.beginLine cmlComment.removeFromText(editor) else: lineNo = item.getFirstLine() pos = item.ref.body.beginPos if item.isDocstring(): pos = item.ref.docstring.beginPos line = CMLcc.generate(bgcolor, fgcolor, bordercolor, pos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onReplaceText(self): """Replace the code with a title""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() dlg = ReplaceTextDialog('Replace text', 'Item caption:', self.parent()) # If it was one item selection and there was a previous text then # set it for editing if len(self.selectedItems()) == 1: cmlComment = CMLVersion.find( self.selectedItems()[0].ref.leadingCMLComments, CMLrt) if cmlComment is not None: dlg.setText(cmlComment.getText()) if dlg.exec_(): replacementText = dlg.text() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLrt) if cmlComment is not None: # Existed, so remove the old one first lineNo = cmlComment.ref.beginLine cmlComment.removeFromText(editor) else: lineNo = item.getFirstLine() line = CMLrt.generate(replacementText, item.ref.body.beginPos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onGroupCollapse(self): """Collapses the selected group""" if not self.__actionPrerequisites(): return # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.OPENED_GROUP_BEGIN: fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() addCollapsedGroup(fileName, item.getGroupId()) QApplication.processEvents() self.parent().redrawNow() def onGroupExpand(self): """Expands the selected group""" if not self.__actionPrerequisites(): return # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.COLLAPSED_GROUP: fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() removeCollapsedGroup(fileName, item.getGroupId()) QApplication.processEvents() self.parent().redrawNow() def onGroupEditTitle(self): """Edit (or view) the group title""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent()) # If it was one item selection and there was a previous text then # set it for editing if len(self.selectedItems()) == 1: title = self.selectedItems()[0].getTitle() if title: dlg.setText(title) if dlg.exec_(): newTitle = dlg.text() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): item.groupBeginCMLRef.updateTitle(editor, newTitle) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onGroupUngroup(self): """Ungroups the items""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): item.groupEndCMLRef.removeFromText(editor) item.groupBeginCMLRef.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByTooltip(selection) def onDelete(self): """Delete the item""" print("Delete") def onGroup(self): """Groups items into a single one""" dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent()) if dlg.exec_(): groupTitle = dlg.text() selected = self.__extendSelectionForGrouping() selected = self.sortSelected(selected) editor = selected[0].getEditor() firstLine, lastLine, pos = self.__getLineRange(selected) groupid = self.parent().generateNewGroupId() beginComment = CMLgb.generate(groupid, groupTitle, None, None, None, pos) endComment = CMLge.generate(groupid, pos) with editor: editor.insertLines(endComment, lastLine + 1) editor.insertLines(beginComment, firstLine) # Redraw the group collapsed fileName = editor._parent.getFileName() if not fileName: fileName = editor._parent.getShortName() addCollapsedGroup(fileName, groupid) QApplication.processEvents() self.parent().redrawNow() def onCopy(self): """Copying...""" selectedItems = self.selectedItems() if selectedItems: if len(selectedItems) > 1: print('Copying multiple items has not been implemented yet') return selectedItems[0].copyToClipboard() def onCut(self): """Cutting...""" print("Cut") def onRemoveCustomColors(self): """Removing the previously set custom colors""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind in [ CellElement.OPENED_GROUP_BEGIN, CellElement.COLLAPSED_GROUP, CellElement.EMPTY_GROUP ]: item.groupBeginCMLRef.removeCustomColors(editor) continue if item.isDocstring(): cmlComment = CMLVersion.find( item.ref.docstring.leadingCMLComments, CMLcc) else: cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLcc) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onRemoveReplacementText(self): """Removing replacement text""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLrt) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def areSelectedOfTypes(self, matchList): """Checks if the selected items belong to the match""" # match is a list of pairs [kind, subKind] # None would mean 'match any' selectedItems = self.selectedItems() if selectedItems: for selectedItem in selectedItems: for kind, subKind in matchList: match = True if kind is not None: if kind != selectedItem.kind: match = False if subKind is not None: if subKind != selectedItem.subKind: match = False if match: break else: return False return True return False def isInSelected(self, matchList): """Checks if any if the match list items is in the selection""" # match is a list of pairs [kind, subKind] # None would mean 'match any' for selectedItem in self.selectedItems(): for kind, subKind in matchList: match = True if kind is not None: if kind != selectedItem.kind: match = False if subKind is not None: if subKind != selectedItem.subKind: match = False if match: return True return False def isDocstringInSelection(self): """True if a docstring item in the selection""" for item in self.selectedItems(): if item.isDocstring(): return True return False def isCommentInSelection(self): """True if a comment item in the selection""" for item in self.selectedItems(): if item.isComment(): return True return False def countItemsWithCML(self, cmlType): """Counts items with have a certain type of a CML comment""" count = 0 for item in self.selectedItems(): if item.isComment(): continue if item.isDocstring(): # Side comments for docstrings? Nonesense! So they are ignored # even if they are collected if CMLVersion.find(item.ref.docstring.leadingCMLComments, cmlType) is not None: count += 1 continue if hasattr(item.ref, 'leadingCMLComments'): if CMLVersion.find(item.ref.leadingCMLComments, cmlType) is not None: count += 1 continue if hasattr(item.ref, 'sideCMLComments'): if CMLVersion.find(item.ref.sideCMLComments, cmlType) is not None: count += 1 return count def countGroups(self): """Counts empty, close and open groups""" emptyCount = 0 closeCount = 0 openCount = 0 for item in self.selectedItems(): if item.kind == CellElement.EMPTY_GROUP: emptyCount += 1 elif item.kind == CellElement.COLLAPSED_GROUP: closeCount += 1 elif item.kind == CellElement.OPENED_GROUP_BEGIN: openCount += 1 return emptyCount, closeCount, openCount def countGroupsWithCustomColors(self): """Counts the groups with any color defined""" emptyCount = 0 closeCount = 0 openCount = 0 for item in self.selectedItems(): if item.kind in [ CellElement.EMPTY_GROUP, CellElement.COLLAPSED_GROUP, CellElement.OPENED_GROUP_BEGIN ]: if item.groupBeginCMLRef.bgColor is not None or \ item.groupBeginCMLRef.fgColor is not None or \ item.groupBeginCMLRef.border is not None: if item.kind == CellElement.EMPTY_GROUP: emptyCount += 1 elif item.kind == CellElement.COLLAPSED_GROUP: closeCount += 1 else: openCount += 1 return emptyCount, closeCount, openCount def sortSelectedReverse(self): """Sorts the selected items in reverse order""" result = [] for item in self.selectedItems(): itemBegin = item.getAbsPosRange()[0] for index in range(len(result)): if itemBegin > result[index].getAbsPosRange()[0]: result.insert(index, item) break else: result.append(item) return result def sortSelected(self, selected): """Sorts the selected items in direct order""" result = [] for item in selected: itemBegin = item.getAbsPosRange()[0] for index in range(len(result)): if itemBegin < result[index].getAbsPosRange()[0]: result.insert(index, item) break else: result.append(item) return result def __canBeGrouped(self): """True if the selected items can be grouped""" # Cannot import it at the top... from .flowuiwidget import SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT if Settings()['smartZoom'] not in [ SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT ]: return False if self.__areAllSelectedComments(): return False if self.__areScopeDocstringOrCommentSelected(): return False if self.__isModuleSelected(): return False # Extend the selection with all the selected items comments selected = self.__extendSelectionForGrouping() if self.__areLoneCommentsSelected(selected): return False if self.__areIncompleteScopeSelected(selected): return False scopeCoveredRegions = self.__getSelectedScopeRegions(selected) # The __areIfFullySelected() also updates the regions with # fully selected if regions if not self.__areIfFullySelected(selected, scopeCoveredRegions): return False selected = self.sortSelected(selected) begin = selected[0].getAbsPosRange()[0] end = selected[-1].getAbsPosRange()[1] if not self.__isSelectionContinuous(selected, scopeCoveredRegions, begin, end): return False if self.__moreThanOneIfBranchSelected(selected, scopeCoveredRegions): return False return True def __areAllSelectedComments(self): """True if all selected items are comments""" for item in self.selectedItems(): if not item.isComment(): return False return True def __areScopeDocstringOrCommentSelected(self): for item in self.selectedItems(): if item.scopedItem(): if item.subKind in [ ScopeCellElement.SIDE_COMMENT, ScopeCellElement.DOCSTRING ]: return True return False def __isModuleSelected(self): """True if the whole module is selected""" for item in self.selectedItems(): if item.kind == CellElement.FILE_SCOPE: return True return False def __areIncompleteScopeSelected(self, selected): """True if an incomplete scope selected""" for item in selected: if item.kind in [CellElement.FOR_SCOPE, CellElement.WHILE_SCOPE]: if item.ref.elsePart: for relatedItem in self.findItemsForRef(item.ref.elsePart): if relatedItem not in selected: return True elif item.kind in [CellElement.TRY_SCOPE]: # It could be that the exception blocks are hidden, so there # will be exactly one more item instead of many and that item # will have a ref which matches the try scope. exceptPartCount = 0 for exceptPart in item.ref.exceptParts: for relatedItem in self.findItemsForRef(exceptPart): exceptPartCount += 1 if relatedItem not in selected: return True if exceptPartCount == 0: # here: no except blocks on the diagram, they are collapsed tryItems = self.findItemsForRef(item.ref) for tryItem in tryItems: if tryItem.kind == CellElement.EXCEPT_MINIMIZED: if not tryItem.isSelected(): return True break else: # The minimized except is not selected return True if item.ref.elsePart: for relatedItem in self.findItemsForRef(item.ref.elsePart): if relatedItem not in selected: return True if item.ref.finallyPart: for relatedItem in self.findItemsForRef( item.ref.finallyPart): if relatedItem not in selected: return True elif item.kind in [ CellElement.ELSE_SCOPE, CellElement.EXCEPT_SCOPE, CellElement.FINALLY_SCOPE ]: for relatedItem in self.findItemsForRef(item.leaderRef): if relatedItem not in selected: return True elif item.kind == CellElement.EXCEPT_MINIMIZED: # here: no except blocks on the diagram, they are collapsed tryItems = self.findItemsForRef(item.ref) for tryItem in tryItems: if tryItem.kind == CellElement.TRY_SCOPE: if tryItem.subKind == ScopeCellElement.TOP_LEFT: if not tryItem.isSelected(): return True break else: # The try is not selected return True return False def __extendSelectionForGrouping(self): """Extends the selection with the leading and side comments""" boundComments = [] selected = self.selectedItems() for item in selected: if not item.isComment() and not self.isOpenGroupItem(item): for relatedItem in self.findItemsForRef(item.ref): if relatedItem not in selected: boundComments.append(relatedItem) return selected + boundComments def __areLoneCommentsSelected(self, selected): """True if there are comments selected which have no main item selected""" for item in selected: if item.isComment(): if item.kind in [ CellElement.SIDE_COMMENT, CellElement.LEADING_COMMENT, CellElement.ABOVE_COMMENT ]: for relatedItem in self.findItemsForRef(item.ref): if relatedItem not in selected: return True return False def __getLineRange(self, selected): first = selected[0] last = selected[-1] if first.kind == CellElement.OPENED_GROUP_BEGIN: firstLine = first.groupBeginCMLRef.ref.parts[0].beginLine pos = first.groupBeginCMLRef.ref.parts[0].beginPos else: firstLine = first.getLineRange()[0] pos = first.ref.beginPos if last.scopedItem(): lastLine = last.ref.endLine elif last.kind == CellElement.OPENED_GROUP_BEGIN: lastLine = last.groupEndCMLRef.ref.parts[-1].endLine else: lastLine = last.getLineRange()[1] return firstLine, lastLine, pos def __getSelectedScopeRegions(self, selected): """Provides the regions of the selected scope items""" coveredRegions = [] for item in selected: if item.scopedItem(): if item.subKind in [ScopeCellElement.TOP_LEFT]: if item.ref.leadingComment: coveredRegions.append( (item.ref.leadingComment.begin, item.ref.end)) else: coveredRegions.append((item.ref.begin, item.ref.end)) elif item.kind == CellElement.OPENED_GROUP_BEGIN: coveredRegions.append(item.getAbsPosRange()) return coveredRegions def __areIfFullySelected(self, selected, regions): """Checks if selected IFs are fully selected""" for item in selected: if item.kind == CellElement.IF: ifBegin = item.ref.begin ifEnd = item.ref.end for item in self.items(): if item.isProxyItem(): continue if item.scopedItem(): if item.subKind not in [ ScopeCellElement.TOP_LEFT, ScopeCellElement.DOCSTRING, ScopeCellElement.SIDE_COMMENT ]: continue if item in selected: continue itemRange = item.getAbsPosRange() if self.isInRegion(itemRange[0], itemRange[1], regions): continue if itemRange[0] > ifBegin and itemRange[0] < ifEnd: return False if itemRange[1] > ifBegin and itemRange[1] < ifEnd: return False regions.append([ifBegin, ifEnd]) return True @staticmethod def isInRegion(start, finish, regions): for region in regions: if start >= region[0] and finish <= region[1]: return True return False def __isSelectionContinuous(self, selected, regions, begin, end): """Checks if the selection is continuous""" for item in self.items(): if item.isProxyItem(): continue if item.scopedItem(): if item.subKind not in [ ScopeCellElement.TOP_LEFT, ScopeCellElement.DOCSTRING, ScopeCellElement.SIDE_COMMENT ]: continue if item in selected: continue itemRange = item.getAbsPosRange() if self.isInRegion(itemRange[0], itemRange[1], regions): continue # It is important to keep < and > instead of <= and >= # This is because the scopes start with the first statement if itemRange[0] > begin and itemRange[0] < end: return False if itemRange[1] > begin and itemRange[1] < end: return False return True def __moreThanOneIfBranchSelected(self, selected, regions): """Checks that the continuous selected items belong to more than one not selected IF statements """ ifRef = None for item in selected: if item.kind != CellElement.IF: itemRange = item.getAbsPosRange() if item.kind != CellElement.OPENED_GROUP_BEGIN: if self.isInRegion(itemRange[0], itemRange[1], regions): # Here: an item is in a selected scope item, in a selected # open group or in a fully selected if continue # Test if an item belongs to an if statement branch if item.kind in [ CellElement.OPENED_GROUP_BEGIN, CellElement.EMPTY_GROUP, CellElement.COLLAPSED_GROUP ]: branchId = item.groupBeginCMLRef.ref.getParentIfID() else: branchId = item.ref.getParentIfID() if branchId is not None: if ifRef is None: ifRef = branchId else: if branchId != ifRef: # Selected items belong to more than one branch return True return False
class ProfileTableViewer(QWidget): """Profiling results table viewer""" sigEscapePressed = pyqtSignal() def __init__(self, scriptName, params, reportTime, dataFile, stats, parent=None): QWidget.__init__(self, parent) self.__table = ProfilerTreeWidget(self) self.__table.sigEscapePressed.connect(self.__onEsc) self.__script = scriptName self.__stats = stats project = GlobalData().project if project.isLoaded(): self.__projectPrefix = os.path.dirname(project.fileName) else: self.__projectPrefix = os.path.dirname(scriptName) if not self.__projectPrefix.endswith(os.path.sep): self.__projectPrefix += os.path.sep self.__table.setAlternatingRowColors(True) self.__table.setRootIsDecorated(False) self.__table.setItemsExpandable(False) self.__table.setSortingEnabled(True) self.__table.setItemDelegate(NoOutlineHeightDelegate(4)) self.__table.setUniformRowHeights(True) self.__table.setSelectionMode(QAbstractItemView.SingleSelection) self.__table.setSelectionBehavior(QAbstractItemView.SelectRows) headerLabels = [ "", "Calls", "Total time", "Per call", "Cum. time", "Per call", "File name:line", "Function", "Callers", "Callees" ] self.__table.setHeaderLabels(headerLabels) headerItem = self.__table.headerItem() headerItem.setToolTip(0, "Indication if it is an outside function") headerItem.setToolTip( 1, "Actual number of calls/primitive calls " "(not induced via recursion)") headerItem.setToolTip( 2, "Total time spent in function " "(excluding time made in calls " "to sub-functions)") headerItem.setToolTip( 3, "Total time divided by number " "of actual calls") headerItem.setToolTip( 4, "Total time spent in function and all " "subfunctions (from invocation till exit)") headerItem.setToolTip( 5, "Cumulative time divided by number " "of primitive calls") headerItem.setToolTip(6, "Function location") headerItem.setToolTip(7, "Function name") headerItem.setToolTip(8, "Function callers") headerItem.setToolTip(9, "Function callees") self.__table.itemActivated.connect(self.__activated) totalCalls = self.__stats.total_calls # The calls were not induced via recursion totalPrimitiveCalls = self.__stats.prim_calls totalTime = self.__stats.total_tt txt = "<b>Script:</b> " + self.__script + " " + \ params['arguments'] + "<br/>" \ "<b>Run at:</b> " + reportTime + "<br/>" + \ str(totalCalls) + " function calls (" + \ str(totalPrimitiveCalls) + " primitive calls) in " + \ FLOAT_FORMAT % totalTime + " CPU seconds" summary = QLabel(txt, self) summary.setToolTip(txt) summary.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) summary.setStyleSheet('QLabel {' + getLabelStyle(summary) + '}') vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) vLayout.addWidget(summary) vLayout.addWidget(self.__table) self.setLayout(vLayout) self.__createContextMenu() self.__populate(totalTime) def __onEsc(self): """Triggered when Esc is pressed""" self.sigEscapePressed.emit() def __createContextMenu(self): """Creates context menu for the table raws""" self.__contextMenu = QMenu(self) self.__callersMenu = QMenu("Callers", self) self.__outsideCallersMenu = QMenu("Outside callers", self) self.__calleesMenu = QMenu("Callees", self) self.__outsideCalleesMenu = QMenu("Outside callees", self) self.__contextMenu.addMenu(self.__callersMenu) self.__contextMenu.addMenu(self.__outsideCallersMenu) self.__contextMenu.addSeparator() self.__contextMenu.addMenu(self.__calleesMenu) self.__contextMenu.addMenu(self.__outsideCalleesMenu) self.__callersMenu.triggered.connect(self.__onCallContextMenu) self.__outsideCallersMenu.triggered.connect(self.__onCallContextMenu) self.__calleesMenu.triggered.connect(self.__onCallContextMenu) self.__outsideCalleesMenu.triggered.connect(self.__onCallContextMenu) self.__table.setContextMenuPolicy(Qt.CustomContextMenu) self.__table.customContextMenuRequested.connect(self.__showContextMenu) def __showContextMenu(self, point): """Context menu""" self.__callersMenu.clear() self.__outsideCallersMenu.clear() self.__calleesMenu.clear() self.__outsideCalleesMenu.clear() # Detect what the item was clicked item = self.__table.itemAt(point) funcName = item.getFunctionName() # Build the context menu if item.callersCount() == 0: self.__callersMenu.setEnabled(False) self.__outsideCallersMenu.setEnabled(False) else: callers = self.__stats.stats[item.getFuncIDs()][4] callersList = list(callers.keys()) callersList.sort() for callerFunc in callersList: menuText = self.__getCallLine(callerFunc, callers[callerFunc]) if self.__isOutsideItem(callerFunc[0]): act = self.__outsideCallersMenu.addAction(menuText) else: act = self.__callersMenu.addAction(menuText) funcFileName, funcLine, funcName = \ self.__getLocationAndName(callerFunc) act.setData(funcFileName + ":" + str(funcLine) + ":" + funcName) self.__callersMenu.setEnabled(not self.__callersMenu.isEmpty()) self.__outsideCallersMenu.setEnabled( not self.__outsideCallersMenu.isEmpty()) if item.calleesCount() == 0: self.__calleesMenu.setEnabled(False) self.__outsideCalleesMenu.setEnabled(False) else: callees = self.__stats.all_callees[item.getFuncIDs()] calleesList = list(callees.keys()) calleesList.sort() for calleeFunc in calleesList: menuText = self.__getCallLine(calleeFunc, callees[calleeFunc]) if self.__isOutsideItem(calleeFunc[0]): act = self.__outsideCalleesMenu.addAction(menuText) else: act = self.__calleesMenu.addAction(menuText) funcFileName, funcLine, funcName = \ self.__getLocationAndName(calleeFunc) act.setData(funcFileName + ":" + str(funcLine) + ":" + funcName) self.__calleesMenu.setEnabled(not self.__calleesMenu.isEmpty()) self.__outsideCalleesMenu.setEnabled( not self.__outsideCalleesMenu.isEmpty()) self.__contextMenu.popup(QCursor.pos()) def __resize(self): """Resizes columns to the content""" self.__table.header().resizeSections(QHeaderView.ResizeToContents) self.__table.header().setStretchLastSection(True) self.__table.header().resizeSection(OUTSIDE_COL_INDEX, 28) self.__table.header().setSectionResizeMode(OUTSIDE_COL_INDEX, QHeaderView.Fixed) def setFocus(self): """Set focus to the proper widget""" self.__table.setFocus() def __isOutsideItem(self, fileName): """Detects if the record should be shown as an outside one""" return not fileName.startswith(self.__projectPrefix) def __activated(self, item, column): """Triggered when the item is activated""" try: line = item.getLineNumber() fileName = item.getFileName() if line == 0 or not os.path.isabs(fileName): return GlobalData().mainWindow.openFile(fileName, line) except: logging.error("Could not jump to function location") @staticmethod def __getFuncShortLocation(funcFileName, funcLine): """Provides unified shortened function location""" if funcFileName == "": return "" return os.path.basename(funcFileName) + ":" + str(funcLine) def __getCallLine(self, func, props): """Provides the formatted call line""" funcFileName, funcLine, funcName = self.__getLocationAndName(func) if isinstance(props, tuple): actualCalls, primitiveCalls, totalTime, cumulativeTime = props if primitiveCalls != actualCalls: callsString = str(actualCalls) + "/" + str(primitiveCalls) else: callsString = str(actualCalls) return callsString + "\t" + FLOAT_FORMAT % totalTime + "\t" + \ FLOAT_FORMAT % cumulativeTime + "\t" + \ self.__getFuncShortLocation(funcFileName, funcLine) + \ "(" + funcName + ")" # I've never seen this branch working so it is just in case. # There was something like this in the pstats module return self.__getFuncShortLocation(funcFileName, funcLine) + \ "(" + funcName + ")" @staticmethod def __getLocationAndName(func): """Provides standardized function file name, line and its name""" if func[:2] == ('~', 0): # special case for built-in functions name = func[2] if name.startswith('<') and name.endswith('>'): return "", 0, "{%s}" % name[1:-1] return "", 0, name return func[0], func[1], func[2] def __createItem(self, func, totalCPUTime, primitiveCalls, actualCalls, totalTime, cumulativeTime, timePerCall, cumulativeTimePerCall, callers): """Creates an item to display""" values = [] values.append("") if primitiveCalls == actualCalls: values.append(str(actualCalls)) else: values.append(str(actualCalls) + "/" + str(primitiveCalls)) if totalCPUTime == 0.0: values.append(FLOAT_FORMAT % totalTime) else: values.append(FLOAT_FORMAT % totalTime + " \t(%3.2f%%)" % (totalTime / totalCPUTime * 100)) values.append(FLOAT_FORMAT % timePerCall) values.append(FLOAT_FORMAT % cumulativeTime) values.append(FLOAT_FORMAT % cumulativeTimePerCall) # Location and name will be filled in the item constructor values.append("") values.append("") # Callers callersCount = len(callers) values.append(str(callersCount)) # Callees if func in self.__stats.all_callees: calleesCount = len(self.__stats.all_callees[func]) else: calleesCount = 0 values.append(str(calleesCount)) item = ProfilingTableItem(values, self.__isOutsideItem(func[0]), func) if callersCount != 0: tooltip = "" callersList = list(callers.keys()) callersList.sort() for callerFunc in callersList[:MAX_CALLS_IN_TOOLTIP]: if tooltip != "": tooltip += "\n" tooltip += self.__getCallLine(callerFunc, callers[callerFunc]) if callersCount > MAX_CALLS_IN_TOOLTIP: tooltip += "\n. . ." item.setToolTip(8, tooltip) if calleesCount != 0: callees = self.__stats.all_callees[func] tooltip = "" calleesList = list(callees.keys()) calleesList.sort() for calleeFunc in calleesList[:MAX_CALLS_IN_TOOLTIP]: if tooltip != "": tooltip += "\n" tooltip += self.__getCallLine(calleeFunc, callees[calleeFunc]) if calleesCount > MAX_CALLS_IN_TOOLTIP: tooltip += "\n. . ." item.setToolTip(9, tooltip) self.__table.addTopLevelItem(item) def __populate(self, totalCPUTime): """Populates the data""" for func, (primitiveCalls, actualCalls, totalTime, cumulativeTime, callers) in self.__stats.stats.items(): # Calc time per call if actualCalls == 0: timePerCall = 0.0 else: timePerCall = totalTime / actualCalls # Calc time per cummulative call if primitiveCalls == 0: cumulativeTimePerCall = 0.0 else: cumulativeTimePerCall = cumulativeTime / primitiveCalls self.__createItem(func, totalCPUTime, primitiveCalls, actualCalls, totalTime, cumulativeTime, timePerCall, cumulativeTimePerCall, callers) self.__resize() self.__table.header().setSortIndicator(2, Qt.DescendingOrder) self.__table.sortItems(2, self.__table.header().sortIndicatorOrder()) def togglePath(self, state): """Switches between showing full paths or file names in locations""" for index in range(0, self.__table.topLevelItemCount()): self.__table.topLevelItem(index).updateLocation(state) self.__resize() return def __onCallContextMenu(self, act): """Triggered when a context menu action is requested""" name = str(act.data().toString()) for index in range(0, self.__table.topLevelItemCount()): item = self.__table.topLevelItem(index) if item.match(name): self.__table.clearSelection() self.__table.scrollToItem(item) self.__table.setCurrentItem(item) def onSaveAs(self, fileName): """Saves the table to a file in CSV format""" try: f = open(fileName, "wt") headerItem = self.__table.headerItem() outsideIndex = -1 for index in range(0, headerItem.columnCount()): title = str(headerItem.text(index)) if title == "": outsideIndex = index title = "Outside" if index != 0: f.write(',' + title) else: f.write(title) for index in range(0, self.__table.topLevelItemCount()): item = self.__table.topLevelItem(index) f.write("\n") for column in range(0, item.columnCount()): if column != 0: f.write(',') if column == outsideIndex: if item.isOutside(): f.write("Y") else: f.write("N") else: text = str(item.text(column)) f.write(text.replace('\t', '')) f.close() except Exception as ex: logging.error(ex)
class StackViewer(QWidget): """Implements the stack viewer for a debugger""" def __init__(self, debugger, parent=None): QWidget.__init__(self, parent) self.__debugger = debugger self.currentStack = None self.currentFrame = 0 self.__contextItem = None self.__createPopupMenu() self.__createLayout() def __createPopupMenu(self): """Creates the popup menu""" self.__framesMenu = QMenu() self.__setCurrentMenuItem = self.__framesMenu.addAction( "Set current (single click)", self.__onSetCurrent) self.__jumpMenuItem = self.__framesMenu.addAction( "Set current and jump to the source (double click)", self.__onSetCurrentAndJump) def __createLayout(self): """Creates the widget layout""" verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(0, 0, 0, 0) verticalLayout.setSpacing(0) self.__stackLabel = HeaderFitLabel(self) self.__stackLabel.setText('Stack') self.__stackLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.__stackLabel.setMinimumWidth(10) self.__showHideButton = QToolButton() self.__showHideButton.setAutoRaise(True) self.__showHideButton.setIcon(getIcon('less.png')) self.__showHideButton.setFixedSize(self.__stackLabel.height(), self.__stackLabel.height()) self.__showHideButton.setToolTip('Hide frames list') self.__showHideButton.setFocusPolicy(Qt.NoFocus) self.__showHideButton.clicked.connect(self.__onShowHide) self.headerToolbar = QToolBar(self) self.headerToolbar.setIconSize(QSize(16, 16)) self.headerToolbar.setContentsMargins(1, 1, 1, 1) self.headerToolbar.addWidget(self.__stackLabel) self.headerToolbar.addWidget(self.__showHideButton) self.__framesList = QTreeWidget(self) self.__framesList.setSortingEnabled(False) # I might not need that because of two reasons: # - the window has no focus # - the window has custom current indicator # self.__framesList.setAlternatingRowColors(True) self.__framesList.setRootIsDecorated(False) self.__framesList.setItemsExpandable(False) self.__framesList.setUniformRowHeights(True) self.__framesList.setSelectionMode(QAbstractItemView.NoSelection) self.__framesList.setSelectionBehavior(QAbstractItemView.SelectRows) self.__framesList.setItemDelegate(NoOutlineHeightDelegate(4)) self.__framesList.setFocusPolicy(Qt.NoFocus) self.__framesList.setContextMenuPolicy(Qt.CustomContextMenu) self.__framesList.itemClicked.connect(self.__onFrameClicked) self.__framesList.itemDoubleClicked.connect( self.__onFrameDoubleClicked) self.__framesList.customContextMenuRequested.connect( self.__showContextMenu) self.__framesList.setHeaderLabels( ['', 'File:line', 'Function', 'Arguments', 'Full path']) verticalLayout.addWidget(self.headerToolbar) verticalLayout.addWidget(self.__framesList) def __onShowHide(self, startup=False): """Triggered when show/hide button is clicked""" if startup or self.__framesList.isVisible(): self.__minH = self.minimumHeight() self.__maxH = self.maximumHeight() self.splitterSize = self.parent().sizes()[1] self.__framesList.setVisible(False) self.__showHideButton.setIcon(getIcon('more.png')) self.__showHideButton.setToolTip('Show frames list') self.setMinimumHeight(self.headerToolbar.height()) self.setMaximumHeight(self.headerToolbar.height()) else: self.__framesList.setVisible(True) self.__showHideButton.setIcon(getIcon('less.png')) self.__showHideButton.setToolTip('Hide frames list') self.setMinimumHeight(self.__minH) self.setMaximumHeight(self.__maxH) splitterSizes = self.parent().sizes() splitterSizes[1] = self.splitterSize self.parent().setSizes(splitterSizes) def clear(self): """Clears the content""" self.__framesList.clear() self.currentStack = None self.__stackLabel.setText("Stack") def __resizeColumns(self): """Resize the files list columns""" self.__framesList.header().setStretchLastSection(True) self.__framesList.header().resizeSections(QHeaderView.ResizeToContents) self.__framesList.header().resizeSection(0, 22) self.__framesList.header().setSectionResizeMode(0, QHeaderView.Fixed) def populate(self, stack): """Sets the new call stack and selects the first item in it""" self.clear() self.currentStack = stack self.currentFrame = 0 frameNumber = 0 for item in stack: fName = item[0] lineNo = item[1] funcName = '' funcArgs = '' if len(item) >= 3: funcName = item[2] if len(item) >= 4: funcArgs = item[3] if funcName.startswith('<'): funcName = '' funcArgs = '' item = StackFrameItem(fName, lineNo, funcName, funcArgs, frameNumber) self.__framesList.addTopLevelItem(item) frameNumber += 1 self.__resizeColumns() self.__framesList.topLevelItem(0).setCurrent(True) self.__stackLabel.setText("Stack (total: " + str(len(stack)) + ")") def getFrameNumber(self): """Provides the current frame number""" return self.currentFrame def __onFrameClicked(self, item, column): """Triggered when a frame is clicked""" del column # unused argument if item.isCurrent(): return # Hide the current indicator self.__framesList.topLevelItem(self.currentFrame).setCurrent(False) # Show the new indicator self.currentFrame = item.getFrameNumber() for index in range(self.__framesList.topLevelItemCount()): item = self.__framesList.topLevelItem(index) if item.getFrameNumber() == self.currentFrame: item.setCurrent(True) self.__debugger.remoteClientVariables(1, self.currentFrame) # globals self.__debugger.remoteClientVariables(0, self.currentFrame) # locals def __onFrameDoubleClicked(self, item, column): """Triggered when a frame is double clicked""" del column # unused argument # The frame has been switched already because the double click # signal always comes after the single click one fileName = item.getFilename() lineNumber = item.getLineNumber() editorsManager = GlobalData().mainWindow.editorsManager() editorsManager.openFile(fileName, lineNumber) editor = editorsManager.currentWidget().getEditor() editor.gotoLine(lineNumber) editorsManager.currentWidget().setFocus() def __showContextMenu(self, coord): """Shows the frames list context menu""" self.__contextItem = self.__framesList.itemAt(coord) if self.__contextItem is not None: self.__setCurrentMenuItem.setEnabled( not self.__contextItem.isCurrent()) self.__framesMenu.popup(QCursor.pos()) def __onSetCurrent(self): """Context menu item handler""" self.__onFrameClicked(self.__contextItem, 0) def __onSetCurrentAndJump(self): """Context menu item handler""" self.__onFrameClicked(self.__contextItem, 0) self.__onFrameDoubleClicked(self.__contextItem, 0) def switchControl(self, isInIDE): """Switches the UI depending where the control flow is""" self.__framesList.setEnabled(isInIDE)
class IgnoredExceptionsViewer(QWidget): """Implements the client exceptions viewer for a debugger""" def __init__(self, parent=None): QWidget.__init__(self, parent) self.__createPopupMenu() self.__createLayout() self.__ignored = [] self.__currentItem = None GlobalData().project.sigProjectChanged.connect(self.__onProjectChanged) if not Settings()['showIgnoredExcViewer']: self.__onShowHide(True) def __createPopupMenu(self): """Creates the popup menu""" self.__excptMenu = QMenu() self.__removeMenuItem = self.__excptMenu.addAction( getIcon('ignexcptdel.png'), "Remove from ignore list", self.__onRemoveFromIgnore) def __createLayout(self): """Creates the widget layout""" verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(0, 0, 0, 0) verticalLayout.setSpacing(0) self.__excptLabel = QLabel("Ignored exception types", self) self.headerFrame = QFrame() self.headerFrame.setObjectName('ignexcpt') self.headerFrame.setStyleSheet('QFrame#ignexcpt {' + getLabelStyle(self.__excptLabel) + '}') self.headerFrame.setFixedHeight(HEADER_HEIGHT) expandingSpacer = QSpacerItem(10, 10, QSizePolicy.Expanding) self.__showHideButton = QToolButton() self.__showHideButton.setAutoRaise(True) self.__showHideButton.setIcon(getIcon('less.png')) self.__showHideButton.setFixedSize(HEADER_BUTTON, HEADER_BUTTON) self.__showHideButton.setToolTip("Hide ignored exceptions list") self.__showHideButton.setFocusPolicy(Qt.NoFocus) self.__showHideButton.clicked.connect(self.__onShowHide) headerLayout = QHBoxLayout() headerLayout.setContentsMargins(0, 0, 0, 0) headerLayout.addSpacing(3) headerLayout.addWidget(self.__excptLabel) headerLayout.addSpacerItem(expandingSpacer) headerLayout.addWidget(self.__showHideButton) self.headerFrame.setLayout(headerLayout) self.exceptionsList = QTreeWidget(self) self.exceptionsList.setSortingEnabled(False) self.exceptionsList.setAlternatingRowColors(True) self.exceptionsList.setRootIsDecorated(False) self.exceptionsList.setItemsExpandable(True) self.exceptionsList.setUniformRowHeights(True) self.exceptionsList.setSelectionMode(QAbstractItemView.SingleSelection) self.exceptionsList.setSelectionBehavior(QAbstractItemView.SelectRows) self.exceptionsList.setItemDelegate(NoOutlineHeightDelegate(4)) self.exceptionsList.setContextMenuPolicy(Qt.CustomContextMenu) self.exceptionsList.customContextMenuRequested.connect( self.__showContextMenu) self.exceptionsList.itemSelectionChanged.connect( self.__onSelectionChanged) self.exceptionsList.setHeaderLabels(["Exception type"]) self.__excTypeEdit = QLineEdit() self.__excTypeEdit.setFixedHeight(26) self.__excTypeEdit.textChanged.connect(self.__onNewFilterChanged) self.__excTypeEdit.returnPressed.connect(self.__onAddExceptionFilter) self.__addButton = QPushButton("Add") # self.__addButton.setFocusPolicy(Qt.NoFocus) self.__addButton.setEnabled(False) self.__addButton.clicked.connect(self.__onAddExceptionFilter) expandingSpacer2 = QWidget() expandingSpacer2.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.__removeButton = QAction(getIcon('delitem.png'), "Remove selected exception type", self) self.__removeButton.triggered.connect(self.__onRemoveFromIgnore) self.__removeButton.setEnabled(False) fixedSpacer1 = QWidget() fixedSpacer1.setFixedWidth(5) self.__removeAllButton = QAction(getIcon('ignexcptdelall.png'), "Remove all the exception types", self) self.__removeAllButton.triggered.connect(self.__onRemoveAllFromIgnore) self.__removeAllButton.setEnabled(False) self.toolbar = QToolBar() self.toolbar.setOrientation(Qt.Horizontal) self.toolbar.setMovable(False) self.toolbar.setAllowedAreas(Qt.TopToolBarArea) self.toolbar.setIconSize(QSize(16, 16)) self.toolbar.setFixedHeight(28) self.toolbar.setContentsMargins(0, 0, 0, 0) self.toolbar.addWidget(expandingSpacer2) self.toolbar.addAction(self.__removeButton) self.toolbar.addWidget(fixedSpacer1) self.toolbar.addAction(self.__removeAllButton) addLayout = QHBoxLayout() addLayout.setContentsMargins(1, 1, 1, 1) addLayout.setSpacing(1) addLayout.addWidget(self.__excTypeEdit) addLayout.addWidget(self.__addButton) verticalLayout.addWidget(self.headerFrame) verticalLayout.addWidget(self.toolbar) verticalLayout.addWidget(self.exceptionsList) verticalLayout.addLayout(addLayout) def clear(self): """Clears the content""" self.exceptionsList.clear() self.__excTypeEdit.clear() self.__addButton.setEnabled(False) self.__ignored = [] self.__currentItem = None self.__updateTitle() def __onShowHide(self, startup=False): """Triggered when show/hide button is clicked""" if startup or self.exceptionsList.isVisible(): self.exceptionsList.setVisible(False) self.__excTypeEdit.setVisible(False) self.__addButton.setVisible(False) self.__removeButton.setVisible(False) self.__removeAllButton.setVisible(False) self.__showHideButton.setIcon(getIcon('more.png')) self.__showHideButton.setToolTip("Show ignored exceptions list") self.__minH = self.minimumHeight() self.__maxH = self.maximumHeight() self.setMinimumHeight(self.headerFrame.height()) self.setMaximumHeight(self.headerFrame.height()) Settings()['showIgnoredExcViewer'] = False else: self.exceptionsList.setVisible(True) self.__excTypeEdit.setVisible(True) self.__addButton.setVisible(True) self.__removeButton.setVisible(True) self.__removeAllButton.setVisible(True) self.__showHideButton.setIcon(getIcon('less.png')) self.__showHideButton.setToolTip("Hide ignored exceptions list") self.setMinimumHeight(self.__minH) self.setMaximumHeight(self.__maxH) Settings()['showIgnoredExcViewer'] = True def __onSelectionChanged(self): """Triggered when the current item is changed""" selected = list(self.exceptionsList.selectedItems()) if selected: self.__currentItem = selected[0] self.__removeButton.setEnabled(True) else: self.__currentItem = None self.__removeButton.setEnabled(False) def __showContextMenu(self, coord): """Shows the frames list context menu""" contextItem = self.exceptionsList.itemAt(coord) if contextItem is not None: self.__currentItem = contextItem self.__excptMenu.popup(QCursor.pos()) def __updateTitle(self): """Updates the section title""" count = self.exceptionsList.topLevelItemCount() if count == 0: self.__excptLabel.setText("Ignored exception types") else: self.__excptLabel.setText("Ignored exception types (total: " + str(count) + ")") self.__removeAllButton.setEnabled(count != 0) def __onProjectChanged(self, what): """Triggered when a project is changed""" if what != CodimensionProject.CompleteProject: return self.clear() project = GlobalData().project if project.isLoaded(): self.__ignored = list(project.exceptionFilters) else: self.__ignored = Settings()['ignoredExceptions'] for exceptionType in self.__ignored: item = QTreeWidgetItem(self.exceptionsList) item.setText(0, exceptionType) self.__updateTitle() def __onNewFilterChanged(self, text): """Triggered when the text is changed""" text = str(text).strip() if text == "": self.__addButton.setEnabled(False) return if " " in text: self.__addButton.setEnabled(False) return if text in self.__ignored: self.__addButton.setEnabled(False) return self.__addButton.setEnabled(True) def __onAddExceptionFilter(self): """Adds an item into the ignored exceptions list""" text = self.__excTypeEdit.text().strip() self.addExceptionFilter(text) def addExceptionFilter(self, excType): """Adds a new item into the ignored exceptions list""" if excType == "": return if " " in excType: return if excType in self.__ignored: return item = QTreeWidgetItem(self.exceptionsList) item.setText(0, excType) project = GlobalData().project if project.isLoaded(): project.addExceptionFilter(excType) else: Settings().addExceptionFilter(excType) self.__ignored.append(excType) self.__updateTitle() def __onRemoveFromIgnore(self): """Removes an item from the ignored exception types list""" if self.__currentItem is None: return text = self.__currentItem.text(0) # Find the item index and remove it index = 0 while True: if self.exceptionsList.topLevelItem(index).text(0) == text: self.exceptionsList.takeTopLevelItem(index) break index += 1 project = GlobalData().project if project.isLoaded(): project.deleteExceptionFilter(text) else: Settings().deleteExceptionFilter(text) self.__ignored.remove(text) self.__updateTitle() def __onRemoveAllFromIgnore(self): """Triggered when all the ignored exceptions should be deleted""" self.clear() project = GlobalData().project if project.isLoaded(): project.setExceptionFilters([]) else: Settings().setExceptionFilters([]) def isIgnored(self, exceptionType): """Returns True if this exception type should be ignored""" return exceptionType in self.__ignored
class CFSceneContextMenuMixin: """Encapsulates the context menu handling""" def __init__(self): self.menu = None self.individualMenus = {} # Scene menu preparation self.sceneMenu = QMenu() self.sceneMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...', self.parent().onSaveAsSVG) self.sceneMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...', self.parent().onSaveAsPDF) self.sceneMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...', self.parent().onSaveAsPNG) self.sceneMenu.addSeparator() self.sceneMenu.addAction(getIcon('copymenu.png'), 'Copy to clipboard', self.parent().copyToClipboard) # Common menu for all the individually selected items self.commonMenu = QMenu() self.__ccAction = self.commonMenu.addAction( getIcon("customcolors.png"), "Custom colors...", self.onCustomColors) self.__rtAction = self.commonMenu.addAction( getIcon("replacetitle.png"), "Replace text...", self.onReplaceText) self.commonMenu.addSeparator() self.__removeCCAction = self.commonMenu.addAction( getIcon('trash.png'), 'Remove custom colors', self.onRemoveCustomColors) self.__removeRTAction = self.commonMenu.addAction( getIcon('trash.png'), 'Remove replacement text', self.onRemoveReplacementText) self.commonMenu.addSeparator() self.__cutAction = self.commonMenu.addAction(getIcon("cutmenu.png"), "Cut", self.onCut) self.__copyAction = self.commonMenu.addAction(getIcon("copymenu.png"), "Copy", self.onCopy) self.commonMenu.addSeparator() self.commonMenu.addAction(getIcon("trash.png"), "Delete", self.onDelete) # Individual items specific menu: begin ifContextMenu = QMenu() ifContextMenu.addAction(getIcon("switchbranches.png"), "Switch branch layout", self.onSwitchIfBranch) self.individualMenus[IfCell] = ifContextMenu # Individual items specific menu: end # Menu for a group of selected items self.groupMenu = QMenu() self.__groupAction = self.groupMenu.addAction(getIcon("cfgroup.png"), "Group...", self.onGroup) def onContextMenu(self, event): """Triggered when a context menu should be shown""" selectedItems = self.selectedItems() selectionCount = len(selectedItems) if selectionCount == 0: self.sceneMenu.popup(event.screenPos()) return if selectionCount == 1: self.__buildIndividualMenu(selectedItems[0]) else: self.__buildGroupMenu(selectedItems) self.menu.popup(event.screenPos()) def __buildIndividualMenu(self, item): """Builds a context menu for the given item""" self.menu = QMenu() if type(item) in self.individualMenus: individualPart = self.individualMenus[type(item)] self.menu.addActions(individualPart.actions()) self.menu.addSeparator() self.menu.addActions(self.commonMenu.actions()) # Note: if certain items need to be disabled then it should be done # here self.__disableMenuItems() def __buildGroupMenu(self, items): """Builds a context menu for the group of items""" self.menu = QMenu() if type(items[0]) in self.individualMenus: if self.areSelectedOfTypes([[items[0].kind, items[0].subKind]]): individualPart = self.individualMenus[type(items[0])] self.menu.addActions(individualPart.actions()) self.menu.addSeparator() self.menu.addActions(self.commonMenu.actions()) self.menu.addSeparator() self.menu.addActions(self.groupMenu.actions()) # Note: if certain items need to be disabled then it should be done # here self.__groupAction.setEnabled(False) self.__disableMenuItems() def __disableMenuItems(self): """Disables the common menu items as needed""" hasComment = self.isCommentInSelection() hasDocstring = self.isDocstringInSelection() hasMinimizedExcepts = self.isInSelected([(CellElement.EXCEPT_MINIMIZED, None)]) count = len(self.selectedItems()) self.__ccAction.setEnabled(not hasComment and not hasMinimizedExcepts) self.__rtAction.setEnabled(not hasComment and not hasDocstring and not hasMinimizedExcepts) self.__removeCCAction.setEnabled( self.countItemsWithCML(CMLcc) == count) self.__removeRTAction.setEnabled( self.countItemsWithCML(CMLrt) == count) self.__cutAction.setEnabled(count == 1) self.__copyAction.setEnabled(count == 1) def __actionPrerequisites(self): """True if an editor related action can be done""" selectedItems = self.selectedItems() if not selectedItems: return False editor = selectedItems[0].getEditor() if editor is None: return False return True def onSwitchIfBranch(self): """If primitive should switch the branches""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() # The selected items need to be sorted in the reverse line no oreder editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.kind == CellElement.IF: cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLsw) if cmlComment is None: # Did not exist, so needs to be generated line = CMLsw.generate(item.ref.body.beginPos) lineNo = item.getFirstLine() editor.insertLines(line, lineNo) else: # Existed, so it just needs to be deleted cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onCustomColors(self): """Custom background and foreground colors""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() bgcolor, fgcolor, bordercolor = self.selectedItems()[0].getColors() hasDocstring = self.isDocstringInSelection() dlg = CustomColorsDialog(bgcolor, fgcolor, None if hasDocstring else bordercolor, self.parent()) if dlg.exec_(): bgcolor = dlg.backgroundColor() fgcolor = dlg.foregroundColor() bordercolor = dlg.borderColor() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.isDocstring(): cmlComment = CMLVersion.find( item.ref.docstring.leadingCMLComments, CMLcc) else: cmlComment = CMLVersion.find( item.ref.leadingCMLComments, CMLcc) if cmlComment is not None: # Existed, so remove the old one first lineNo = cmlComment.ref.beginLine cmlComment.removeFromText(editor) else: lineNo = item.getFirstLine() pos = item.ref.body.beginPos if item.isDocstring(): pos = item.ref.docstring.beginPos line = CMLcc.generate(bgcolor, fgcolor, bordercolor, pos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onReplaceText(self): """Replace the code with a title""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() dlg = ReplaceTextDialog(self.parent()) # If it was one item selection and there was a previous text then # set it for editing if len(self.selectedItems()) == 1: cmlComment = CMLVersion.find( self.selectedItems()[0].ref.leadingCMLComments, CMLrt) if cmlComment is not None: dlg.setText(cmlComment.getText()) if dlg.exec_(): replacementText = dlg.text() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLrt) if cmlComment is not None: # Existed, so remove the old one first lineNo = cmlComment.ref.beginLine cmlComment.removeFromText(editor) else: lineNo = item.getFirstLine() line = CMLrt.generate(replacementText, item.ref.body.beginPos) editor.insertLines(line, lineNo) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onDelete(self): """Delete the item""" print("Delete") def onGroup(self): """Groups items into a single one""" print("Group") def onCopy(self): """Copying...""" print("Copy") def onCut(self): """Cutting...""" print("Cut") def onRemoveCustomColors(self): """Removing the previously set custom colors""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): if item.isDocstring(): cmlComment = CMLVersion.find( item.ref.docstring.leadingCMLComments, CMLcc) else: cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLcc) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def onRemoveReplacementText(self): """Removing replacement text""" if not self.__actionPrerequisites(): return # Memorize the current selection selection = self.serializeSelection() editor = self.selectedItems()[0].getEditor() with editor: for item in self.sortSelectedReverse(): cmlComment = CMLVersion.find(item.ref.leadingCMLComments, CMLrt) if cmlComment is not None: cmlComment.removeFromText(editor) QApplication.processEvents() self.parent().redrawNow() self.restoreSelectionByID(selection) def areSelectedOfTypes(self, matchList): """Checks if the selected items belong to the match""" # match is a list of pairs [kind, subKind] # None would mean 'match any' selectedItems = self.selectedItems() if selectedItems: for selectedItem in selectedItems: for kind, subKind in matchList: match = True if kind is not None: if kind != selectedItem.kind: match = False if subKind is not None: if subKind != selectedItem.subKind: match = False if match: break else: return False return True return False def isInSelected(self, matchList): """Checks if any if the match list items is in the selection""" # match is a list of pairs [kind, subKind] # None would mean 'match any' for selectedItem in self.selectedItems(): for kind, subKind in matchList: match = True if kind is not None: if kind != selectedItem.kind: match = False if subKind is not None: if subKind != selectedItem.subKind: match = False if match: return True return False def isDocstringInSelection(self): """True if a docstring item in the selection""" for item in self.selectedItems(): if item.isDocstring(): return True return False def isCommentInSelection(self): """True if a comment item in the selection""" for item in self.selectedItems(): if item.isComment(): return True return False def countItemsWithCML(self, cmlType): """Counts items with have a certain type of a CML comment""" count = 0 for item in self.selectedItems(): if item.isComment(): continue if item.isDocstring(): # Side comments for docstrings? Nonesense! So they are ignored # even if they are collected if CMLVersion.find(item.ref.docstring.leadingCMLComments, cmlType) is not None: count += 1 continue if hasattr(item.ref, 'leadingCMLComments'): if CMLVersion.find(item.ref.leadingCMLComments, cmlType) is not None: count += 1 continue if hasattr(item.ref, 'sideCMLComments'): if CMLVersion.find(item.ref.sideCMLComments, cmlType) is not None: count += 1 return count def sortSelectedReverse(self): """Sorts the selected items in reverse order""" result = [] for item in self.selectedItems(): itemBegin = item.getAbsPosRange()[0] for index in range(len(result)): if itemBegin > result[index].getAbsPosRange()[0]: result.insert(index, item) break else: result.append(item) return result
class WatchPointView(QTreeView): """Watch expression viewer widget""" sigSelectionChanged = pyqtSignal(QModelIndex) def __init__(self, parent, wpointsModel): QTreeView.__init__(self, parent) self.__model = None self.setModel(wpointsModel) self.setItemsExpandable(False) self.setRootIsDecorated(False) self.setAlternatingRowColors(True) self.setUniformRowHeights(True) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setItemDelegate(NoOutlineHeightDelegate(4)) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.__showContextMenu) self.doubleClicked.connect(self.__doubleClicked) self.__createPopupMenus() def setModel(self, model): """Sets the watch expression model""" self.__model = model self.sortingModel = QSortFilterProxyModel() self.sortingModel.setSourceModel(self.__model) QTreeView.setModel(self, self.sortingModel) header = self.header() header.setSortIndicator(0, Qt.AscendingOrder) header.setSortIndicatorShown(True) header.setSectionsClickable(True) self.setSortingEnabled(True) self.__layoutDisplay() def __layoutDisplay(self): """Performs the layout operation""" self.__resizeColumns() self.__resort() def __resizeColumns(self): """Resizes the view when items get added, edited or deleted""" self.header().resizeSections(QHeaderView.ResizeToContents) self.header().setStretchLastSection(True) def __resort(self): """Resorts the tree""" self.model().sort(self.header().sortIndicatorSection(), self.header().sortIndicatorOrder()) def __toSourceIndex(self, index): """Converts an index to a source index""" return self.sortingModel.mapToSource(index) def __fromSourceIndex(self, sindex): """Converts a source index to an index""" return self.sortingModel.mapFromSource(sindex) def __setRowSelected(self, index, selected=True): """Selects a row""" if not index.isValid(): return if selected: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows) else: flags = QItemSelectionModel.SelectionFlags( QItemSelectionModel.Deselect | QItemSelectionModel.Rows) self.selectionModel().select(index, flags) def __createPopupMenus(self): """Generates the popup menus""" self.menu = QMenu() self.menu.addAction("Add", self.__addWatchPoint) self.menu.addAction("Edit...", self.__editWatchPoint) self.menu.addSeparator() self.menu.addAction("Enable", self.__enableWatchPoint) self.menu.addAction("Enable all", self.__enableAllWatchPoints) self.menu.addSeparator() self.menu.addAction("Disable", self.__disableWatchPoint) self.menu.addAction("Disable all", self.__disableAllWatchPoints) self.menu.addSeparator() self.menu.addAction("Delete", self.__deleteWatchPoint) self.menu.addAction("Delete all", self.__deleteAllWatchPoints) self.backMenuActions = {} self.backMenu = QMenu() self.backMenu.addAction("Add", self.__addWatchPoint) self.backMenuActions["EnableAll"] = \ self.backMenu.addAction("Enable all", self.__enableAllWatchPoints) self.backMenuActions["DisableAll"] = \ self.backMenu.addAction("Disable all", self.__disableAllWatchPoints) self.backMenuActions["DeleteAll"] = \ self.backMenu.addAction("Delete all", self.__deleteAllWatchPoints) self.backMenu.aboutToShow.connect(self.__showBackMenu) self.multiMenu = QMenu() self.multiMenu.addAction("Add", self.__addWatchPoint) self.multiMenu.addSeparator() self.multiMenu.addAction("Enable selected", self.__enableSelectedWatchPoints) self.multiMenu.addAction("Enable all", self.__enableAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction("Disable selected", self.__disableSelectedWatchPoints) self.multiMenu.addAction("Disable all", self.__disableAllWatchPoints) self.multiMenu.addSeparator() self.multiMenu.addAction("Delete selected", self.__deleteSelectedWatchPoints) self.multiMenu.addAction("Delete all", self.__deleteAllWatchPoints) def __showContextMenu(self, coord): """Show the context menu""" cnt = self.__getSelectedItemsCount() if cnt <= 1: index = self.indexAt(coord) if index.isValid(): cnt = 1 self.__setRowSelected(index) coord = self.mapToGlobal(coord) if cnt > 1: self.multiMenu.popup(coord) elif cnt == 1: self.menu.popup(coord) else: self.backMenu.popup(coord) def __findDuplicates(self, cond, special, showMessage=False, index=QModelIndex()): """Checks if an entry already exists""" idx = self.__model.getWatchPointIndex(cond, special) duplicate = idx.isValid( ) and idx.internalPointer() != index.internalPointer() # if showMessage and duplicate: # if not special: # msg = """<p>A watch expression '<b>%1</b>'""" # """ already exists.</p>""".arg(Utilities.html_encode(unicode(cond))) # else: # msg = self.trUtf8("""<p>A watch expression '<b>%1</b>'""" # """ for the variable <b>%2</b> already exists.</p>""")\ # .arg(special)\ # .arg(Utilities.html_encode(unicode(cond))) # KQMessageBox.warning(None, # "Watch expression already exists", msg) return duplicate def __clearSelection(self): """Clears the selection""" for index in self.selectedIndexes(): self.__setRowSelected(index, False) def __addWatchPoint(self): """Adds watch expression via a context menu entry""" # dlg = EditWatchpointDialog( ( "", False, True, 0, "" ), self ) # if dlg.exec_() == QDialog.Accepted: # cond, temp, enabled, ignorecount, special = dlg.getData() # if not self.__findDuplicates(cond, special, True): # self.__model.addWatchPoint(cond, special, (temp, enabled, ignorecount)) # self.__resizeColumns() # self.__resort() return def __doubleClicked(self, index): """Handles the double clicked signal""" if index.isValid(): self.__doEditWatchPoint(index) def __editWatchPoint(self): """Handles the edit watch expression context menu entry""" index = self.currentIndex() if index.isValid(): self.__doEditWatchPoint(index) def __doEditWatchPoint(self, index): """Edits a watch expression""" sindex = self.__toSourceIndex(index) if sindex.isValid(): wp = self.__model.getWatchPointByIndex(sindex) if not wp: return cond, special, temp, enabled, count = wp[:5] # dlg = EditWatchpointDialog( # (cond, temp, enabled, count, special), self) # if dlg.exec_() == QDialog.Accepted: # cond, temp, enabled, count, special = dlg.getData() # if not self.__findDuplicates(cond, special, True, sindex): # self.__model.setWatchPointByIndex(sindex, # unicode(cond), unicode(special), (temp, enabled, count)) # self.__resizeColumns() # self.__resort() return def __setWpEnabled(self, index, enabled): """Sets the enabled status of a watch expression""" sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.setWatchPointEnabledByIndex(sindex, enabled) def __enableWatchPoint(self): """Handles the enable watch expression context menu entry""" index = self.currentIndex() self.__setWpEnabled(index, True) self.__resizeColumns() self.__resort() def __enableAllWatchPoints(self): """Handles the enable all watch expressions context menu entry""" index = self.model().index(0, 0) while index.isValid(): self.__setWpEnabled(index, True) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __enableSelectedWatchPoints(self): """Handles the enable selected watch expressions context menu entry""" for index in self.selectedIndexes(): if index.column() == 0: self.__setWpEnabled(index, True) self.__resizeColumns() self.__resort() def __disableWatchPoint(self): """Handles the disable watch expression context menu entry""" index = self.currentIndex() self.__setWpEnabled(index, False) self.__resizeColumns() self.__resort() def __disableAllWatchPoints(self): """Handles the disable all watch expressions context menu entry""" index = self.model().index(0, 0) while index.isValid(): self.__setWpEnabled(index, False) index = self.indexBelow(index) self.__resizeColumns() self.__resort() def __disableSelectedWatchPoints(self): """Handles the disable selected watch expressions context menu entry""" for index in self.selectedIndexes(): if index.column() == 0: self.__setWpEnabled(index, False) self.__resizeColumns() self.__resort() def __deleteWatchPoint(self): """Handles the delete watch expression context menu entry""" index = self.currentIndex() sindex = self.__toSourceIndex(index) if sindex.isValid(): self.__model.deleteWatchPointByIndex(sindex) def __deleteAllWatchPoints(self): """Handles the delete all watch expressions context menu entry""" self.__model.deleteAll() def __deleteSelectedWatchPoints(self): """Handles the delete selected watch expressions context menu entry""" idxList = [] for index in self.selectedIndexes(): sindex = self.__toSourceIndex(index) if sindex.isValid() and index.column() == 0: lastrow = index.row() idxList.append(sindex) self.__model.deleteWatchPoints(idxList) def __showBackMenu(self): """Handles the aboutToShow signal of the background menu""" if self.model().rowCount() == 0: self.backMenuActions["EnableAll"].setEnabled(False) self.backMenuActions["DisableAll"].setEnabled(False) self.backMenuActions["DeleteAll"].setEnabled(False) else: self.backMenuActions["EnableAll"].setEnabled(True) self.backMenuActions["DisableAll"].setEnabled(True) self.backMenuActions["DeleteAll"].setEnabled(True) def __getSelectedItemsCount(self): """Provides the count of items selected""" count = len(self.selectedIndexes()) / (self.__model.columnCount() - 1) # column count is 1 greater than selectable return count def selectionChanged(self, selected, deselected): """The slot is called when the selection has changed""" if selected.indexes(): self.sigSelectionChanged.emit(selected.indexes()[0]) else: self.sigSelectionChanged.emit(QModelIndex()) QTreeView.selectionChanged(self, selected, deselected)
class RedirectedIOConsole(QutepartWrapper): """Widget which implements the redirected IO console""" sigUserInput = pyqtSignal(str) MODE_OUTPUT = 0 MODE_INPUT = 1 def __init__(self, parent): QutepartWrapper.__init__(self, parent) self.setAttribute(Qt.WA_KeyCompression) self.mode = self.MODE_OUTPUT self.lastOutputPos = None self.inputEcho = True self.inputBuffer = "" self.__messages = IOConsoleMessages() self.__initGeneralSettings() self.__initMargins() self._initContextMenu() self.onTextZoomChanged() self.__hotKeys = {} self.__initHotKeys() self.installEventFilter(self) self.cursorPositionChanged.connect(self.setCursorStyle) def __initHotKeys(self): """Initializes a map for the hot keys event filter""" self.autoIndentLineAction.setShortcut('Ctrl+Shift+I') self.invokeCompletionAction.setEnabled(False) self.__hotKeys = { CTRL_SHIFT: { Qt.Key_C: self.onCtrlShiftC }, SHIFT: { Qt.Key_End: self.onShiftEnd, Qt.Key_Home: self.onShiftHome, Qt.Key_Insert: self.onPasteText, Qt.Key_Delete: self.onShiftDel }, CTRL: { Qt.Key_V: self.onPasteText, Qt.Key_X: self.onShiftDel, Qt.Key_C: self.onCtrlC, Qt.Key_Insert: self.onCtrlC, Qt.Key_Minus: Settings().onTextZoomOut, Qt.Key_Equal: Settings().onTextZoomIn, Qt.Key_0: Settings().onTextZoomReset, Qt.Key_Home: self.onFirstChar, Qt.Key_End: self.onLastChar }, CTRL_KEYPAD: { Qt.Key_Minus: Settings().onTextZoomOut, Qt.Key_Plus: Settings().onTextZoomIn, Qt.Key_0: Settings().onTextZoomReset }, NO_MODIFIER: { Qt.Key_Home: self.onHome, Qt.Key_End: self.moveToLineEnd } } def eventFilter(self, _, event): """Event filter to catch shortcuts on UBUNTU""" if event.type() == QEvent.KeyPress: key = event.key() modifiers = int(event.modifiers()) try: if modifiers in self.__hotKeys: if key in self.__hotKeys[modifiers]: self.__hotKeys[modifiers][key]() return True if modifiers == NO_MODIFIER: if key in [Qt.Key_Delete, Qt.Key_Backspace]: if not self.__isCutDelAvailable(): return True except Exception as exc: logging.warning(str(exc)) return False def keyPressEvent(self, event): """Triggered when a key is pressed""" key = event.key() if key == Qt.Key_Escape: self.clearSearchIndicators() return if self.mode == self.MODE_OUTPUT: if key in [ Qt.Key_Left, Qt.Key_Up, Qt.Key_Right, Qt.Key_Down, Qt.Key_PageUp, Qt.Key_PageDown ]: QutepartWrapper.keyPressEvent(self, event) return # It is an input mode txt = event.text() if len(txt) and txt >= ' ': # Printable character if self.absCursorPosition < self.lastOutputPos: # Out of the input zone return if self.inputEcho: QutepartWrapper.keyPressEvent(self, event) else: self.inputBuffer += txt return # Non-printable character or some other key if key == Qt.Key_Enter or key == Qt.Key_Return: userInput = self.__getUserInput() self.switchMode(self.MODE_OUTPUT) timestampLine, _ = self.getEndPosition() self.append('\n') self.clearUndoRedoHistory() line, pos = self.getEndPosition() self.cursorPosition = line, pos self.ensureLineOnScreen(line) msg = IOConsoleMsg(IOConsoleMsg.STDIN_MESSAGE, userInput + '\n') self.__messages.append(msg) # margin data timestamp = msg.getTimestamp() margin = self.getMargin('cdm_redirected_io_margin') margin.addData(timestampLine + 1, timestamp, timestamp, IOConsoleMsg.STDIN_MESSAGE) self.sigUserInput.emit(userInput) return if key == Qt.Key_Backspace: if self.absCursorPosition == self.lastOutputPos: if not self.inputEcho: self.inputBuffer = self.inputBuffer[:-1] return QutepartWrapper.keyPressEvent(self, event) def mouseDoubleClickEvent(self, event): """Disable search highlight on double click""" Qutepart.mouseDoubleClickEvent(self, event) def onPasteText(self): """Triggered when insert is requested""" if self.mode == self.MODE_OUTPUT: return if self.absCursorPosition < self.lastOutputPos: return # Check what is in the buffer text = QApplication.clipboard().text() if '\n' in text or '\r' in text: return if not self.inputEcho: self.inputBuffer += text return self.paste() def __getUserInput(self): """Provides the collected user input""" if self.mode != self.MODE_INPUT: return '' if self.inputEcho: _, endPos = self.getEndPosition() _, beginPos = self.mapToLineCol(self.lastOutputPos) return self.lines[-1][beginPos:endPos] value = self.inputBuffer self.inputBuffer = "" return value def __initGeneralSettings(self): """Sets some generic look and feel""" skin = GlobalData().skin self.updateSettings() self.setPaper(skin['ioconsolePaper']) self.setColor(skin['ioconsoleColor']) self.currentLineColor = None self.lineLengthEdge = None self.setCursorStyle() def updateSettings(self): """Updates the IO console settings""" if Settings()['ioconsolelinewrap']: self.setWordWrapMode(QTextOption.WrapAnywhere) else: self.setWordWrapMode(QTextOption.NoWrap) self.drawAnyWhitespace = Settings()['ioconsoleshowspaces'] self.drawIncorrectIndentation = Settings()['ioconsoleshowspaces'] def setCursorStyle(self): """Sets the cursor style depending on the mode and the cursor pos""" if self.mode == self.MODE_OUTPUT: if self.cursorWidth() != 1: self.setCursorWidth(1) else: if self.absCursorPosition >= self.lastOutputPos: if self.cursorWidth() == 1: fontMetrics = QFontMetrics(self.font(), self) self.setCursorWidth(fontMetrics.width('W')) self.update() else: if self.cursorWidth() != 1: self.setCursorWidth(1) self.update() def switchMode(self, newMode): """Switches between input/output mode""" self.mode = newMode if self.mode == self.MODE_OUTPUT: self.lastOutputPos = None self.inputEcho = True self.inputBuffer = "" else: line, pos = self.getEndPosition() self.cursorPosition = line, pos self.lastOutputPos = self.absCursorPosition self.ensureLineOnScreen(line) self.setCursorStyle() def __initMargins(self): """Initializes the IO console margins""" # The supported margins: timestamp self.addMargin(CDMRedirectedIOMargin(self)) def _initContextMenu(self): """Called to initialize a context menu""" self._menu = QMenu(self) self.__menuUndo = self._menu.addAction(getIcon('undo.png'), '&Undo', self.onUndo, "Ctrl+Z") self.__menuRedo = self._menu.addAction(getIcon('redo.png'), '&Redo', self.onRedo, "Ctrl+Y") self._menu.addSeparator() self.__menuCut = self._menu.addAction(getIcon('cutmenu.png'), 'Cu&t', self.onShiftDel, "Ctrl+X") self.__menuCopy = self._menu.addAction(getIcon('copymenu.png'), '&Copy', self.onCtrlC, "Ctrl+C") self.__menucopyTimestamp = self._menu.addAction( getIcon('copymenu.png'), '&Copy all with timestamps', self.onCtrlShiftC, "Ctrl+Shift+C") self.__menuPaste = self._menu.addAction(getIcon('pastemenu.png'), '&Paste', self.onPasteText, "Ctrl+V") self.__menuSelectAll = self._menu.addAction( getIcon('selectallmenu.png'), 'Select &all', self.selectAll, "Ctrl+A") self._menu.addSeparator() self.__menuOpenAsFile = self._menu.addAction(getIcon('filemenu.png'), 'O&pen as file', self.openAsFile) self.__menuDownloadAndShow = self._menu.addAction( getIcon('filemenu.png'), 'Do&wnload and show', self.downloadAndShow) self.__menuOpenInBrowser = self._menu.addAction( getIcon('homepagemenu.png'), 'Open in browser', self.openInBrowser) self._menu.addSeparator() self._menu.aboutToShow.connect(self._contextMenuAboutToShow) self._menu.aboutToHide.connect(self._contextMenuAboutToHide) def contextMenuEvent(self, event): """Called just before showing a context menu""" event.accept() self._menu.popup(event.globalPos()) def _contextMenuAboutToShow(self): """IO Console context menu is about to show""" self.__menuUndo.setEnabled(self.document().isUndoAvailable()) self.__menuRedo.setEnabled(self.document().isRedoAvailable()) pasteText = QApplication.clipboard().text() pasteEnable = pasteText != "" and \ '\n' not in pasteText and \ '\r' not in pasteText and \ self.mode != self.MODE_OUTPUT if pasteEnable: if self.absCursorPosition < self.lastOutputPos: pasteEnable = False # Need to make decision about menu items for modifying the input self.__menuCut.setEnabled(self.__isCutDelAvailable()) self.__menuCopy.setEnabled(self.__messages.size > 0) self.__menucopyTimestamp.setEnabled(self.__messages.size > 0) self.__menuPaste.setEnabled(pasteEnable) self.__menuSelectAll.setEnabled(self.__messages.size > 0) self.__menuOpenAsFile.setEnabled(self.openAsFileAvailable()) self.__menuDownloadAndShow.setEnabled(self.downloadAndShowAvailable()) self.__menuOpenInBrowser.setEnabled(self.downloadAndShowAvailable()) def _contextMenuAboutToHide(self): """IO console context menu is about to hide""" self.__menuUndo.setEnabled(True) self.__menuRedo.setEnabled(True) self.__menuCut.setEnabled(True) self.__menuCopy.setEnabled(True) self.__menucopyTimestamp.setEnabled(True) self.__menuPaste.setEnabled(True) self.__menuSelectAll.setEnabled(True) self.__menuOpenAsFile.setEnabled(True) self.__menuDownloadAndShow.setEnabled(True) self.__menuOpenInBrowser.setEnabled(True) def __isCutDelAvailable(self): """Returns True if cutting or deletion is possible""" if self.mode == self.MODE_OUTPUT: return False if self.selectedText: startPosition, cursorPosition = self.absSelectedPosition minPos = min(startPosition, cursorPosition) return minPos >= self.lastOutputPos return self.absCursorPosition > self.lastOutputPos def onShiftDel(self): """Deletes the selected text""" if self.selectedText: if self.__isCutDelAvailable(): self.cut() return True return True def onUndo(self): """undo implementation""" if self.document().isUndoAvailable(): self.undo() def onRedo(self): """redo implementation""" if self.document().isRedoAvailable(): self.redo() def onCtrlShiftC(self): """Copy all with timestamps""" QApplication.clipboard().setText( self.__messages.renderWithTimestamps()) def appendIDEMessage(self, text): """Appends an IDE message""" msg = IOConsoleMsg(IOConsoleMsg.IDE_MESSAGE, text) self.__appendMessage(msg) return msg def appendStdoutMessage(self, text): """Appends an stdout message""" msg = IOConsoleMsg(IOConsoleMsg.STDOUT_MESSAGE, text) self.__appendMessage(msg) return msg def appendStderrMessage(self, text): """Appends an stderr message""" msg = IOConsoleMsg(IOConsoleMsg.STDERR_MESSAGE, text) self.__appendMessage(msg) return msg def __appendMessage(self, message): """Appends a new message to the console""" if not self.__messages.append(message): # There was no trimming of the message list self.__renderMessage(message) else: # Some messages were stripped self.renderContent() def renderContent(self): """Regenerates the viewer content""" self.clear() self.getMargin('cdm_redirected_io_margin').clear() for msg in self.__messages.msgs: self.__renderMessage(msg) def __renderMessage(self, msg): """Adds a single message""" margin = self.getMargin('cdm_redirected_io_margin') timestamp = msg.getTimestamp() if msg.msgType == IOConsoleMsg.IDE_MESSAGE: line, pos = self.getEndPosition() txt = msg.msgText startMarkLine = line if pos != 0: txt = '\n' + txt startMarkLine += 1 self.append(txt) line, _ = self.getEndPosition() for lineNo in range(startMarkLine, line + 1): margin.addData(lineNo + 1, timestamp, timestamp, IOConsoleMsg.IDE_MESSAGE) else: line, pos = self.getEndPosition() txt = msg.msgText startTimestampLine = line if pos != 0: lastMsgType = margin.getLineMessageType(line + 1) if lastMsgType == IOConsoleMsg.IDE_MESSAGE: txt = '\n' + txt startTimestampLine = line + 1 self.append(txt) endTimestampLine, pos = self.getEndPosition() if pos == 0: endTimestampLine -= 1 for lineNo in range(startTimestampLine, endTimestampLine + 1): margin.addData(lineNo + 1, timestamp, timestamp, msg.msgType) self.clearUndoRedoHistory() if Settings()['ioconsoleautoscroll']: line, pos = self.getEndPosition() self.gotoLine(line + 1, pos + 1) def clearData(self): """Clears the collected data""" self.__messages.clear() self.getMargin('cdm_redirected_io_margin').clear() def clearAll(self): """Clears both data and visible content""" self.clearData() self.clear() self.clearUndoRedoHistory()