class ResourceRetriever(QThread): """Retrieves the item from the web""" sigRetrieveOK = pyqtSignal(str, str, str) # url, uuid, file sigRetrieveError = pyqtSignal(str, str) # url, file def __init__(self, parent=None): QThread.__init__(self, parent) self.__url = None self.__uuid = None self.__fName = None def get(self, url, fName, uuid): """Initiate the resource request""" self.__url = url self.__uuid = uuid self.__fName = fName self.start() def run(self): """Run the retriever""" try: req = urllib.request.urlopen(self.__url, timeout=TIMEOUT) saveBinaryToFile(self.__fName, req.read()) self.sigRetrieveOK.emit(self.__url, self.__uuid, self.__fName) except Exception as exc: logging.error('Cannot retrieve %s: %s', self.__url, str(exc)) self.sigRetrieveError.emit(self.__url, self.__fName)
class PlantUMLRenderer(QThread): """Runs plantuml""" sigFinishedOK = pyqtSignal(str, str, str) # md5, uuid, file sigFinishedError = pyqtSignal(str, str) # md5, file def __init__(self, parent=None): QThread.__init__(self, parent) self.__source = None self.__md5 = None self.__uuid = None self.__fName = None def get(self, source, md5, fName, uuid): """Initiates rendering the diagram""" self.__source = source self.__md5 = md5 self.__uuid = uuid self.__fName = fName self.start() @staticmethod def safeUnlink(fName): """Safe file removal""" try: os.unlink(fName) except: pass def run(self): """Runs plantUML""" srcFile = self.__fName[:-3] + 'txt' try: # Run plantUML saveToFile(srcFile, self.__source) retCode = subprocess.call([ 'java', '-jar', JAR_PATH, '-charset', 'utf-8', '-nometadata', srcFile ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) self.safeUnlink(srcFile) if retCode == 0: self.sigFinishedOK.emit(self.__md5, self.__uuid, self.__fName) else: self.sigFinishedError.emit(self.__md5, self.__fName) except Exception as exc: logging.error('Cannot render a plantUML diagram: %s', str(exc)) self.safeUnlink(srcFile) self.sigFinishedError.emit(self.__md5, self.__fName)
class CDMPluginBase(IPlugin, QObject): """Base class for all codimension plugin categories""" pluginLogMessage = pyqtSignal(int, str) def __init__(self): IPlugin.__init__(self) QObject.__init__(self) self.ide = IDEAccess(self) def activate(self, ideSettings, ideGlobalData): """Activates the plugin. Also saves references to the IDE settings and global data """ IPlugin.activate(self) self.ide.activate(ideSettings, ideGlobalData) def deactivate(self): """Deactivates the plugin. Also clears references to the IDE settings and global data """ self.ide.deactivate() IPlugin.deactivate(self) def getConfigFunction(self): """A plugin may provide a function for configuring it. If a plugin does not require any config parameters then None should be returned. By default no configuring is required. """ return None
class DebuggerExceptions(QWidget): """Implements the debugger context viewer""" sigClientExceptionsCleared = pyqtSignal() def __init__(self, parent=None): QWidget.__init__(self, parent) self.__createLayout() self.clientExcptViewer.sigClientExceptionsCleared.connect( self.__onClientExceptionsCleared) def __createLayout(self): """Creates the widget layout""" verticalLayout = QVBoxLayout(self) verticalLayout.setContentsMargins(1, 1, 1, 1) self.splitter = QSplitter(Qt.Vertical) self.ignoredExcptViewer = IgnoredExceptionsViewer(self.splitter) self.clientExcptViewer = ClientExceptionsViewer( self.splitter, self.ignoredExcptViewer) self.splitter.addWidget(self.clientExcptViewer) self.splitter.addWidget(self.ignoredExcptViewer) self.splitter.setCollapsible(0, False) self.splitter.setCollapsible(1, False) verticalLayout.addWidget(self.splitter) def clear(self): """Clears everything""" self.clientExcptViewer.clear() def addException(self, exceptionType, exceptionMessage, stackTrace): """Adds the exception to the view""" self.clientExcptViewer.addException(exceptionType, exceptionMessage, stackTrace) def isIgnored(self, exceptionType): """Returns True if this exception type should be ignored""" return self.ignoredExcptViewer.isIgnored(exceptionType) def setFocus(self): """Sets the focus to the client exception window""" self.clientExcptViewer.setFocus() def getTotalClientExceptionCount(self): """Provides the total number of the client exceptions""" return self.clientExcptViewer.getTotalCount() def __onClientExceptionsCleared(self): """Triggered when the user cleared exceptions""" self.sigClientExceptionsCleared.emit()
class ProfilerTreeWidget(QTreeWidget): """Need only to generate sigEscapePressed signal""" sigEscapePressed = pyqtSignal() def __init__(self, parent=None): QTreeWidget.__init__(self, parent) def keyPressEvent(self, event): """Handles the key press events""" if event.key() == Qt.Key_Escape: self.sigEscapePressed.emit() event.accept() else: QTreeWidget.keyPressEvent(self, event)
class SettingsButton(QPushButton): """Custom settings button""" CustomClick = pyqtSignal(int) def __init__(self): QPushButton.__init__(self, getIcon('pluginsettings.png'), "") self.setFixedSize(24, 24) self.setFocusPolicy(Qt.NoFocus) self.index = -1 self.clicked.connect(self.onClick) def onClick(self): """Emits a signal with the button index""" self.CustomClick.emit(self.index)
class DisassemblyTreeWidget(QTreeWidget): """Need only to generate sigEscapePressed signal""" sigEscapePressed = pyqtSignal() def __init__(self, parent=None): QTreeWidget.__init__(self, parent) self.setAlternatingRowColors(True) self.setRootIsDecorated(True) self.setItemsExpandable(True) self.setSortingEnabled(False) self.setItemDelegate(NoOutlineHeightDelegate(4)) self.setUniformRowHeights(True) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setExpandsOnDoubleClick(False) headerLabels = [ "Line", "Jump", "Address", "Instruction", "Argument", "Argument interpretation" ] self.setHeaderLabels(headerLabels) headerItem = self.headerItem() headerItem.setToolTip( 0, "The corresponding line number in the source code") headerItem.setToolTip( 1, "A possible JUMP from an earlier instruction to this one") headerItem.setToolTip( 2, "The address in the bytecode which corresponds to " "the byte index") headerItem.setToolTip(3, "The instruction name (also called opname)") headerItem.setToolTip( 4, "The argument (if any) of the instruction which is used " "internally by Python to fetch some constants or variables, " "manage the stack, jump to a specific instruction, etc.") headerItem.setToolTip( 5, "The human-friendly interpretation of the instruction argument") def keyPressEvent(self, event): """Handles the key press events""" if event.key() == Qt.Key_Escape: self.sigEscapePressed.emit() event.accept() else: QTreeWidget.keyPressEvent(self, event)
class NavBarComboBox(QComboBox): """Navigation bar combo box""" jumpToLine = pyqtSignal(int) def __init__(self, parent=None): QComboBox.__init__(self, parent) self.setSizeAdjustPolicy(QComboBox.AdjustToMinimumContentsLength) sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(sizePolicy) self.activated.connect(self.onActivated) self.view().installEventFilter(self) self.pathIndex = None # Arguments: obj, event def eventFilter(self, _, event): """Event filter for the qcombobox list view""" if event.type() == QEvent.KeyPress: key = event.key() if key == Qt.Key_Escape: self.parent().getEditor().setFocus() return True if key == Qt.Key_Left: # Move cursor to the left combo if self.pathIndex is not None: self.parent().activateCombo(self, self.pathIndex - 1) return True if key == Qt.Key_Right: # Move cursor to the right combo if self.pathIndex is not None: self.parent().activateCombo(self, self.pathIndex + 1) else: self.parent().activateCombo(self, 0) return True return False def onActivated(self, index): """User selected an item""" if index >= 0: self.jumpToLine.emit(self.itemData(index))
class TextEditor(QutepartWrapper, EditorContextMenuMixin): """Text editor implementation""" sigEscapePressed = pyqtSignal() sigCFlowSyncRequested = pyqtSignal(int, int, int) def __init__(self, parent, debugger): self._parent = parent QutepartWrapper.__init__(self, parent) EditorContextMenuMixin.__init__(self) self.setAttribute(Qt.WA_KeyCompression) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) skin = GlobalData().skin self.setPaper(skin['nolexerPaper']) self.setColor(skin['nolexerColor']) self.currentLineColor = skin['currentLinePaper'] self.onTextZoomChanged() self.__initMargins(debugger) self.cursorPositionChanged.connect(self._onCursorPositionChanged) self.__skipChangeCursor = False self.__openedLine = None self.setFocusPolicy(Qt.StrongFocus) self.indentWidth = 4 self.updateSettings() # Completion support self.__completionPrefix = '' self.__completionLine = -1 self.__completionPos = -1 self.__completer = CodeCompleter(self) self.__inCompletion = False self.__completer.activated.connect(self.insertCompletion) self.__lastTabPosition = None # Calltip support self.__calltip = None self.__callPosition = None self.__calltipTimer = QTimer(self) self.__calltipTimer.setSingleShot(True) self.__calltipTimer.timeout.connect(self.__onCalltipTimer) self.__initHotKeys() self.installEventFilter(self) def dedentLine(self): """Dedent the current line or selection""" self.decreaseIndentAction.activate(QAction.Trigger) 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_T: self.onJumpToTop, Qt.Key_M: self.onJumpToMiddle, Qt.Key_B: self.onJumpToBottom}, SHIFT: {Qt.Key_Delete: self.onShiftDel, Qt.Key_Backtab: self.dedentLine, Qt.Key_End: self.onShiftEnd, Qt.Key_Home: self.onShiftHome}, CTRL: {Qt.Key_X: self.onShiftDel, Qt.Key_C: self.onCtrlC, Qt.Key_Insert: self.onCtrlC, Qt.Key_Apostrophe: self.onHighlight, Qt.Key_Period: self.onNextHighlight, Qt.Key_Comma: self.onPrevHighlight, Qt.Key_M: self.onCommentUncomment, Qt.Key_Space: self.onAutoComplete, Qt.Key_F1: self.onTagHelp, Qt.Key_Backslash: self.onGotoDefinition, Qt.Key_BracketRight: self.onOccurences, Qt.Key_Slash: self.onShowCalltip, 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, Qt.Key_B: self.highlightInOutline, Qt.Key_QuoteLeft: self.highlightInCFlow}, ALT: {Qt.Key_U: self.onScopeBegin}, 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, Qt.Key_F12: self.makeLineFirst}} # Not all the derived classes need certain tool functionality if hasattr(self._parent, "getType"): widgetType = self._parent.getType() if widgetType in [MainWindowTabWidgetBase.PlainTextEditor]: if hasattr(self._parent, "onOpenImport"): self.__hotKeys[CTRL][Qt.Key_I] = self._parent.onOpenImport if hasattr(self._parent, "onNavigationBar"): self.__hotKeys[NO_MODIFIER][Qt.Key_F2] = \ self._parent.onNavigationBar # Arguments: obj, event def eventFilter(self, _, event): """Event filter to catch shortcuts on UBUNTU""" if event.type() == QEvent.KeyPress: key = event.key() if self.isReadOnly(): if key in [Qt.Key_Delete, Qt.Key_Backspace, Qt.Key_Backtab, Qt.Key_X, Qt.Key_Tab, Qt.Key_Space, Qt.Key_Slash, Qt.Key_Z, Qt.Key_Y]: return True modifiers = int(event.modifiers()) try: self.__hotKeys[modifiers][key]() return True except KeyError: return False except Exception as exc: logging.warning(str(exc)) return False def wheelEvent(self, event): """Mouse wheel event""" if QApplication.keyboardModifiers() == Qt.ControlModifier: angleDelta = event.angleDelta() if not angleDelta.isNull(): if angleDelta.y() > 0: Settings().onTextZoomIn() else: Settings().onTextZoomOut() event.accept() else: QutepartWrapper.wheelEvent(self, event) def focusInEvent(self, event): """Enable Shift+Tab when the focus is received""" if self._parent.shouldAcceptFocus(): QutepartWrapper.focusInEvent(self, event) else: self._parent.setFocus() def focusOutEvent(self, event): """Disable Shift+Tab when the focus is lost""" self.__completer.hide() if not self.__inCompletion: self.__resetCalltip() QutepartWrapper.focusOutEvent(self, event) def updateSettings(self): """Updates the editor settings""" settings = Settings() if settings['verticalEdge']: self.lineLengthEdge = settings['editorEdge'] self.lineLengthEdgeColor = GlobalData().skin['edgeColor'] self.drawSolidEdge = True else: self.lineLengthEdge = None self.drawAnyWhitespace = settings['showSpaces'] self.drawIncorrectIndentation = settings['showSpaces'] if settings['lineWrap']: self.setWordWrapMode(QTextOption.WrapAnywhere) else: self.setWordWrapMode(QTextOption.NoWrap) if hasattr(self._parent, "getNavigationBar"): navBar = self._parent.getNavigationBar() if navBar: navBar.updateSettings() def __initMargins(self, debugger): """Initializes the editor margins""" self.addMargin(CDMLineNumberMargin(self)) self.addMargin(CDMFlakesMargin(self)) self.getMargin('cdm_flakes_margin').setVisible(False) if debugger: self.addMargin(CDMBreakpointMargin(self, debugger)) self.getMargin('cdm_bpoint_margin').setVisible(False) def highlightCurrentDebuggerLine(self, line, asException): """Highlights the current debugger line""" margin = self.getMargin('cdm_flakes_margin') if margin: if asException: margin.setExceptionLine(line) else: margin.setCurrentDebugLine(line) def clearCurrentDebuggerLine(self): """Removes the current debugger line marker""" margin = self.getMargin('cdm_flakes_margin') if margin: margin.clearDebugMarks() def readFile(self, fileName): """Reads the text from a file""" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: content, self.encoding = readEncodedFile(fileName) self.eol = detectEolString(content) # Copied from enki (enki/core/document.py: _readFile()): # Strip last EOL. Qutepart adds it when saving file if content.endswith('\r\n'): content = content[:-2] elif content.endswith('\n') or content.endswith('\r'): content = content[:-1] self.text = content self.mime, _, xmlSyntaxFile = getFileProperties(fileName) if xmlSyntaxFile: self.detectSyntax(xmlSyntaxFile) self.document().setModified(False) except: QApplication.restoreOverrideCursor() raise QApplication.restoreOverrideCursor() def writeFile(self, fileName): """Writes the text to a file""" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if Settings()['removeTrailingOnSave']: self.removeTrailingWhitespaces() try: encoding = detectWriteEncoding(self, fileName) if encoding is None: QApplication.restoreOverrideCursor() logging.error('Could not detect write encoding for ' + fileName) return False writeEncodedFile(fileName, self.textForSaving(), encoding) except Exception as exc: logging.error(str(exc)) QApplication.restoreOverrideCursor() return False self.encoding = encoding if self.explicitUserEncoding: userEncoding = getFileEncoding(fileName) if userEncoding != self.explicitUserEncoding: setFileEncoding(fileName, self.explicitUserEncoding) self.explicitUserEncoding = None self._parent.updateModificationTime(fileName) self._parent.setReloadDialogShown(False) QApplication.restoreOverrideCursor() return True def setReadOnly(self, mode): """Overridden version""" QPlainTextEdit.setReadOnly(self, mode) if mode: # Otherwise the cursor is suppressed in the RO mode self.setTextInteractionFlags(self.textInteractionFlags() | Qt.TextSelectableByKeyboard) self.increaseIndentAction.setEnabled(not mode) self.decreaseIndentAction.setEnabled(not mode) self.autoIndentLineAction.setEnabled(not mode) self.indentWithSpaceAction.setEnabled(not mode) self.unIndentWithSpaceAction.setEnabled(not mode) self.undoAction.setEnabled(not mode) self.redoAction.setEnabled(not mode) self.moveLineUpAction.setEnabled(not mode) self.moveLineDownAction.setEnabled(not mode) self.deleteLineAction.setEnabled(not mode) self.pasteLineAction.setEnabled(not mode) self.cutLineAction.setEnabled(not mode) self.duplicateLineAction.setEnabled(not mode) def keyPressEvent(self, event): """Handles the key press events""" key = event.key() if self.isReadOnly(): # Space scrolls # Ctrl+X/Shift+Del/Alt+D/Alt+X deletes something if key in [Qt.Key_Delete, Qt.Key_Backspace, Qt.Key_Space, Qt.Key_X, Qt.Key_Tab, Qt.Key_Z, Qt.Key_Y]: return # Qutepart has its own handler and lets to insert new lines when # ENTER is clicked, so use the QPlainTextEdit return QPlainTextEdit.keyPressEvent(self, event) self.__skipChangeCursor = True if self.__completer.isVisible(): self.__skipChangeCursor = False if key == Qt.Key_Escape: self.__completer.hide() self.setFocus() return # There could be backspace or printed characters only QutepartWrapper.keyPressEvent(self, event) QApplication.processEvents() if key == Qt.Key_Backspace: if self.__completionPrefix == '': self.__completer.hide() self.setFocus() else: self.__completionPrefix = self.__completionPrefix[:-1] self.__completer.setPrefix(self.__completionPrefix) else: self.__completionPrefix += event.text() self.__completer.setPrefix(self.__completionPrefix) if self.__completer.completionCount() == 0: self.__completer.hide() self.setFocus() elif key in [Qt.Key_Enter, Qt.Key_Return]: QApplication.processEvents() line, _ = self.cursorPosition QutepartWrapper.keyPressEvent(self, event) QApplication.processEvents() if line == self.__openedLine: self.lines[line] = '' # If the new line has one or more spaces then it is a candidate for # automatic trimming line, pos = self.cursorPosition text = self.lines[line] self.__openedLine = None if pos > 0 and len(text.strip()) == 0: self.__openedLine = line elif key in [Qt.Key_Up, Qt.Key_PageUp, Qt.Key_Down, Qt.Key_PageDown]: line, _ = self.cursorPosition lineToTrim = line if line == self.__openedLine else None QutepartWrapper.keyPressEvent(self, event) QApplication.processEvents() if lineToTrim is not None: line, _ = self.cursorPosition if line != lineToTrim: # The cursor was really moved to another line self.lines[lineToTrim] = '' self.__openedLine = None elif key == Qt.Key_Escape: self.__resetCalltip() self.sigEscapePressed.emit() event.accept() elif key == Qt.Key_Tab: if self.selectedText: QutepartWrapper.keyPressEvent(self, event) self.__lastTabPosition = None else: line, pos = self.cursorPosition currentPosition = self.absCursorPosition if pos != 0: char = self.lines[line][pos - 1] if char not in [' ', ':', '{', '}', '[', ']', ',', '<', '>', '+', '!', ')'] and \ currentPosition != self.__lastTabPosition: self.__lastTabPosition = currentPosition self.onAutoComplete() event.accept() else: QutepartWrapper.keyPressEvent(self, event) self.__lastTabPosition = currentPosition else: QutepartWrapper.keyPressEvent(self, event) self.__lastTabPosition = currentPosition elif key == Qt.Key_Z and \ int(event.modifiers()) == (Qt.ControlModifier + Qt.ShiftModifier): event.accept() elif key == Qt.Key_ParenLeft: if Settings()['editorCalltips']: QutepartWrapper.keyPressEvent(self, event) self.onShowCalltip(False) else: QutepartWrapper.keyPressEvent(self, event) else: # Special keyboard keys are delivered as 0 values if key != 0: self.__openedLine = None QutepartWrapper.keyPressEvent(self, event) self.__skipChangeCursor = False def _onCursorPositionChanged(self): """Triggered when the cursor changed the position""" self.__lastTabPosition = None line, _ = self.cursorPosition if self.__calltip: if self.__calltipTimer.isActive(): self.__calltipTimer.stop() if self.absCursorPosition < self.__callPosition: self.__resetCalltip() else: self.__calltipTimer.start(500) if not self.__skipChangeCursor: if line == self.__openedLine: self.__openedLine = None return if self.__openedLine is not None: self.__skipChangeCursor = True self.lines[self.__openedLine] = '' self.__skipChangeCursor = False self.__openedLine = None def getCurrentPosFont(self): """Provides the font of the current character""" if self.lexer_ is not None: font = self.lexer_.font(self.styleAt(self.currentPosition())) else: font = self.font() font.setPointSize(font.pointSize() + self.getZoom()) return font def onCommentUncomment(self): """Triggered when Ctrl+M is received""" if self.isReadOnly() or not self.isPythonBuffer(): return with self: # Detect what we need - comment or uncomment line, _ = self.cursorPosition txt = self.lines[line] nonSpaceIndex = self.firstNonSpaceIndex(txt) if self.isCommentLine(line): # need to uncomment if nonSpaceIndex == len(txt) - 1: # Strip the only '#' character stripCount = 1 else: # Strip up to two characters if the next char is a ' ' if txt[nonSpaceIndex + 1] == ' ': stripCount = 2 else: stripCount = 1 newTxt = txt[:nonSpaceIndex] + txt[nonSpaceIndex + stripCount:] if not newTxt.strip(): newTxt = '' self.lines[line] = newTxt else: # need to comment if nonSpaceIndex is None: self.lines[line] = '# ' else: newTxt = '# '.join((txt[:nonSpaceIndex], txt[nonSpaceIndex:])) self.lines[line] = newTxt # Jump to the beginning of the next line if line + 1 < len(self.lines): line += 1 self.cursorPosition = line, 0 self.ensureLineOnScreen(line) def onAutoComplete(self): """Triggered when ctrl+space or TAB is clicked""" if self.isReadOnly(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.__inCompletion = True self.__completionPrefix = self.getWordBeforeCursor() words = getCompletionList(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if len(words) == 0: self.setFocus() self.__inCompletion = False return self.__completer.setWordsList(words, self.font()) self.__completer.setPrefix(self.__completionPrefix) count = self.__completer.completionCount() if count == 0: self.setFocus() self.__inCompletion = False return # Make sure the line is visible line, _ = self.cursorPosition self.ensureLineOnScreen(line + 1) # Remove the selection as it could be interpreted not as expected if self.selectedText: self.clearSelection() if count == 1: self.insertCompletion(self.__completer.currentCompletion()) else: cRectangle = self.cursorRect() cRectangle.setLeft(cRectangle.left() + self.viewport().x()) cRectangle.setTop(cRectangle.top() + self.viewport().y() + 2) self.__completer.complete(cRectangle) # If something was selected then the next tab does not need to # bring the completion again. This is covered in the # insertCompletion() method. Here we reset the last tab position # preliminary because it is unknown if something will be inserted. self.__lastTabPosition = None self.__inCompletion = False def onTagHelp(self): """Provides help for an item if available""" if not self.isPythonBuffer(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) definitions = getDefinitions(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() parts = [] for definition in definitions: header = 'Type: ' + definition[3] if definition[5]: header += '\nModule: ' + definition[5] parts.append(header + '\n\n' + definition[4]) if parts: QToolTip.showText(self.mapToGlobal(self.cursorRect().bottomLeft()), '<pre>' + '\n\n'.join(parts) + '</pre>') else: GlobalData().mainWindow.showStatusBarMessage( "Definition is not found") def makeLineFirst(self): """Make the cursor line the first on the screen""" currentLine, _ = self.cursorPosition self.setFirstVisibleLine(currentLine) def onJumpToTop(self): """Jumps to the first position of the first visible line""" self.cursorPosition = self.firstVisibleLine(), 0 def onJumpToMiddle(self): """Jumps to the first line pos in a middle of the editing area""" # Count the number of the visible line count = 0 firstVisible = self.firstVisibleLine() lastVisible = self.lastVisibleLine() candidate = firstVisible while candidate <= lastVisible: if self.isLineVisible(candidate): count += 1 candidate += 1 shift = int(count / 2) jumpTo = firstVisible while shift > 0: if self.isLineVisible(jumpTo): shift -= 1 jumpTo += 1 self.cursorPosition = jumpTo, 0 def onJumpToBottom(self): """Jumps to the first position of the last line""" currentFirstVisible = self.firstVisibleLine() self.cursorPosition = self.lastVisibleLine(), 0 safeLastVisible = self.lastVisibleLine() while self.firstVisibleLine() != currentFirstVisible: # Here: a partially visible last line caused scrolling. So the # cursor needs to be set to the previous visible line self.cursorPosition = currentFirstVisible, 0 safeLastVisible -= 1 while not self.isLineVisible(safeLastVisible): safeLastVisible -= 1 self.cursorPosition = safeLastVisible, 0 def onGotoDefinition(self): """The user requested a jump to definition""" if not self.isPythonBuffer(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) definitions = getDefinitions(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if definitions: if len(definitions) == 1: GlobalData().mainWindow.openFile( definitions[0][0], definitions[0][1], definitions[0][2] + 1) else: if hasattr(self._parent, "importsBar"): self._parent.importsBar.showDefinitions(definitions) else: GlobalData().mainWindow.showStatusBarMessage( "Definition is not found") def onScopeBegin(self): """The user requested jumping to the current scope begin""" if self.isPythonBuffer(): info = getBriefModuleInfoFromMemory(self.text) context = getContext(self, info, True) if context.getScope() != context.GlobalScope: GlobalData().mainWindow.jumpToLine(context.getLastScopeLine()) return def onShowCalltip(self, showMessage=True): """The user requested show calltip""" if self.__calltip is not None: self.__resetCalltip() return if not self.isPythonBuffer(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) signatures = getCallSignatures(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if not signatures: if showMessage: GlobalData().mainWindow.showStatusBarMessage( "No calltip found") return # For the time being let's take only the first signature... calltipParams = [] for param in signatures[0].params: calltipParams.append(param.description[len(param.type) + 1:]) calltip = signatures[0].name + '(' + ', '.join(calltipParams) + ')' self.__calltip = Calltip(self) self.__calltip.showCalltip(calltip, signatures[0].index) line = signatures[0].bracket_start[0] column = signatures[0].bracket_start[1] self.__callPosition = self.mapToAbsPosition(line - 1, column) def __resetCalltip(self): """Hides the calltip and resets how it was shown""" self.__calltipTimer.stop() if self.__calltip is not None: self.__calltip.hide() self.__calltip = None self.__callPosition = None def resizeCalltip(self): """Resizes the calltip if so""" if self.__calltip: self.__calltip.resize() def __onCalltipTimer(self): """Handles the calltip update timer""" if self.__calltip: if self.absCursorPosition < self.__callPosition: self.__resetCalltip() return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) signatures = getCallSignatures(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if not signatures: self.__resetCalltip() return line = signatures[0].bracket_start[0] column = signatures[0].bracket_start[1] callPosition = self.mapToAbsPosition(line - 1, column) if callPosition != self.__callPosition: self.__resetCalltip() else: # It is still the same call, check the commas self.__calltip.highlightParameter(signatures[0].index) def onOccurences(self): """The user requested a list of occurences""" if not self.isPythonBuffer(): return if self._parent.getType() == MainWindowTabWidgetBase.VCSAnnotateViewer: return if not os.path.isabs(self._parent.getFileName()): GlobalData().mainWindow.showStatusBarMessage( "Please save the buffer and try again") return fileName = self._parent.getFileName() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) definitions = getOccurrences(self, fileName) QApplication.restoreOverrideCursor() if len(definitions) == 0: GlobalData().mainWindow.showStatusBarMessage('No occurences found') return # There are found items GlobalData().mainWindow.showStatusBarMessage('') result = [] for definition in definitions: fName = definition.module_path if not fName: fName = fileName lineno = definition.line index = getSearchItemIndex(result, fName) if index < 0: widget = GlobalData().mainWindow.getWidgetForFileName(fName) if widget is None: uuid = "" else: uuid = widget.getUUID() newItem = ItemToSearchIn(fName, uuid) result.append(newItem) index = len(result) - 1 result[index].addMatch(definition.name, lineno) GlobalData().mainWindow.displayFindInFiles('', result) def insertCompletion(self, text): """Triggered when a completion is selected""" if text: currentWord = self.getCurrentWord() line, pos = self.cursorPosition prefixLength = len(self.__completionPrefix) if text != currentWord and text != self.__completionPrefix: with self: lineContent = self.lines[line] leftPart = lineContent[0:pos - prefixLength] rightPart = lineContent[pos:] self.lines[line] = leftPart + text + rightPart newPos = pos + len(text) - prefixLength self.cursorPosition = line, newPos self.__completionPrefix = '' self.__completer.hide() # The next time there is nothing to insert for sure self.__lastTabPosition = self.absCursorPosition def insertLines(self, text, line): """Inserts the given text into new lines starting from 1-based line""" toInsert = text.splitlines() with self: if line > 0: line -= 1 for item in toInsert: self.lines.insert(line, item) line += 1 def hideCompleter(self): """Hides the completer if visible""" self.__completer.hide() def clearPyflakesMessages(self): """Clears all the pyflakes markers""" self.getMargin('cdm_flakes_margin').clearPyflakesMessages() def setPyflakesMessages(self, messages): """Shows up a pyflakes messages""" self.getMargin('cdm_flakes_margin').setPyflakesMessages(messages) def highlightInCFlow(self): """Triggered when highlight in the control flow is requested""" if self.isPythonBuffer(): line, pos = self.cursorPosition absPos = self.absCursorPosition self.sigCFlowSyncRequested.emit(absPos, line + 1, pos + 1) def setDebugMode(self, debugOn, disableEditing): """Called to switch between debug/development""" skin = GlobalData().skin if debugOn: if disableEditing: self.setLinenoMarginBackgroundColor(skin['marginPaperDebug']) self.setLinenoMarginForegroundColor(skin['marginColorDebug']) self.setReadOnly(True) else: self.setLinenoMarginBackgroundColor(skin['marginPaper']) self.setLinenoMarginForegroundColor(skin['marginColor']) self.setReadOnly(False) bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: bpointMargin.setDebugMode(debugOn, disableEditing) def restoreBreakpoints(self): """Restores the breakpoints""" bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: bpointMargin.restoreBreakpoints() def isLineBreakable(self): """True if a line is breakable""" bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: return bpointMargin.isLineBreakable() return False def validateBreakpoints(self): """Checks breakpoints and deletes those which are invalid""" bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: bpointMargin.validateBreakpoints() def isPythonBuffer(self): """True if it is a python buffer""" return isPythonMime(self.mime) def setLinenoMarginBackgroundColor(self, color): """Sets the margins background""" linenoMargin = self.getMargin('cdm_line_number_margin') if linenoMargin: linenoMargin.setBackgroundColor(color) def setLinenoMarginForegroundColor(self, color): """Sets the lineno margin foreground color""" linenoMargin = self.getMargin('cdm_line_number_margin') if linenoMargin: linenoMargin.setForegroundColor(color) def terminate(self): """Overloaded version to pass the request to margins too""" for margin in self.getMargins(): if hasattr(margin, 'onClose'): margin.onClose() QutepartWrapper.terminate(self) def resizeEvent(self, event): """Resize the parent panels if required""" QutepartWrapper.resizeEvent(self, event) self.hideCompleter() if hasattr(self._parent, 'resizeBars'): self._parent.resizeBars()
class MDWidget(QWidget): """The MD rendered content widget which goes along with the text editor""" sigEscapePressed = pyqtSignal() def __init__(self, editor, parent): QWidget.__init__(self, parent) self.setVisible(False) self.__editor = editor self.__parentWidget = parent self.__connected = False hLayout = QHBoxLayout() hLayout.setContentsMargins(0, 0, 0, 0) hLayout.setSpacing(0) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) # Make pylint happy self.__toolbar = None self.__topBar = None # Create the update timer self.__updateTimer = QTimer(self) self.__updateTimer.setSingleShot(True) self.__updateTimer.timeout.connect(self.process) vLayout.addWidget(self.__createTopBar()) vLayout.addWidget(self.__createMDView()) hLayout.addLayout(vLayout) hLayout.addWidget(self.__createToolbar()) self.setLayout(hLayout) # Connect to the change file type signal self.__mainWindow = GlobalData().mainWindow editorsManager = self.__mainWindow.em editorsManager.sigFileTypeChanged.connect(self.__onFileTypeChanged) def __createToolbar(self): """Creates the toolbar""" self.__toolbar = QToolBar(self) self.__toolbar.setOrientation(Qt.Vertical) self.__toolbar.setMovable(False) self.__toolbar.setAllowedAreas(Qt.RightToolBarArea) self.__toolbar.setIconSize(QSize(16, 16)) self.__toolbar.setFixedWidth(30) self.__toolbar.setContentsMargins(0, 0, 0, 0) # Some control buttons could be added later self.printButton = QAction(getIcon('printer.png'), 'Print', self) self.printButton.triggered.connect(self.__onPrint) self.__toolbar.addAction(self.printButton) return self.__toolbar def __createTopBar(self): """Creates the top bar""" self.__topBar = MDTopBar(self) return self.__topBar def __createMDView(self): """Creates the graphics view""" self.mdView = MDViewer(self) self.mdView.sigEscapePressed.connect(self.__onEsc) return self.mdView def __onPrint(self): """Print the markdown page""" dialog = QPrintDialog(self) if dialog.exec_() == QDialog.Accepted: printer = dialog.printer() self.mdView.print_(printer) def __onEsc(self): """Triggered when Esc is pressed""" self.sigEscapePressed.emit() def process(self): """Parses the content and displays the results""" if not self.__connected: self.__connectEditorSignals() renderedText, errors, warnings = renderMarkdown( self.getUUID(), self.__editor.text, self.getFileName()) if errors: self.__topBar.updateInfoIcon(self.__topBar.STATE_BROKEN_UTD) self.__topBar.setErrors(errors) return if renderedText is None: self.__topBar.updateInfoIcon(self.__topBar.STATE_BROKEN_UTD) self.__topBar.setErrors(['Unknown markdown rendering error']) return # That will clear the error tooltip as well self.__topBar.updateInfoIcon(self.__topBar.STATE_OK_UTD) if warnings: self.__topBar.setWarnings(warnings) else: self.__topBar.clearWarnings() hsbValue, vsbValue = self.getScrollbarPositions() self.mdView.setHtml(renderedText) self.setScrollbarPositions(hsbValue, vsbValue) def __onFileTypeChanged(self, fileName, uuid, newFileType): """Triggered when a buffer content type has changed""" if self.getUUID() != uuid: return if not isMarkdownMime(newFileType): self.__disconnectEditorSignals() self.__updateTimer.stop() self.setVisible(False) self.__topBar.updateInfoIcon(self.__topBar.STATE_UNKNOWN) return # Update the bar and show it self.setVisible(True) self.process() # The buffer type change event comes when the content is loaded first # time. So this is a good point to restore the position _, _, _, hPos, vPos = getFilePosition(fileName) self.setScrollbarPositions(hPos, vPos) def terminate(self): """Called when a tab is to be closed""" self.mdView.terminate() self.mdView.deleteLater() if self.__updateTimer.isActive(): self.__updateTimer.stop() self.__updateTimer.deleteLater() self.__disconnectEditorSignals() editorsManager = self.__mainWindow.em editorsManager.sigFileTypeChanged.disconnect(self.__onFileTypeChanged) self.printButton.triggered.disconnect(self.__onPrint) self.printButton.deleteLater() self.__topBar.deleteLater() self.__toolbar.deleteLater() self.__editor = None self.__parentWidget = None def __connectEditorSignals(self): """When it is a python file - connect to the editor signals""" if not self.__connected: self.__editor.cursorPositionChanged.connect( self.__cursorPositionChanged) self.__editor.textChanged.connect(self.__onBufferChanged) self.__connected = True def __disconnectEditorSignals(self): """Disconnect the editor signals when the file is not a python one""" if self.__connected: self.__editor.cursorPositionChanged.disconnect( self.__cursorPositionChanged) self.__editor.textChanged.disconnect(self.__onBufferChanged) self.__connected = False def __cursorPositionChanged(self): """Cursor position changed""" # The timer should be reset only in case if the redrawing was delayed if self.__updateTimer.isActive(): self.__updateTimer.stop() self.__updateTimer.start(IDLE_TIMEOUT) def __onBufferChanged(self): """Triggered to update status icon and to restart the timer""" self.__updateTimer.stop() if self.__topBar.getCurrentState() in [ self.__topBar.STATE_OK_UTD, self.__topBar.STATE_OK_CHN, self.__topBar.STATE_UNKNOWN ]: self.__topBar.updateInfoIcon(self.__topBar.STATE_OK_CHN) else: self.__topBar.updateInfoIcon(self.__topBar.STATE_BROKEN_CHN) self.__updateTimer.start(IDLE_TIMEOUT) def redrawNow(self): """Redraw the diagram regardless of the timer""" if self.__updateTimer.isActive(): self.__updateTimer.stop() self.process() def getScrollbarPositions(self): """Provides the scrollbar positions""" return self.mdView.getScrollbarPositions() def setScrollbarPositions(self, hPos, vPos): """Sets the scrollbar positions for the view""" self.mdView.setScrollbarPositions(hPos, vPos) def getFileName(self): return self.__parentWidget.getFileName() def getUUID(self): return self.__parentWidget.getUUID()
class ProfileGraphViewer(QWidget): """Profiling results as a graph""" sigEscapePressed = pyqtSignal() def __init__(self, scriptName, params, reportTime, dataFile, stats, parent=None): QWidget.__init__(self, parent) self.__dataFile = dataFile self.__script = scriptName self.__reportTime = reportTime self.__params = params 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.__createLayout() self.__getDiagramLayout() self.__viewer.setScene(self.__scene) def setFocus(self): """Sets the focus properly""" self.__viewer.setFocus() def __isOutsideItem(self, fileName): """Detects if the record should be shown as an outside one""" return not fileName.startswith(self.__projectPrefix) def __createLayout(self): """Creates the widget layout""" 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 + " " + \ self.__params['arguments'] + "<br/>" \ "<b>Run at:</b> " + self.__reportTime + "<br/>" + \ str(totalCalls) + " function calls (" + \ str(totalPrimitiveCalls) + " primitive calls) in " + \ FLOAT_FORMAT % totalTime + " CPU seconds" summary = HeaderFitLabel(self) summary.setText(txt) summary.setToolTip(txt) summary.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) summary.setMinimumWidth(10) self.__scene = QGraphicsScene() self.__viewer = DiagramWidget() self.__viewer.sigEscapePressed.connect(self.__onESC) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) vLayout.addWidget(summary) vLayout.addWidget(self.__viewer) self.setLayout(vLayout) @staticmethod def __getDotFont(parts): """Provides a QFont object if a font spec is found""" for part in parts: if 'fontname=' in part: fontName = part.replace('fontname=', '') fontName = fontName.replace('[', '') fontName = fontName.replace(']', '') fontName = fontName.replace(',', '') return QFont(fontName) return None def __postprocessFullDotSpec(self, dotSpec): """Removes the arrow size, extracts tooltips, extracts font info""" nodeFont = None edgeFont = None tooltips = {} processed = [] for line in dotSpec.splitlines(): parts = line.split() lineModified = False if parts: if parts[0] == 'node': # need to extract the fontname nodeFont = self.__getDotFont(parts) elif parts[0] == 'edge': # need to extract the fontname edgeFont = self.__getDotFont(parts) elif parts[0].isdigit(): if parts[1] == '->': # certain edge spec: replace arrowsize and font size for index, part in enumerate(parts): if part.startswith('[arrowsize='): modified = parts[:] modified[index] = '[arrowsize="0.0",' processed.append(' '.join(modified)) elif part.startswith('fontsize='): size = float(part.split('"')[1]) if edgeFont: edgeFont.setPointSize(size) lineModified = True elif parts[1].startswith('['): # certain node spec: pick the tooltip and font size lineno = None for part in parts: if part.startswith('tooltip='): nodePath = part.split('"')[1] pathLine = nodePath + ':' + str(lineno) tooltips[int(parts[0])] = pathLine elif part.startswith('fontsize='): size = float(part.split('"')[1]) if nodeFont: nodeFont.setPointSize(size) elif part.startswith('label='): try: lineno = int(part.split(':')[1]) except: pass if not lineModified: processed.append(line) return '\n'.join(processed), tooltips, nodeFont, edgeFont def __rungprof2dot(self): """Runs gprof2dot which produces a full dot spec""" nodeLimit = Settings().getProfilerSettings().nodeLimit edgeLimit = Settings().getProfilerSettings().edgeLimit with io.StringIO() as buf: gprofParser = gprof2dot.PstatsParser(self.__dataFile) profileData = gprofParser.parse() profileData.prune(nodeLimit / 100.0, edgeLimit / 100.0, False, False) dot = gprof2dot.DotWriter(buf) dot.strip = False dot.wrap = False dot.graph(profileData, gprof2dot.TEMPERATURE_COLORMAP) output = buf.getvalue() return self.__postprocessFullDotSpec(output) def __getDiagramLayout(self): """Runs external tools to get the diagram layout""" fullDotSpec, tooltips, nodeFont, edgeFont = self.__rungprof2dot() dotProc = Popen(["dot", "-Tplain"], stdin=PIPE, stdout=PIPE, bufsize=1) graphDescr = dotProc.communicate( fullDotSpec.encode('utf-8'))[0].decode('utf-8') graph = getGraphFromPlainDotData(graphDescr) graph.normalize(self.physicalDpiX(), self.physicalDpiY()) self.__scene.clear() self.__scene.setSceneRect(0, 0, graph.width, graph.height) for edge in graph.edges: self.__scene.addItem(FuncConnection(edge)) if edge.label != "": self.__scene.addItem(FuncConnectionLabel(edge, edgeFont)) for node in graph.nodes: fileName = "" lineNumber = 0 try: nodeNameAsInt = int(node.name) if nodeNameAsInt in tooltips: parts = tooltips[nodeNameAsInt].rsplit(':', 1) fileName = parts[0] if parts[1].isdigit(): lineNumber = int(parts[1]) except: pass self.__scene.addItem( Function(node, fileName, lineNumber, self.__isOutsideItem(fileName), nodeFont)) def __onESC(self): """Triggered when ESC is clicked""" self.sigEscapePressed.emit() def onCopy(self): """Copies the diagram to the exchange buffer""" self.__viewer.onCopy() def onSaveAs(self, fileName): """Saves the diagram to a file""" self.__viewer.onSaveAs(fileName) def zoomIn(self): """Triggered on the 'zoom in' button""" self.__viewer.zoomIn() def zoomOut(self): """Triggered on the 'zoom out' button""" self.__viewer.zoomOut() def resetZoom(self): """Triggered on the 'zoom reset' button""" self.__viewer.resetZoom()
class ProfileResultsWidget(QWidget, MainWindowTabWidgetBase): """Profiling results widget""" sigEscapePressed = pyqtSignal() def __init__(self, scriptName, params, reportTime, dataFile, parent=None): MainWindowTabWidgetBase.__init__(self) QWidget.__init__(self, parent) # The same stats object is needed for both - a table and a graph # So, parse profile output once and then pass the object further stats = pstats.Stats(dataFile) stats.calc_callees() self.__profTable = ProfileTableViewer(scriptName, params, reportTime, dataFile, stats, self) self.__profGraph = ProfileGraphViewer(scriptName, params, reportTime, dataFile, stats, self) self.__profTable.hide() self.__profTable.sigEscapePressed.connect(self.__onEsc) self.__profGraph.sigEscapePressed.connect(self.__onEsc) self.__createLayout() def __createLayout(self): """Creates the toolbar and layout""" # Buttons self.__toggleViewButton = QAction(getIcon('tableview.png'), 'Switch to table view', self) self.__toggleViewButton.setCheckable(True) self.__toggleViewButton.toggled.connect(self.__switchView) self.__togglePathButton = QAction(getIcon('longpath.png'), 'Show full paths for item location', self) self.__togglePathButton.setCheckable(True) self.__togglePathButton.toggled.connect(self.__togglePath) self.__togglePathButton.setEnabled(False) self.__printButton = QAction(getIcon('printer.png'), 'Print', self) self.__printButton.triggered.connect(self.__onPrint) self.__printButton.setEnabled(False) self.__printPreviewButton = QAction(getIcon('printpreview.png'), 'Print preview', self) self.__printPreviewButton.triggered.connect(self.__onPrintPreview) self.__printPreviewButton.setEnabled(False) fixedSpacer = QWidget() fixedSpacer.setFixedHeight(16) self.__zoomInButton = QAction(getIcon('zoomin.png'), 'Zoom in (Ctrl+=)', self) self.__zoomInButton.setShortcut('Ctrl+=') self.__zoomInButton.triggered.connect(self.onZoomIn) self.__zoomOutButton = QAction(getIcon('zoomout.png'), 'Zoom out (Ctrl+-)', self) self.__zoomOutButton.setShortcut('Ctrl+-') self.__zoomOutButton.triggered.connect(self.onZoomOut) self.__zoomResetButton = QAction(getIcon('zoomreset.png'), 'Zoom reset (Ctrl+0)', self) self.__zoomResetButton.setShortcut('Ctrl+0') self.__zoomResetButton.triggered.connect(self.onZoomReset) # Toolbar toolbar = QToolBar(self) toolbar.setOrientation(Qt.Vertical) toolbar.setMovable(False) toolbar.setAllowedAreas(Qt.RightToolBarArea) toolbar.setIconSize(QSize(16, 16)) toolbar.setFixedWidth(28) toolbar.setContentsMargins(0, 0, 0, 0) toolbar.addAction(self.__toggleViewButton) toolbar.addAction(self.__togglePathButton) toolbar.addAction(self.__printPreviewButton) toolbar.addAction(self.__printButton) toolbar.addWidget(fixedSpacer) toolbar.addAction(self.__zoomInButton) toolbar.addAction(self.__zoomOutButton) toolbar.addAction(self.__zoomResetButton) hLayout = QHBoxLayout() hLayout.setContentsMargins(0, 0, 0, 0) hLayout.setSpacing(0) hLayout.addWidget(self.__profTable) hLayout.addWidget(self.__profGraph) hLayout.addWidget(toolbar) self.setLayout(hLayout) def setFocus(self): """Overriden setFocus""" if self.__profTable.isVisible(): self.__profTable.setFocus() else: self.__profGraph.setFocus() def __onEsc(self): """Triggered when Esc is pressed""" self.sigEscapePressed.emit() def __switchView(self, state): """Triggered when view is to be switched""" if state: self.__profGraph.hide() self.__profTable.show() self.__toggleViewButton.setIcon(getIcon('profdgmview.png')) self.__toggleViewButton.setToolTip('Switch to diagram view') self.__zoomInButton.setEnabled(False) self.__zoomOutButton.setEnabled(False) self.__zoomResetButton.setEnabled(False) self.__togglePathButton.setEnabled(True) self.__profTable.setFocus() else: self.__profTable.hide() self.__profGraph.show() self.__toggleViewButton.setIcon(getIcon('tableview.png')) self.__toggleViewButton.setToolTip('Switch to table view') self.__zoomInButton.setEnabled(True) self.__zoomOutButton.setEnabled(True) self.__zoomResetButton.setEnabled(True) self.__togglePathButton.setEnabled(False) self.__profGraph.setFocus() def __togglePath(self, state): """Triggered when full path/file name is switched""" self.__profTable.togglePath(state) if state: fName = 'shortpath.png' tip = 'Show file names only for item location' else: fName = 'longpath.png' tip = 'Show full paths for item location' self.__togglePathButton.setIcon(getIcon(fName)) self.__togglePathButton.setToolTip(tip) def __onPrint(self): """Triggered on the 'print' button""" pass def __onPrintPreview(self): """Triggered on the 'print preview' button""" pass def isZoomApplicable(self): """Should the zoom menu items be available""" return self.__profGraph.isVisible() def onZoomIn(self): """Triggered on the 'zoom in' button""" if self.__profGraph.isVisible(): self.__profGraph.zoomIn() def onZoomOut(self): """Triggered on the 'zoom out' button""" if self.__profGraph.isVisible(): self.__profGraph.zoomOut() def onZoomReset(self): """Triggered on the 'zoom reset' button""" if self.__profGraph.isVisible(): self.__profGraph.resetZoom() def isCopyAvailable(self): """Tells id the main menu copy item should be switched on""" return self.__profGraph.isVisible() def isDiagramActive(self): """Tells if the diagram is active""" return self.__profGraph.isVisible() def onCopy(self): """Ctrl+C triggered""" if self.__profGraph.isVisible(): self.__profGraph.onCopy() def onSaveAs(self, fileName): """Saves the diagram into a file""" if self.__profGraph.isVisible(): self.__profGraph.onSaveAs(fileName) else: self.__profTable.onSaveAs(fileName) # Mandatory interface part is below def isModified(self): """Tells if the file is modified""" return False def getRWMode(self): """Tells if the file is read only""" return "RO" def getType(self): """Tells the widget type""" return MainWindowTabWidgetBase.ProfileViewer def getLanguage(self): """Tells the content language""" return "Profiler" def setFileName(self, name): """Sets the file name - not applicable""" raise Exception("Setting a file name for profile results " "is not applicable") def setEncoding(self, newEncoding): """Not applicable for the profiler results viewer""" return def getShortName(self): """Tells the display name""" return "Profiling results" def setShortName(self, name): """Sets the display name - not applicable""" raise Exception("Setting a file name for profiler " "results is not applicable")
class BreakPointModel(QAbstractItemModel): """Class implementing a custom model for breakpoints""" sigDataAboutToBeChanged = pyqtSignal(QModelIndex, QModelIndex) sigBreakpoinsChanged = pyqtSignal() def __init__(self, parent=None): QAbstractItemModel.__init__(self, parent) self.breakpoints = [] self.__fields = { COLUMN_LOCATION: ['File:line', Qt.Alignment(Qt.AlignLeft)], COLUMN_CONDITION: ['Condition', Qt.Alignment(Qt.AlignLeft)], COLUMN_TEMPORARY: ['T', Qt.Alignment(Qt.AlignHCenter)], COLUMN_ENABLED: ['E', Qt.Alignment(Qt.AlignHCenter)], COLUMN_IGNORE_COUNT: ['Ignore Count', Qt.Alignment(Qt.AlignRight)] } self.__columnCount = len(self.__fields) def columnCount(self, parent=None): """Provides the current column count""" return self.__columnCount def rowCount(self, parent=None): """Provides the current row count""" # we do not have a tree, parent should always be invalid if parent is None or not parent.isValid(): return len(self.breakpoints) return 0 def data(self, index, role=Qt.DisplayRole): """Provides the requested data""" if not index.isValid(): return None column = index.column() row = index.row() if role == Qt.DisplayRole: if column == COLUMN_LOCATION: return self.breakpoints[row].getLocation() if column == COLUMN_CONDITION: return self.breakpoints[row].getCondition() if column == COLUMN_IGNORE_COUNT: return self.breakpoints[row].getIgnoreCount() elif role == Qt.CheckStateRole: if column == COLUMN_TEMPORARY: return self.breakpoints[row].isTemporary() if column == COLUMN_ENABLED: return self.breakpoints[row].isEnabled() elif role == Qt.ToolTipRole: if column < self.__columnCount: return self.breakpoints[row].getTooltip() elif role == Qt.TextAlignmentRole: if column < self.__columnCount: return self.__fields[column][1] return None def setData(self, index, _, role=Qt.EditRole): """Change data in the model""" if index.isValid(): if role == Qt.CheckStateRole: column = index.column() if column in [COLUMN_TEMPORARY, COLUMN_ENABLED]: # Flip the boolean row = index.row() bp = self.breakpoints[row] self.sigDataAboutToBeChanged.emit(index, index) if column == COLUMN_TEMPORARY: bp.setTemporary(not bp.isTemporary()) else: bp.setEnabled(not bp.isEnabled()) self.dataChanged.emit(index, index) self.sigBreakpoinsChanged.emit() return True return False def flags(self, index): """Provides the item flags""" if not index.isValid(): return Qt.ItemIsEnabled column = index.column() if column in [COLUMN_TEMPORARY, COLUMN_ENABLED]: return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable return Qt.ItemIsEnabled | Qt.ItemIsSelectable def headerData(self, section, orientation, role=Qt.DisplayRole): """Provides header data""" if orientation == Qt.Horizontal and role == Qt.DisplayRole: if section < self.__columnCount: return self.__fields[section][0] return "" return None def index(self, row, column, parent=None): """Creates an index""" if (parent and parent.isValid()) or \ row < 0 or row >= len(self.breakpoints) or \ column < 0 or column >= self.__columnCount: return QModelIndex() return self.createIndex(row, column, self.breakpoints[row]) def parent(self, index): """Provides the parent index""" return QModelIndex() def hasChildren(self, parent=None): """Checks if there are child items""" if parent is None or not parent.isValid(): return len(self.breakpoints) > 0 return False def addBreakpoint(self, bpoint): """Adds a new breakpoint to the list""" cnt = len(self.breakpoints) self.beginInsertRows(QModelIndex(), cnt, cnt) self.breakpoints.append(bpoint) self.endInsertRows() self.sigBreakpoinsChanged.emit() def setBreakPointByIndex(self, index, bpoint): """Set the values of a breakpoint given by index""" if index.isValid(): row = index.row() index1 = self.createIndex(row, 0, self.breakpoints[row]) index2 = self.createIndex(row, self.__columnCount - 1, self.breakpoints[row]) self.sigDataAboutToBeChanged.emit(index1, index2) self.breakpoints[row].update(bpoint) self.dataChanged.emit(index1, index2) self.sigBreakpoinsChanged.emit() def updateLineNumberByIndex(self, index, newLineNumber): """Update the line number by index""" if index.isValid(): row = index.row() index1 = self.createIndex(row, 0, self.breakpoints[row]) index2 = self.createIndex(row, self.__columnCount - 1, self.breakpoints[row]) self.sigDataAboutToBeChanged.emit(index1, index2) self.breakpoints[row].updateLineNumber(newLineNumber) self.dataChanged.emit(index1, index2) self.sigBreakpoinsChanged.emit() def setBreakPointEnabledByIndex(self, index, enabled): """Sets the enable state""" if index.isValid(): row = index.row() index1 = self.createIndex(row, 0, self.breakpoints[row]) index2 = self.createIndex(row, self.__columnCount - 1, self.breakpoints[row]) self.sigDataAboutToBeChanged.emit(index1, index2) self.breakpoints[row].setEnabled(enabled) self.dataChanged.emit(index1, index2) self.sigBreakpoinsChanged.emit() def deleteBreakPointByIndex(self, index): """Deletes the breakpoint by its index""" if index.isValid(): row = index.row() self.beginRemoveRows(QModelIndex(), row, row) del self.breakpoints[row] self.endRemoveRows() self.sigBreakpoinsChanged.emit() def deleteBreakPoints(self, idxList): """Deletes a list of breakpoints""" rows = [] for index in idxList: if index.isValid(): rows.append(index.row()) rows.sort(reverse=True) for row in rows: self.beginRemoveRows(QModelIndex(), row, row) del self.breakpoints[row] self.endRemoveRows() self.sigBreakpoinsChanged.emit() def deleteAll(self): """Deletes all breakpoints""" if self.breakpoints: self.beginRemoveRows(QModelIndex(), 0, len(self.breakpoints) - 1) self.breakpoints = [] self.endRemoveRows() self.sigBreakpoinsChanged.emit() def getBreakPointByIndex(self, index): """Provides a breakpoint by index""" if index.isValid(): return self.breakpoints[index.row()] return None def getBreakPointIndex(self, fname, lineno): """Provides an index of a breakpoint""" for row in range(len(self.breakpoints)): bpoint = self.breakpoints[row] if bpoint.getAbsoluteFileName() == fname and \ bpoint.getLineNumber() == lineno: return self.createIndex(row, 0, self.breakpoints[row]) return QModelIndex() def isBreakPointTemporaryByIndex(self, index): """Checks if a breakpoint given by it's index is temporary""" if index.isValid(): return self.breakpoints[index.row()].isTemporary() return False def getCounts(self): """Provides enable/disable counters""" enableCount = 0 disableCount = 0 for bp in self.breakpoints: if bp.isEnabled(): enableCount += 1 else: disableCount += 1 return enableCount, disableCount def serialize(self): """Provides a list of serialized breakpoints""" result = [] for bp in self.breakpoints: result.append(bp.serialize()) return result
class RunManager(QObject): """Manages the external running processes""" # script path, output file, start time, finish time, redirected sigProfilingResults = pyqtSignal(str, str, str, str, bool) sigDebugSessionPrologueStarted = pyqtSignal(object, str, object, object) sigIncomingMessage = pyqtSignal(str, str, object) sigProcessFinished = pyqtSignal(str, int) def __init__(self, mainWindow): QObject.__init__(self) self.__mainWindow = mainWindow self.__processes = [] self.__prologueProcesses = [] self.__tcpServer = QTcpServer() self.__tcpServer.newConnection.connect(self.__newConnection) self.__tcpServer.listen(QHostAddress.LocalHost) self.__waitTimer = QTimer(self) self.__waitTimer.setSingleShot(True) self.__waitTimer.timeout.connect(self.__onWaitTimer) self.__prologueTimer = QTimer(self) self.__prologueTimer.setSingleShot(True) self.__prologueTimer.timeout.connect(self.__onPrologueTimer) def __newConnection(self): """Handles new incoming connections""" clientSocket = self.__tcpServer.nextPendingConnection() clientSocket.setSocketOption(QAbstractSocket.KeepAliveOption, 1) clientSocket.setSocketOption(QAbstractSocket.LowDelayOption, 1) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: self.__waitForHandshake(clientSocket) except: QApplication.restoreOverrideCursor() raise QApplication.restoreOverrideCursor() def __waitForHandshake(self, clientSocket): """Waits for the message with the proc ID""" if clientSocket.waitForReadyRead(1000): try: method, procuuid, params, jsonStr = getParsedJSONMessage( clientSocket) if IDE_DEBUG: print("Run manager (wait for handshake) received: " + str(jsonStr)) if method != METHOD_PROC_ID_INFO: logging.error('Unexpected message at the handshake stage. ' 'Expected: ' + METHOD_PROC_ID_INFO + '. Received: ' + str(method)) self.__safeSocketClose(clientSocket) return None except (TypeError, ValueError) as exc: self.__mainWindow.showStatusBarMessage( 'Unsolicited connection to the RunManager. Ignoring...') self.__safeSocketClose(clientSocket) return None procIndex = self.__getProcessIndex(procuuid) if procIndex is not None: self.__onProcessStarted(procuuid) self.__processes[procIndex].procWrapper.setSocket(clientSocket) return params @staticmethod def __safeSocketClose(clientSocket): """No exception socket close""" try: clientSocket.close() except Exception as exc: logging.error('Run manager safe socket close: ' + str(exc)) def __pickWidget(self, procuuid, kind): """Picks the widget for a process""" consoleReuse = Settings()['ioconsolereuse'] if consoleReuse == NO_REUSE: widget = IOConsoleWidget(procuuid, kind) self.__mainWindow.addIOConsole(widget, kind) return widget widget = None consoles = self.__mainWindow.getIOConsoles() for console in consoles: if console.kind == kind: procIndex = self.__getProcessIndex(console.procuuid) if procIndex is None: widget = console widget.onReuse(procuuid) self.__mainWindow.onReuseConsole(widget, kind) if consoleReuse == CLEAR_AND_REUSE: widget.clear() break if widget is None: widget = IOConsoleWidget(procuuid, kind) self.__mainWindow.addIOConsole(widget, kind) return widget def __updateParameters(self, path, kind): """Displays the dialog and updates the parameters if needed""" params = getRunParameters(path) profilerParams = None debuggerParams = None if kind == PROFILE: profilerParams = Settings().getProfilerSettings() elif kind == DEBUG: debuggerParams = Settings().getDebuggerSettings() dlg = RunDialog(path, params, profilerParams, debuggerParams, kind, self.__mainWindow) if dlg.exec_() == QDialog.Accepted: addRunParams(path, dlg.runParams) if kind == PROFILE: if dlg.profilerParams != profilerParams: Settings().setProfilerSettings(dlg.profilerParams) elif kind == DEBUG: if dlg.debuggerParams != debuggerParams: Settings().setDebuggerSettings(dlg.debuggerParams) return True return False def __prepareRemoteProcess(self, path, kind): """Prepares the data structures to start a remote proces""" redirected = getRunParameters(path)['redirected'] remoteProc = RemoteProcess() remoteProc.kind = kind remoteProc.procWrapper = RemoteProcessWrapper( path, self.__tcpServer.serverPort(), redirected, kind) remoteProc.procWrapper.state = STATE_PROLOGUE if redirected or kind == DEBUG: self.__prologueProcesses.append( (remoteProc.procWrapper.procuuid, time.time())) if not self.__prologueTimer.isActive(): self.__prologueTimer.start(1000) if redirected: remoteProc.widget = self.__pickWidget( remoteProc.procWrapper.procuuid, kind) remoteProc.widget.appendIDEMessage('Starting script ' + path + '...') remoteProc.procWrapper.sigClientStdout.connect( remoteProc.widget.appendStdoutMessage) remoteProc.procWrapper.sigClientStderr.connect( remoteProc.widget.appendStderrMessage) remoteProc.procWrapper.sigClientInput.connect( remoteProc.widget.input) remoteProc.widget.sigUserInput.connect(self.__onUserInput) remoteProc.procWrapper.sigFinished.connect(self.__onProcessFinished) remoteProc.procWrapper.sigIncomingMessage.connect( self.__onIncomingMessage) self.__processes.append(remoteProc) return remoteProc def run(self, path, needDialog): """Runs the given script regardless if it is redirected""" if needDialog: if not self.__updateParameters(path, RUN): return remoteProc = self.__prepareRemoteProcess(path, RUN) try: remoteProc.procWrapper.start() if not remoteProc.procWrapper.redirected: remoteProc.procWrapper.startTime = datetime.now() if not self.__waitTimer.isActive(): self.__waitTimer.start(1000) except Exception as exc: self.__onProcessFinished(remoteProc.procWrapper.procuuid, FAILED_TO_START) logging.error(str(exc)) def profile(self, path, needDialog): """Profiles the given script regardless if it is redirected""" if needDialog: if not self.__updateParameters(path, PROFILE): return remoteProc = self.__prepareRemoteProcess(path, PROFILE) try: remoteProc.procWrapper.start() if not remoteProc.procWrapper.redirected: remoteProc.procWrapper.startTime = datetime.now() if not self.__waitTimer.isActive(): self.__waitTimer.start(1000) except Exception as exc: self.__onProcessFinished(remoteProc.procWrapper.procuuid, FAILED_TO_START) logging.error(str(exc)) def debug(self, path, needDialog): """Debugs the given script regardless if it is redirected""" if needDialog: if not self.__updateParameters(path, DEBUG): return remoteProc = self.__prepareRemoteProcess(path, DEBUG) # The run parameters could be changed by another run after the # debugging has started so they need to be saved per session self.sigDebugSessionPrologueStarted.emit( remoteProc.procWrapper, path, getRunParameters(path), Settings().getDebuggerSettings()) try: remoteProc.procWrapper.start() if not remoteProc.procWrapper.redirected: remoteProc.procWrapper.startTime = datetime.now() if not self.__waitTimer.isActive(): self.__waitTimer.start(1000) except Exception as exc: self.__onProcessFinished(remoteProc.procWrapper.procuuid, FAILED_TO_START) logging.error(str(exc)) def killAll(self): """Kills all the processes if needed""" index = len(self.__processes) - 1 while index >= 0: item = self.__processes[index] if item.procWrapper.redirected: item.procWrapper.stop() index -= 1 # Wait till all the processes stopped count = self.__getDetachedCount() while count > 0: time.sleep(0.01) QApplication.processEvents() count = self.__getDetachedCount() def __getDetachedCount(self): """Return the number of detached processes still running""" count = 0 index = len(self.__processes) - 1 while index >= 0: if self.__processes[index].procWrapper.redirected: count += 1 index -= 1 return count def kill(self, procuuid): """Kills a single process""" index = self.__getProcessIndex(procuuid) if index is None: return item = self.__processes[index] if not item.procWrapper.redirected: return item.procWrapper.stop() def __getProcessIndex(self, procuuid): """Returns a process index in the list""" for index, item in enumerate(self.__processes): if item.procWrapper.procuuid == procuuid: return index return None def __onProcessFinished(self, procuuid, retCode): """Triggered when a redirected process has finished""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] item.procWrapper.finishTime = datetime.now() needProfileSignal = False if retCode == KILLED: msg = "Script killed" tooltip = "killed" elif retCode == DISCONNECTED: msg = "Connection lost to the script process" tooltip = "connection lost" elif retCode == FAILED_TO_START: msg = "Script failed to start" tooltip = "failed to start" elif retCode == STOPPED_BY_REQUEST: # Debugging only: user clicked 'stop' msg = "Script finished by the user request" tooltip = "stopped by user" item.procWrapper.wait() elif retCode == UNHANDLED_EXCEPTION: # Debugging only: unhandled exception msg = "Script finished due to an unhandled exception" tooltip = "unhandled exception" item.procWrapper.wait() elif retCode == SYNTAX_ERROR_AT_START: msg = "Failure to run due to a syntax error" tooltip = "syntax error" item.procWrapper.wait() else: msg = "Script finished with exit code " + str(retCode) tooltip = "finished, exit code " + str(retCode) item.procWrapper.wait() if item.kind == PROFILE: needProfileSignal = True if item.widget: item.widget.scriptFinished() item.widget.appendIDEMessage(msg) self.__mainWindow.updateIOConsoleTooltip(procuuid, tooltip) self.__mainWindow.onConsoleFinished(item.widget) item.widget.sigUserInput.disconnect(self.__onUserInput) if needProfileSignal: self.__sendProfileResultsSignal(item.procWrapper) del self.__processes[index] self.sigProcessFinished.emit(procuuid, retCode) def __onProcessStarted(self, procuuid): """Triggered when a process has started""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] if item.widget: msg = item.widget.appendIDEMessage('Script started') item.procWrapper.startTime = msg.timestamp def __onUserInput(self, procuuid, userInput): """Triggered when the user input is collected""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] if item.procWrapper.redirected: item.procWrapper.userInput(userInput) def __onWaitTimer(self): """Triggered when the timer fired""" needNewTimer = False index = len(self.__processes) - 1 while index >= 0: item = self.__processes[index] if not item.procWrapper.redirected: if item.procWrapper.waitDetached(): item.procWrapper.finishTime = datetime.now() if item.procWrapper.kind == PROFILE: self.__sendProfileResultsSignal(item.procWrapper) del self.__processes[index] else: needNewTimer = True index -= 1 if needNewTimer: self.__waitTimer.start(1000) def __sendProfileResultsSignal(self, procWrapper): """Sends a signal to tell that the results are available""" self.sigProfilingResults.emit( procWrapper.path, GlobalData().getProfileOutputPath(procWrapper.procuuid), printableTimestamp(procWrapper.startTime), printableTimestamp(procWrapper.finishTime), procWrapper.redirected) def __onIncomingMessage(self, procuuid, method, params): """Handles a debugger incoming message""" if IDE_DEBUG: print('Debugger message from ' + procuuid + ' Method: ' + method + ' Prameters: ' + repr(params)) self.sigIncomingMessage.emit(procuuid, method, params) def __onPrologueTimer(self): """Triggered when a prologue phase controlling timer fired""" needNewTimer = False index = len(self.__prologueProcesses) - 1 while index >= 0: procuuid, startTime = self.__prologueProcesses[index] procIndex = self.__getProcessIndex(procuuid) if procIndex is None: # No such process anymore del self.__prologueProcesses[index] else: item = self.__processes[procIndex] if item.procWrapper.state != STATE_PROLOGUE: # The state has been changed del self.__prologueProcesses[index] else: if time.time() - startTime > HANDSHAKE_TIMEOUT: # Waited too long item.widget.appendIDEMessage( 'Timeout: the process did not start; ' 'killing the process.') item.procWrapper.stop() else: needNewTimer = True index -= 1 if needNewTimer: self.__prologueTimer.start(1000) def appendIDEMessage(self, procuuid, message): """Appends a message to the appropriate IO window""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] if item.widget: item.widget.appendIDEMessage(message) return logging.error(message)
class DisassemblyView(QWidget): sigGotoLine = pyqtSignal(int, int) sigEscapePressed = pyqtSignal() def __init__(self, navBar, parent): QWidget.__init__(self, parent) self.__navBar = navBar self.__table = DisassemblyTreeWidget(self) self.__table.sigEscapePressed.connect(self.__onEsc) self.__table.itemActivated.connect(self.__activated) self.__table.itemSelectionChanged.connect(self.__selectionChanged) self.__summary = HeaderLabel(parent=self) self.__summary.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) self.__summary.setMinimumWidth(10) self.__summary.setVisible(False) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) vLayout.addWidget(self.__summary) vLayout.addWidget(self.__table) self.setLayout(vLayout) def serializeScrollAndSelection(self): """Memorizes the selection and expanded items""" # Scroll self.__hScroll = self.__table.horizontalScrollBar().value() self.__vScroll = self.__table.verticalScrollBar().value() # Collapsed self.__collapsed = [] for index in range(self.__table.topLevelItemCount()): item = self.__table.topLevelItem(index) if not item.isExpanded(): name = item.text(0) if '(' in name: name = name.split('(')[0].strip() self.__collapsed.append(name) # Selection # - top level item # - non-top empty # - non-top something selected = self.__table.selectedItems() if len(selected) != 1: self.__selectedParent = None self.__selected = None self.__selectedIndex = None else: selected = selected[0] self.__selectedParent = selected.parent() if self.__selectedParent is None: # Top level selected self.__selected = selected.text(0) if '(' in self.__selected: self.__selected = self.__selected.split('(')[0].strip() self.__selectedIndex = None else: # Non-top level self.__selectedIndex = self.__selectedParent.indexOfChild( selected) self.__selectedParent = self.__selectedParent.text(0) if '(' in self.__selectedParent: self.__selectedParent = self.__selectedParent.split( '(')[0].strip() self.__selected = (selected.text(0), selected.text(1), selected.text(2), selected.text(3), selected.text(4), selected.text(5)) def restoreScrollAndSelection(self): """Restores the selection and scroll position""" # Selection if (self.__selectedParent is not None or self.__selected is not None or self.__selectedIndex is not None): # Need to restore the selection if self.__selectedParent is None: # Top level was selected topItem = self.__findTopLevel(self.__selected) if topItem is not None: topItem.setSelected(True) else: # Non-top item was selected topItem = self.__findTopLevel(self.__selectedParent) if topItem is not None: maxIndex = topItem.childCount() - 1 if self.__selectedIndex <= maxIndex: item = topItem.child(self.__selectedIndex) if (item.text(0) == self.__selected[0] and item.text(1) == self.__selected[1] and item.text(2) == self.__selected[2] and item.text(3) == self.__selected[3] and item.text(4) == self.__selected[4] and item.text(5) == self.__selected[5]): item.setSelected(True) # Collapsed for index in range(self.__table.topLevelItemCount()): item = self.__table.topLevelItem(index) title = item.text(0) if '(' in title: title = title.split('(')[0].strip() if title in self.__collapsed: item.setExpanded(False) # Scroll self.__table.horizontalScrollBar().setValue(self.__hScroll) self.__table.verticalScrollBar().setValue(self.__vScroll) def __findTopLevel(self, name): """Provides a reference to the top level item if found""" for index in range(self.__table.topLevelItemCount()): item = self.__table.topLevelItem(index) title = item.text(0) if title == name: return item if title.startswith(name + ' ('): return item return None def populateDisassembly(self, source, encoding, filename): """Populates the disassembly tree""" self.__navBar.clearWarnings() self.serializeScrollAndSelection() try: optLevel = Settings()['disasmLevel'] if source is None: props, disassembly = getFileDisassembled(filename, optLevel, stringify=False) else: props, disassembly = getBufferDisassembled(source, encoding, filename, optLevel, stringify=False) self.__table.clear() self.__setupLabel(props) self.__populate(disassembly) self.__table.header().resizeSections(QHeaderView.ResizeToContents) self.__navBar.updateInfoIcon(self.__navBar.STATE_OK_UTD) self.restoreScrollAndSelection() except Exception as exc: self.__navBar.updateInfoIcon(self.__navBar.STATE_BROKEN_UTD) self.__navBar.setErrors('Disassembling error:\n' + str(exc)) def __setupLabel(self, props): """Updates the property label""" txt = '' for item in props: if txt: txt += '<br/>' txt += '<b>' + item[0] + ':</b> ' + item[1] self.__summary.setText(txt) self.__summary.setToolTip(txt) self.__summary.setVisible(True) def __populate(self, disassembly): """Populates disassembly""" currentTopLevel = None emptyCount = 0 for line in disassembly.splitlines(): if line.lower().startswith('disassembly of'): line = line.strip() emptyCount = 0 # Two options: # Disassembly of <code object optToString at 0x7f63b7bf9920, file "...", line 45>: # Disassembly of optToString: if line.endswith(':'): line = line[:-1] if '<' in line and '>' in line: # First option begin = line.find('code object ') + len('code object ') end = line.find(' at 0x') name = line[begin:end] begin = line.find(', line ') + len(', line ') lineNo = line[begin:-1] currentTopLevel = QTreeWidgetItem( [name + ' (' + lineNo + ')']) else: # Second option currentTopLevel = QTreeWidgetItem([line.split()[-1]]) self.__table.addTopLevelItem(currentTopLevel) continue if currentTopLevel is None: continue if not line.strip(): emptyCount += 1 continue # Here: not an empty line and there is a parent # so parse and add as a child while emptyCount > 0: currentTopLevel.addChild(QTreeWidgetItem([])) emptyCount -= 1 # Line numbers may occupy more than 3 positions so the first # part is taken with a good margin parts = line.split() if '>>' in parts: jump = '>>' parts.remove('>>') else: jump = '' if '-->' in parts: parts.remove('-->') if parts[0].isdigit() and parts[1].isdigit(): # Line number and address lineNo = parts.pop(0) else: # Only adderess lineNo = '' address = parts.pop(0) instruction = parts.pop(0) if parts: argument = parts.pop(0) else: argument = '' if parts: interpretation = ' '.join(parts) else: interpretation = '' currentTopLevel.addChild( QTreeWidgetItem([ lineNo, jump, address, instruction, argument, interpretation ])) self.__table.expandItem(currentTopLevel) def __selectionChanged(self): """Handles an AST item selection""" selected = list(self.__table.selectedItems()) self.__navBar.setSelectionLabel(len(selected), None) if selected: if len(selected) == 1: self.__navBar.setPath(self.__getPath(selected[0])) return self.__navBar.setPath('') @staticmethod def __getPath(node): if node.parent() is None: return node.text(0) instruction = node.text(3) if not instruction: instruction = '?' return node.parent().text(0) + u' \u2192 ' + instruction def __activated(self, item, _): """Handles the double click (or Enter) on an AST item""" if item.parent() is None: # Top level item title = item.text(0) if '(' in title: lineNo = title.split('(')[1] lineNo = lineNo.split(')')[0] if lineNo.isdigit(): self.sigGotoLine.emit(int(lineNo), 1) return # Search in the children for index in range(item.childCount()): lineNo = item.child(index).text(0) if lineNo.isdigit(): self.sigGotoLine.emit(int(lineNo), 1) return return parent = item.parent() itemIndex = parent.indexOfChild(item) for index in range(itemIndex, -1, -1): lineNo = parent.child(index).text(0) if lineNo.isdigit(): self.sigGotoLine.emit(int(lineNo), 1) return def __onEsc(self): """Triggered when Esc is pressed""" self.sigEscapePressed.emit()
class CodimensionDebugger(QObject): """Debugger server implementation""" sigDebuggerStateChanged = pyqtSignal(int) sigClientLine = pyqtSignal(str, int, bool) sigClientException = pyqtSignal(str, str, list, bool) sigClientSyntaxError = pyqtSignal(str, str, str, int, int) sigClientStack = pyqtSignal(list) sigClientThreadList = pyqtSignal(int, list) sigClientVariables = pyqtSignal(int, list) sigClientVariable = pyqtSignal(int, list) sigClientThreadSet = pyqtSignal() sigClientClearBreak = pyqtSignal(str, int) sigClientBreakConditionError = pyqtSignal(str, int) sigClientCallTrace = pyqtSignal(bool, str, int, str, str, int, str) STATE_STOPPED = 0 STATE_IN_CLIENT = 1 STATE_IN_IDE = 2 def __init__(self, mainWindow): QObject.__init__(self) # To control the user interface elements self.__mainWindow = mainWindow self.__state = self.STATE_STOPPED self.__stopAtFirstLine = None self.__procWrapper = None self.__procuuid = None self.__fileName = None self.__runParameters = None self.__debugSettings = None self.__breakpointModel = BreakPointModel(self) self.__watchpointModel = WatchPointModel(self) self.__breakpointModel.rowsAboutToBeRemoved.connect( self.__deleteBreakPoints) self.__breakpointModel.sigDataAboutToBeChanged.connect( self.__breakPointDataAboutToBeChanged) self.__breakpointModel.dataChanged.connect(self.__changeBreakPoints) self.__breakpointModel.rowsInserted.connect(self.__addBreakPoints) self.sigClientClearBreak.connect(self.__clientClearBreakPoint) self.sigClientBreakConditionError.connect( self.__clientBreakConditionError) self.__handlers = {} self.__initHandlers() def __initHandlers(self): """Initializes the incoming messages handlers""" self.__handlers = { METHOD_LINE: self.__handleLine, METHOD_STACK: self.__handleStack, METHOD_THREAD_LIST: self.__handleThreadList, METHOD_VARIABLES: self.__handleVariables, METHOD_DEBUG_STARTUP: self.__handleStartup, METHOD_FORK_TO: self.__handleForkTo, METHOD_CLEAR_BP: self.__handleClearBP, METHOD_SYNTAX_ERROR: self.__handleSyntaxError, METHOD_VARIABLE: self.__handleVariable, METHOD_BP_CONDITION_ERROR: self.__handleBPConditionError, METHOD_EXCEPTION: self.__handleException, METHOD_CALL_TRACE: self.__handleCallTrace, METHOD_EXEC_STATEMENT_ERROR: self.__handleExecStatementError, METHOD_EXEC_STATEMENT_OUTPUT: self.__handleExecuteStatementOutput, METHOD_SIGNAL: self.__handleSignal, METHOD_THREAD_SET: self.__handleThreadSet } def getScriptPath(self): """Provides the path to the debugged script""" return self.__fileName def getState(self): """Provides the debugger state""" return self.__state def getRunDebugParameters(self): """Provides the running and debugging parameters""" return self.__runParameters, self.__debugSettings def getBreakPointModel(self): """Provides a reference to the breakpoints model""" return self.__breakpointModel def getWatchPointModel(self): """Provides a reference to the watch points model""" return self.__watchpointModel def __changeDebuggerState(self, newState): """Changes the debugger state""" if newState != self.__state: self.__state = newState self.sigDebuggerStateChanged.emit(newState) def onDebugSessionStarted(self, procWrapper, fileName, runParameters, debugSettings): """Starts debugging a script. Run manager informs about it.""" if self.__state != self.STATE_STOPPED: raise Exception('Logic error. Debugging session started while the ' 'previous one has not finished.') self.__procWrapper = procWrapper self.__procuuid = procWrapper.procuuid self.__fileName = fileName self.__runParameters = runParameters self.__debugSettings = debugSettings self.__stopAtFirstLine = debugSettings.stopAtFirstLine self.__mainWindow.switchDebugMode(True) self.__changeDebuggerState(self.STATE_IN_CLIENT) def onIncomingMessage(self, procuuid, method, params): """Message from the debuggee has been received""" if self.__procuuid == procuuid: try: self.__handlers[method](params) except KeyError: logging.error('Unhandled message received by the debugger. ' 'Method: ' + str(method) + ' Parameters: ' + repr(params)) def __handleLine(self, params): """Handles METHOD_LINE""" stack = params['stack'] if self.__stopAtFirstLine: topFrame = stack[0] self.sigClientLine.emit(topFrame[0], int(topFrame[1]), False) self.sigClientStack.emit(stack) else: self.__stopAtFirstLine = True QTimer.singleShot(0, self.remoteContinue) self.__changeDebuggerState(self.STATE_IN_IDE) def __handleStack(self, params): """Handles METHOD_STACK""" stack = params['stack'] if self.__stopAtFirstLine: topFrame = stack[0] self.sigClientLine.emit(topFrame[0], int(topFrame[1]), True) self.sigClientStack.emit(stack) else: self.__stopAtFirstLine = True QTimer.singleShot(0, self.remoteContinue) def __handleThreadList(self, params): """Handles METHOD_THREAD_LIST""" self.sigClientThreadList.emit(params['currentID'], params['threadList']) def __handleVariables(self, params): """Handles METHOD_VARIABLES""" self.sigClientVariables.emit(params['scope'], params['variables']) def __handleStartup(self, params): """Handles METHOD_DEBUG_STARTUP""" del params # unused argument self.__sendBreakpoints() self.__sendWatchpoints() def __handleForkTo(self, params): """Handles METHOD_FORK_TO""" del params # unused argument self.__askForkTo() def __handleClearBP(self, params): """Handles METHOD_CLEAR_BP""" self.sigClientClearBreak.emit(params['filename'], params['line']) def __handleSyntaxError(self, params): """Handles METHOD_SYNTAX_ERROR""" self.sigClientSyntaxError.emit(self.__procuuid, params['message'], params['filename'], params['line'], params['characternumber']) def __handleVariable(self, params): """Handles METHOD_VARIABLE""" self.sigClientVariable.emit(params['scope'], [params['variable']] + params['variables']) def __handleBPConditionError(self, params): """Handles METHOD_BP_CONDITION_ERROR""" self.sigClientBreakConditionError.emit(params['filename'], params['line']) def __handleException(self, params): """Handles METHOD_EXCEPTION""" self.__changeDebuggerState(self.STATE_IN_IDE) if params: stack = params['stack'] if stack: if stack[0] and stack[0][0] == "<string>": for stackEntry in stack: if stackEntry[0] == "<string>": stackEntry[0] = self.__fileName else: break excType = params['type'] isUnhandled = excType is None or \ excType.lower().startswith('unhandled') or \ not stack self.sigClientException.emit(excType, params['message'], stack, isUnhandled) else: isUnhandled = True self.sigClientException.emit('', '', [], True) def __handleCallTrace(self, params): """Handles METHOD_CALL_TRACE""" isCall = params['event'] == 'c' src = params['from'] dest = params['to'] self.sigClientCallTrace.emit(isCall, src['filename'], src['linenumber'], src['codename'], dest['filename'], dest['linenumber'], dest['codename']) @staticmethod def __handleExecStatementError(params): """Handles METHOD_EXEC_STATEMENT_ERROR""" logging.error('Execute statement error:\n' + params['text']) @staticmethod def __handleExecuteStatementOutput(params): """Handles METHOD_EXEC_STATEMENT_OUTPUT""" text = params['text'] if text: logging.info('Statement execution succeeded. Output:\n' + text) else: logging.info('Statement execution succeeded. No output generated.') def __handleSignal(self, params): """Handles METHOD_SIGNAL""" message = params['message'] fileName = params['filename'] linenumber = params['linenumber'] # funcName = params['function'] # arguments = params['arguments'] self.sigClientLine.emit(fileName, linenumber, False) logging.error('The program generated the signal "' + message + '"\n' 'File: ' + fileName + ' Line: ' + str(linenumber)) def __handleThreadSet(self, params): """Handles METHOD_THREAD_SET""" del params # unused argument self.sigClientThreadSet.emit() def onProcessFinished(self, procuuid, retCode): """Process finished. The retCode may indicate a disconnection.""" del retCode # unused argument if self.__procuuid == procuuid: self.__procWrapper = None self.__procuuid = None self.__fileName = None self.__runParameters = None self.__debugSettings = None self.__stopAtFirstLine = None self.__changeDebuggerState(self.STATE_STOPPED) self.__mainWindow.switchDebugMode(False) def __askForkTo(self): " Asks what to follow, a parent or a child " dlg = QMessageBox(QMessageBox.Question, "Client forking", "Select the fork branch to follow") dlg.addButton(QMessageBox.Ok) dlg.addButton(QMessageBox.Cancel) btn1 = dlg.button(QMessageBox.Ok) btn1.setText("&Child process") btn1.setIcon(getIcon('')) btn2 = dlg.button(QMessageBox.Cancel) btn2.setText("&Parent process") btn2.setIcon(getIcon('')) dlg.setDefaultButton(QMessageBox.Cancel) res = dlg.exec_() if res == QMessageBox.Cancel: self.__sendJSONCommand(METHOD_FORK_TO, {'target': 'parent'}) else: self.__sendJSONCommand(METHOD_FORK_TO, {'target': 'child'}) def __validateBreakpoints(self): """Checks all the breakpoints validity and deletes invalid""" # It is excepted that the method is called when all the files are # saved, e.g. when a new debugging session is started. for row in range(0, self.__breakpointModel.rowCount()): index = self.__breakpointModel.index(row, 0, QModelIndex()) bpoint = self.__breakpointModel.getBreakPointByIndex(index) fileName = bpoint.getAbsoluteFileName() line = bpoint.getLineNumber() if not os.path.exists(fileName): logging.warning("Breakpoint at " + fileName + ":" + str(line) + " is invalid (the file " "disappeared from the filesystem). " "The breakpoint is deleted.") self.__breakpointModel.deleteBreakPointByIndex(index) continue breakableLines = getBreakpointLines(fileName, None, True) if breakableLines is None: logging.warning("Breakpoint at " + fileName + ":" + str(line) + " does not point to a breakable " "line (the file could not be compiled). " "The breakpoint is deleted.") self.__breakpointModel.deleteBreakPointByIndex(index) continue if line not in breakableLines: logging.warning("Breakpoint at " + fileName + ":" + str(line) + " does not point to a breakable " "line (the file was modified). " "The breakpoint is deleted.") self.__breakpointModel.deleteBreakPointByIndex(index) continue # The breakpoint is OK, keep it return def __sendBreakpoints(self): """Sends the breakpoints to the debugged program""" self.__validateBreakpoints() self.__addBreakPoints(QModelIndex(), 0, self.__breakpointModel.rowCount() - 1) def __addBreakPoints(self, parentIndex, start, end): """Adds breakpoints""" if self.__state == self.STATE_STOPPED: return for row in range(start, end + 1): index = self.__breakpointModel.index(row, 0, parentIndex) bpoint = self.__breakpointModel.getBreakPointByIndex(index) fileName = bpoint.getAbsoluteFileName() line = bpoint.getLineNumber() self.remoteBreakpoint(fileName, line, True, bpoint.getCondition(), bpoint.isTemporary()) if not bpoint.isEnabled(): self.__remoteBreakpointEnable(fileName, line, False) ignoreCount = bpoint.getIgnoreCount() if ignoreCount > 0: self.__remoteBreakpointIgnore(fileName, line, ignoreCount) def __deleteBreakPoints(self, parentIndex, start, end): """Deletes breakpoints""" if self.__state == self.STATE_STOPPED: return for row in range(start, end + 1): index = self.__breakpointModel.index(row, 0, parentIndex) bpoint = self.__breakpointModel.getBreakPointByIndex(index) fileName = bpoint.getAbsoluteFileName() line = bpoint.getLineNumber() self.remoteBreakpoint(fileName, line, False) def __breakPointDataAboutToBeChanged(self, startIndex, endIndex): """Handles the sigDataAboutToBeChanged signal of the bpoint model""" self.__deleteBreakPoints(QModelIndex(), startIndex.row(), endIndex.row()) def __changeBreakPoints(self, startIndex, endIndex): """Sets changed breakpoints""" self.__addBreakPoints(QModelIndex(), startIndex.row(), endIndex.row()) def __sendWatchpoints(self): """Sends the watchpoints to the debugged program""" pass def __remoteBreakpointEnable(self, fileName, line, enable): """Sends the breakpoint enability""" self.__sendJSONCommand(METHOD_BP_ENABLE, { 'filename': fileName, 'line': line, 'enable': enable }) def __remoteBreakpointIgnore(self, fileName, line, ignoreCount): """Sends the breakpoint ignore count""" self.__sendJSONCommand(METHOD_BP_IGNORE, { 'filename': fileName, 'line': line, 'count': ignoreCount }) def __clientClearBreakPoint(self, fileName, line): """Handles the sigClientClearBreak signal""" if self.__state == self.STATE_STOPPED: return index = self.__breakpointModel.getBreakPointIndex(fileName, line) if index.isValid(): self.__breakpointModel.deleteBreakPointByIndex(index) def __clientBreakConditionError(self, fileName, line): """Handles the condition error""" logging.error("The condition of the breakpoint at " + fileName + ":" + str(line) + " contains a syntax error.") index = self.__breakpointModel.getBreakPointIndex(fileName, line) if not index.isValid(): return bpoint = self.__breakpointModel.getBreakPointByIndex(index) if not bpoint: return dlg = BreakpointEditDialog(bpoint) if dlg.exec_() == QDialog.Accepted: newBpoint = dlg.getData() if newBpoint == bpoint: return self.__breakpointModel.setBreakPointByIndex(index, newBpoint) def remoteStep(self): """Single step in the debugged program""" self.__changeDebuggerState(self.STATE_IN_CLIENT) self.__sendJSONCommand(METHOD_STEP, None) def remoteStepOver(self): """Step over the debugged program""" self.__changeDebuggerState(self.STATE_IN_CLIENT) self.__sendJSONCommand(METHOD_STEP_OVER, None) def remoteStepOut(self): """Step out the debugged program""" self.__changeDebuggerState(self.STATE_IN_CLIENT) self.__sendJSONCommand(METHOD_STEP_OUT, None) def remoteContinue(self, special=False): """Continues the debugged program""" self.__changeDebuggerState(self.STATE_IN_CLIENT) self.__sendJSONCommand(METHOD_CONTINUE, {'special': special}) def remoteThreadList(self): """Provides the threads list""" self.__sendJSONCommand(METHOD_THREAD_LIST, None) def remoteClientVariables(self, scope, framenr=0, filters=None): """Provides the client variables. scope - 0 => local, 1 => global """ if filters is None: filters = [] self.__sendJSONCommand(METHOD_VARIABLES, { 'frameNumber': framenr, 'scope': scope, 'filters': filters }) def remoteClientVariable(self, scope, var, framenr=0, filters=None): """Provides the client variable. scope - 0 => local, 1 => global """ self.__sendJSONCommand( METHOD_VARIABLE, { 'frameNumber': framenr, 'variable': var, 'scope': scope, 'filters': filters }) def remoteExecuteStatement(self, statement, framenr): """Executes the expression in the current context of the debuggee""" self.__sendJSONCommand(METHOD_EXECUTE_STATEMENT, { 'statement': statement, 'frameNumber': framenr }) def remoteBreakpoint(self, fileName, line, isSetting, condition=None, temporary=False): """Sets or clears a breakpoint""" params = { 'filename': fileName, 'line': line, 'setBreakpoint': isSetting, 'condition': condition, 'temporary': temporary } self.__sendJSONCommand(METHOD_SET_BP, params) def remoteSetThread(self, tid): """Sets the given thread as the current""" self.__sendJSONCommand(METHOD_THREAD_SET, {'threadID': tid}) def stopDebugging(self, exitCode=None): """Stops the debugging session""" if self.__procWrapper: if not self.__procWrapper.hasConnected(): # The counterpart has not connected back to the IDE # So there is no need to send a cancel message. # Instead, just clean all the initialized data structures # in the run manager and switch to the editing mode self.__procWrapper.cancelPendingDebugSession() else: if exitCode is None: self.__sendJSONCommand(METHOD_STEP_QUIT, None) else: self.__sendJSONCommand(METHOD_STEP_QUIT, {'exitCode': exitCode}) def stopCalltrace(self): """Sends a message to stop call tracing""" if self.__procWrapper: self.__sendJSONCommand(METHOD_CALL_TRACE, {'enable': False}) def startCalltrace(self): """Sends a message to start call tracing""" if self.__procWrapper: self.__sendJSONCommand(METHOD_CALL_TRACE, {'enable': True}) def __sendJSONCommand(self, method, params): """Sends a message to the debuggee""" if self.__procWrapper: self.__procWrapper.sendJSONCommand(method, params) else: raise Exception('Trying to send JSON command from the debugger ' 'to the debugged program wneh there is no remote ' 'process wrapper. Method: ' + str(method) + 'Parameters: ' + repr(params))
class ASTView(QTreeWidget): sigGotoLine = pyqtSignal(int, int) def __init__(self, navBar, parent): QTreeWidget.__init__(self, parent) self.__navBar = navBar self.setAlternatingRowColors(True) self.setRootIsDecorated(True) self.setItemsExpandable(True) self.setSortingEnabled(False) self.setItemDelegate(NoOutlineHeightDelegate(4)) self.setUniformRowHeights(True) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setExpandsOnDoubleClick(False) self.__headerItem = QTreeWidgetItem(['Node', 'Position / items']) self.setHeaderItem(self.__headerItem) self.itemSelectionChanged.connect(self.__selectionChanged) self.itemActivated.connect(self.__activated) def populateAST(self, source, filename): """Populates the AST tree""" self.__navBar.clearWarnings() hScroll = self.horizontalScrollBar().value() vScroll = self.verticalScrollBar().value() try: tree = parseSourceToAST(source, filename) self.__parentStack = [None] self.clear() self.addNodeRecursive(tree) self.header().resizeSections(QHeaderView.ResizeToContents) self.__navBar.updateInfoIcon(self.__navBar.STATE_OK_UTD) self.horizontalScrollBar().setValue(hScroll) self.verticalScrollBar().setValue(vScroll) except Exception as exc: self.__navBar.updateInfoIcon(self.__navBar.STATE_BROKEN_UTD) self.__navBar.setErrors( 'Parse source to AST error:\n' + str(exc)) def addNodeRecursive(self, node, prefix=None): nodeName = node.__class__.__name__ if prefix is not None: nodeName = prefix + nodeName treeNode = QTreeWidgetItem([nodeName, self.__getNodePosition(node)]) if self.__parentStack[-1] is None: self.addTopLevelItem(treeNode) else: self.__parentStack[-1].addChild(treeNode) for fieldName in node._fields: fieldValue = getattr(node, fieldName) if isinstance(fieldValue, ast.AST): if fieldValue._fields: self.__parentStack.append(treeNode) self.addNodeRecursive(fieldValue, fieldName + ': ') self.__parentStack.pop(-1) else: treeNode.addChild( QTreeWidgetItem( [fieldName + ': ' + fieldValue.__class__.__name__, self.__getNodePosition(fieldValue)])) elif self.__isScalar(fieldValue): treeNode.addChild( QTreeWidgetItem([fieldName + ': ' + repr(fieldValue), ''])) elif isinstance(fieldValue, list): listLength = len(fieldValue) txt = str(listLength) + ' item' if listLength != 1: txt += 's' listNode = QTreeWidgetItem([fieldName + ': [...]', txt]) treeNode.addChild(listNode) self.expandItem(listNode) self.__parentStack.append(listNode) for index, listItem in enumerate(fieldValue): prefix = '[' + str(index) + ']: ' if self.__isScalar(listItem): treeNode.addChild( QTreeWidgetItem([prefix + repr(listItem), ''])) else: self.addNodeRecursive(listItem, prefix) self.__parentStack.pop(-1) else: logging.error('AST node is not recognized. Skipping...') self.expandItem(treeNode) @staticmethod def __getNodePosition(astNode): pos = '' if hasattr(astNode, 'lineno'): pos = str(astNode.lineno) if hasattr(astNode, 'col_offset'): pos += ':' + str(astNode.col_offset) if hasattr(astNode, 'end_lineno'): pos += ' - ' + str(astNode.end_lineno) if hasattr(astNode, 'end_col_offset'): pos += ':' + str(astNode.end_col_offset) return pos @staticmethod def __isScalar(val): if isinstance(val, str): return True if isinstance(val, int): return True if isinstance(val, float): return True if isinstance(val, bytes): return True if val is None: return True def __selectionChanged(self): """Handles an AST item selection""" selected = list(self.selectedItems()) self.__navBar.setSelectionLabel(len(selected), None) if selected: if len(selected) == 1: current = selected[0] path = self.__getPathElement(current) while current.parent() is not None: current = current.parent() path = self.__getPathElement(current) + u' \u2192 ' + path self.__navBar.setPath(path) else: self.__navBar.setPath('') else: self.__navBar.setPath('') @staticmethod def __getPathElement(node): text = node.text(0) if text.startswith('['): # List item, the actual purpose follows the index return text # Regular node, after ':' there might be its type return text.split(':')[0] @staticmethod def __getLinePos(node): while node is not None: text = node.text(1) if text: if not 'item' in text: firstRegion = text.split('-')[0].strip() try: parts = firstRegion.split(':') line = int(parts[0]) pos = 0 if len(parts) == 2: pos = int(parts[1]) return line, pos except: pass node = node.parent() # Not found, e.g. it is a module return 1, 0 def __activated(self, item, _): """Handles the double click (or Enter) on an AST item""" line, pos = self.__getLinePos(item) self.sigGotoLine.emit(line, pos + 1)
class Watcher(QObject): """Filesystem watcher implementation""" sigFSChanged = pyqtSignal(list) def __init__(self, excludeFilters, dirToWatch): QObject.__init__(self) self.__dirWatcher = QFileSystemWatcher(self) # data members self.__excludeFilter = [] # Files exclude filter self.__srcDirsToWatch = set() # Came from the user self.__fsTopLevelSnapshot = {} # Current snapshot self.__fsSnapshot = {} # Current snapshot # Sets of dirs which are currently watched self.__dirsToWatch = set() self.__topLevelDirsToWatch = set() # Generated till root # precompile filters for flt in excludeFilters: self.__excludeFilter.append(re.compile(flt)) # Initialise the list of dirs to watch self.__srcDirsToWatch.add(dirToWatch) self.__topLevelDirsToWatch = self.__buildTopDirsList( self.__srcDirsToWatch) self.__fsTopLevelSnapshot = self.__buildTopLevelSnapshot( self.__topLevelDirsToWatch, self.__srcDirsToWatch) self.__dirsToWatch = self.__buildSnapshot() # Here __dirsToWatch and __topLevelDirsToWatch have a complete # set of what should be watched # Add the dirs to the watcher dirs = [] for path in self.__dirsToWatch | self.__topLevelDirsToWatch: dirs.append(path) self.__dirWatcher.addPaths(dirs) self.__dirWatcher.directoryChanged.connect(self.__onDirChanged) # self.debug() return @staticmethod def __buildTopDirsList(srcDirs): """Takes a list of dirs to be watched and builds top dirs set""" topDirsList = set() for path in srcDirs: parts = path.split(os.path.sep) for index in range(1, len(parts) - 1): candidate = os.path.sep.join(parts[0:index]) + os.path.sep if os.path.exists(candidate): if os.access(candidate, os.R_OK): topDirsList.add(candidate) return topDirsList @staticmethod def __buildTopLevelSnapshot(topLevelDirs, srcDirs): """Takes top level dirs and builds their snapshot""" snapshot = {} for path in topLevelDirs: itemsSet = set() # search for all the dirs to be watched for candidate in topLevelDirs | srcDirs: if len(candidate) <= len(path): continue if candidate.startswith(path): candidate = candidate[len(path):] slashIndex = candidate.find(os.path.sep) + 1 item = candidate[:slashIndex] if os.path.exists(path + item): itemsSet.add(item) snapshot[path] = itemsSet return snapshot def __buildSnapshot(self): """Builds the filesystem snapshot""" snapshotDirs = set() for path in self.__srcDirsToWatch: self.__addSnapshotPath(path, snapshotDirs) return snapshotDirs def __addSnapshotPath(self, path, snapshotDirs, itemsToReport=None): """Adds one path to the FS snapshot""" if not os.path.exists(path): return snapshotDirs.add(path) dirItems = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): dirName = path + item + os.path.sep dirItems.add(item + os.path.sep) if itemsToReport is not None: itemsToReport.append("+" + dirName) self.__addSnapshotPath(dirName, snapshotDirs, itemsToReport) continue dirItems.add(item) if itemsToReport is not None: itemsToReport.append("+" + path + item) self.__fsSnapshot[path] = dirItems return def __onDirChanged(self, path): """Triggered when the dir is changed""" if not path.endswith(os.path.sep): path = path + os.path.sep # Check if it is a top level dir try: oldSet = self.__fsTopLevelSnapshot[path] # Build a new set of what is in that top level dir newSet = set() for item in os.listdir(path): if not os.path.isdir(path + item): continue # Only dirs are of interest for the top level item = item + os.path.sep if item in oldSet: newSet.add(item) # Now we have an old set and a new one with those from the old # which actually exist diff = oldSet - newSet # diff are those which disappeared. We need to do the following: # - build a list of all the items in the fs snapshot which start # from this dir # - build a list of dirs which should be deregistered from the # watcher. This list includes both top level and project level # - deregister dirs from the watcher # - emit a signal of what disappeared if not diff: return # no changes self.__fsTopLevelSnapshot[path] = newSet dirsToBeRemoved = [] itemsToReport = [] for item in diff: self.__processRemoveTopDir(path + item, dirsToBeRemoved, itemsToReport) # Here: it is possible that the last dir to watch disappeared if not newSet: # There is nothing to watch here anymore dirsToBeRemoved.append(path) del self.__fsTopLevelSnapshot[path] parts = path[1:-1].split(os.path.sep) for index in range(len(parts) - 2, 0, -1): candidate = os.path.sep + \ os.path.sep.join(parts[0:index]) + \ os.path.sep dirSet = self.__fsTopLevelSnapshot[candidate] dirSet.remove(parts[index + 1] + os.path.sep) if not dirSet: dirsToBeRemoved.append(candidate) del self.__fsTopLevelSnapshot[candidate] continue break # it is not the last item in the set # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths(dirsToBeRemoved) # Report if itemsToReport: self.sigFSChanged.emit(itemsToReport) return except: # it is not a top level dir - no key pass # Here: the change is in the project level dir try: oldSet = self.__fsSnapshot[path] # Build a new set of what is in that top level dir newSet = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): newSet.add(item + os.path.sep) else: newSet.add(item) # Here: we have a new and old snapshots # Lets calculate the difference deletedItems = oldSet - newSet addedItems = newSet - oldSet if not deletedItems and not addedItems: return # No changes # Update the changed dir set self.__fsSnapshot[path] = newSet # We need to build some lists: # - list of files which were added # - list of dirs which were added # - list of files which were deleted # - list of dirs which were deleted # The deleted dirs must be unregistered in the watcher # The added dirs must be registered itemsToReport = [] dirsToBeAdded = [] dirsToBeRemoved = [] for item in addedItems: if item.endswith(os.path.sep): # directory was added self.__processAddedDir(path + item, dirsToBeAdded, itemsToReport) else: itemsToReport.append("+" + path + item) for item in deletedItems: if item.endswith(os.path.sep): # directory was deleted self.__processRemovedDir(path + item, dirsToBeRemoved, itemsToReport) else: itemsToReport.append("-" + path + item) # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths(dirsToBeRemoved) if dirsToBeAdded: self.__dirWatcher.addPaths(dirsToBeAdded) # Report self.sigFSChanged.emit(itemsToReport) except: # It could be a queued signal about what was already reported pass # self.debug() return def __shouldExclude(self, name): """Tests if a file must be excluded""" for excl in self.__excludeFilter: if excl.match(name): return True return False def __processAddedDir(self, path, dirsToBeAdded, itemsToReport): """called for an appeared dir in the project tree""" dirsToBeAdded.append(path) itemsToReport.append("+" + path) # it should add dirs recursively into the snapshot and care # of the items to report dirItems = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): dirName = path + item + os.path.sep dirItems.add(item + os.path.sep) self.__processAddedDir(dirName, dirsToBeAdded, itemsToReport) continue itemsToReport.append("+" + path + item) dirItems.add(item) self.__fsSnapshot[path] = dirItems return def __processRemovedDir(self, path, dirsToBeRemoved, itemsToReport): """called for a disappeared dir in the project tree""" # it should remove the dirs recursively from the fs snapshot # and care of items to report dirsToBeRemoved.append(path) itemsToReport.append("-" + path) oldSet = self.__fsSnapshot[path] for item in oldSet: if item.endswith(os.path.sep): # Nested dir self.__processRemovedDir(path + item, dirsToBeRemoved, itemsToReport) else: # a file itemsToReport.append("-" + path + item) del self.__fsSnapshot[path] return def __processRemoveTopDir(self, path, dirsToBeRemoved, itemsToReport): """Called for a disappeared top level dir""" if path in self.__fsTopLevelSnapshot: # It is still a top level dir dirsToBeRemoved.append(path) for item in self.__fsTopLevelSnapshot[path]: self.__processRemoveTopDir(path + item, dirsToBeRemoved, itemsToReport) del self.__fsTopLevelSnapshot[path] else: # This is a project level dir self.__processRemovedDir(path, dirsToBeRemoved, itemsToReport) return def reset(self): """Resets the watcher (it does not report any changes)""" self.__dirWatcher.removePaths(self.__dirWatcher.directories()) self.__srcDirsToWatch = set() self.__fsTopLevelSnapshot = {} self.__fsSnapshot = {} self.__dirsToWatch = set() self.__topLevelDirsToWatch = set() def registerDir(self, path): """Adds a directory to the list of watched ones""" if not path.endswith(os.path.sep): path = path + os.path.sep if path in self.__srcDirsToWatch: return # It is there already # It is necessary to do the following: # - add the dir to the fs snapshot # - collect dirs to add to the watcher # - collect items to report self.__srcDirsToWatch.add(path) dirsToWatch = set() itemsToReport = [] self.__registerDir(path, dirsToWatch, itemsToReport) # It might be that top level dirs should be updated too newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch) addedDirs = newTopLevelDirsToWatch - self.__topLevelDirsToWatch for item in addedDirs: dirsToWatch.add(item) # Identify items to be watched by this dir dirItems = set() for candidate in newTopLevelDirsToWatch | self.__srcDirsToWatch: if len(candidate) <= len(item): continue if candidate.startswith(item): candidate = candidate[len(item):] slashIndex = candidate.find(os.path.sep) + 1 dirName = candidate[:slashIndex] if os.path.exists(item + dirName): dirItems.add(dirName) # Update the top level dirs snapshot self.__fsTopLevelSnapshot[item] = dirItems # Update the top level snapshot with the added dir upperDir = os.path.dirname(path[:-1]) + os.path.sep dirName = path.replace(upperDir, '') self.__fsTopLevelSnapshot[upperDir].add(dirName) # Update the list of top level dirs to watch self.__topLevelDirsToWatch = newTopLevelDirsToWatch # Update the watcher if dirsToWatch: dirs = [] for item in dirsToWatch: dirs.append(item) self.__dirWatcher.addPaths(dirs) # Report the changes if itemsToReport: self.sigFSChanged.emit(itemsToReport) # self.debug() return def __registerDir(self, path, dirsToWatch, itemsToReport): """Adds one path to the FS snapshot""" if not os.path.exists(path): return dirsToWatch.add(path) itemsToReport.append("+" + path) dirItems = set() for item in os.listdir(path): if self.__shouldExclude(item): continue if os.path.isdir(path + item): dirName = path + item + os.path.sep dirItems.add(item + os.path.sep) itemsToReport.append("+" + path + item + os.path.sep) self.__addSnapshotPath(dirName, dirsToWatch, itemsToReport) continue dirItems.add(item) itemsToReport.append("+" + path + item) self.__fsSnapshot[path] = dirItems def deregisterDir(self, path): """Removes the directory from the list of the watched ones""" if not path.endswith(os.path.sep): path = path + os.path.sep if path not in self.__srcDirsToWatch: return # It is not there already self.__srcDirsToWatch.remove(path) # It is necessary to do the following: # - remove the dir from the fs snapshot # - collect the dirs to be removed from watching # - collect item to report itemsToReport = [] dirsToBeRemoved = [] self.__deregisterDir(path, dirsToBeRemoved, itemsToReport) # It is possible that some of the top level watched dirs should be # removed as well newTopLevelDirsToWatch = self.__buildTopDirsList(self.__srcDirsToWatch) deletedDirs = self.__topLevelDirsToWatch - newTopLevelDirsToWatch for item in deletedDirs: dirsToBeRemoved.append(item) del self.__fsTopLevelSnapshot[item] # It might be the case that some of the items should be deleted in the # top level dirs sets for dirName in self.__fsTopLevelSnapshot: itemsSet = self.__fsTopLevelSnapshot[dirName] for item in itemsSet: candidate = dirName + item if candidate == path or candidate in deletedDirs: itemsSet.remove(item) self.__fsTopLevelSnapshot[dirName] = itemsSet break # Update the list of dirs to be watched self.__topLevelDirsToWatch = newTopLevelDirsToWatch # Update the watcher if dirsToBeRemoved: self.__dirWatcher.removePaths(dirsToBeRemoved) # Report the changes if itemsToReport: self.sigFSChanged.emit(itemsToReport) # self.debug() def __deregisterDir(self, path, dirsToBeRemoved, itemsToReport): """Deregisters a directory recursively""" dirsToBeRemoved.append(path) itemsToReport.append("-" + path) if path in self.__fsTopLevelSnapshot: # This is a top level dir for item in self.__fsTopLevelSnapshot[path]: if item.endswith(os.path.sep): # It's a dir self.__deregisterDir(path + item, dirsToBeRemoved, itemsToReport) else: # It's a file itemsToReport.append("-" + path + item) del self.__fsTopLevelSnapshot[path] return # It is from an a project level snapshot if path in self.__fsSnapshot: for item in self.__fsSnapshot[path]: if item.endswith(os.path.sep): # It's a dir self.__deregisterDir(path + item, dirsToBeRemoved, itemsToReport) else: # It's a file itemsToReport.append("-" + path + item) del self.__fsSnapshot[path] return def debug(self): """Debugging printouts""" print("Top level dirs to watch: " + str(self.__topLevelDirsToWatch)) print("Project dirs to watch: " + str(self.__dirsToWatch)) print("Top level snapshot: " + str(self.__fsTopLevelSnapshot)) print("Project snapshot: " + str(self.__fsSnapshot))
class PylintDriver(QWidget): """Pylint driver which runs pylint in the background""" sigFinished = pyqtSignal(dict) def __init__(self, ide): QWidget.__init__(self) self.__ide = ide self.__process = None self.__args = None self.__stdout = '' self.__stderr = '' def isInProcess(self): """True if pylint is still running""" return self.__process is not None def start(self, fileName, encoding): """Runs the analysis process""" if self.__process is not None: return 'Another pylint analysis is in progress' self.__fileName = fileName self.__encoding = 'utf-8' if encoding is None else encoding self.__process = QProcess(self) self.__process.setProcessChannelMode(QProcess.SeparateChannels) self.__process.setWorkingDirectory(os.path.dirname(self.__fileName)) self.__process.readyReadStandardOutput.connect(self.__readStdOutput) self.__process.readyReadStandardError.connect(self.__readStdError) self.__process.finished.connect(self.__finished) self.__stdout = '' self.__stderr = '' self.__args = [ '-m', 'pylint', '--output-format', 'text', '--msg-template', '{msg_id}:{line:3d},{column}: {obj}: {msg}', os.path.basename(self.__fileName) ] rcfile = PylintDriver.getPylintrc(self.__ide, self.__fileName) if rcfile: self.__args.append("--rcfile") self.__args.append(rcfile) initHook = self.getInitHook() if initHook: self.__args.append("--init-hook") self.__args.append(initHook) processEnvironment = QProcessEnvironment() processEnvironment.insert('PYTHONIOENCODING', self.__encoding) self.__process.setProcessEnvironment(processEnvironment) self.__process.start(sys.executable, self.__args) running = self.__process.waitForStarted() if not running: self.__process = None return 'pylint analysis failed to start' return None def stop(self): """Interrupts the analysis""" if self.__process is not None: if self.__process.state() == QProcess.Running: self.__process.kill() self.__process.waitForFinished() self.__process = None self.__args = None def generateRCFile(self, ide, fileName): """Generates the pylintrc file""" if ide.project.isLoaded(): rcfile = ide.project.getProjectDir() + 'pylintrc' else: rcfile = os.path.dirname(fileName) + os.path.sep + 'pylintrc' process = QProcess(self) process.setStandardOutputFile(rcfile) process.start(sys.executable, ['-m', 'pylint', '--generate-rcfile']) process.waitForFinished() return rcfile @staticmethod def getPylintrc(ide, fileName): """Provides the pylintrc path""" names = ['pylintrc', '.pylintrc'] dirs = [] if fileName: dirs = [os.path.dirname(fileName) + os.path.sep] if ide.project.isLoaded(): dirs.append(ide.project.getProjectDir()) for dirPath in dirs: for name in names: if os.path.exists(dirPath + name): return dirPath + name return None def getInitHook(self): """Provides the init hook with the import directories""" if not self.__ide.project.isLoaded(): return None importDirs = self.__ide.project.getImportDirsAsAbsolutePaths() if not importDirs: return None importDirs.reverse() code = 'import sys' for importDir in importDirs: code += ';sys.path.insert(0,"' + importDir + '")' return code def __readStdOutput(self): """Handles reading from stdout""" self.__process.setReadChannel(QProcess.StandardOutput) qba = QByteArray() while self.__process.bytesAvailable(): qba += self.__process.readAllStandardOutput() self.__stdout += str(qba.data(), self.__encoding) def __readStdError(self): """Handles reading from stderr""" self.__process.setReadChannel(QProcess.StandardError) qba = QByteArray() while self.__process.bytesAvailable(): qba += self.__process.readAllStandardError() self.__stderr += str(qba.data(), self.__encoding) def __finished(self, exitCode, exitStatus): """Handles the process finish""" self.__process = None results = { 'ExitCode': exitCode, 'ExitStatus': exitStatus, 'FileName': self.__fileName, 'Timestamp': getLocaleDateTime(), 'CommandLine': [sys.executable] + self.__args } if not self.__stdout: if self.__stderr: results['ProcessError'] = 'pylint error:\n' + self.__stderr else: results['ProcessError'] = 'pylint produced no output ' \ '(finished abruptly) for ' + \ self.__fileName self.sigFinished.emit(results) self.__args = None return # Convention, Refactor, Warning, Error results.update({ 'C': [], 'R': [], 'W': [], 'E': [], 'StdOut': self.__stdout, 'StdErr': self.__stderr }) modulePattern = '************* Module ' module = '' for line in self.__stdout.splitlines(): if line.startswith(modulePattern): module = line[len(modulePattern):] continue if not re.match(r'^[CRWE]+([0-9]{4})?:', line): continue colonPos1 = line.find(':') if colonPos1 == -1: continue msgId = line[:colonPos1] colonPos2 = line.find(':', colonPos1 + 1) if colonPos2 == -1: continue lineNo = line[colonPos1 + 1:colonPos2].strip() if not lineNo: continue lineNo = int(lineNo.split(',')[0]) message = line[colonPos2 + 1:].strip() if message.startswith(':'): message = message[1:].strip() item = (module, lineNo, message, msgId) results[line[0]].append(item) # Rate and previous run ratePattern = 'Your code has been rated at ' ratePos = self.__stdout.find(ratePattern) if ratePos > 0: rateEndPos = self.__stdout.find('/10', ratePos) if rateEndPos > 0: rate = self.__stdout[ratePos + len(ratePattern):rateEndPos] results['Rate'] = rate # Previous run prevRunPattern = 'previous run: ' prevRunPos = self.__stdout.find(prevRunPattern, rateEndPos) if prevRunPos > 0: prevRunEndPos = self.__stdout.find('/10', prevRunPos) previous = self.__stdout[prevRunPos + len(prevRunPattern):prevRunEndPos] results['PreviousRunRate'] = previous self.sigFinished.emit(results) self.__args = None
class RemoteProcessWrapper(QObject): """Wrapper to control the remote process""" sigFinished = pyqtSignal(str, int) sigClientStdout = pyqtSignal(str, str) sigClientStderr = pyqtSignal(str, str) sigClientInput = pyqtSignal(str, str, int) sigIncomingMessage = pyqtSignal(str, str, object) def __init__(self, path, serverPort, redirected, kind): QObject.__init__(self) self.procuuid = str(uuid.uuid1()) self.path = path self.redirected = redirected self.kind = kind self.state = None self.startTime = None self.finishTime = None self.__serverPort = serverPort self.__clientSocket = None self.__proc = None def start(self): """Starts the remote process""" params = getRunParameters(self.path) if self.redirected: cmd, environment = getCwdCmdEnv(self.kind, self.path, params, self.__serverPort, self.procuuid) else: cmd, environment = getCwdCmdEnv(self.kind, self.path, params, self.__serverPort, self.procuuid) self.__proc = Popen(cmd, shell=True, cwd=getWorkingDir(self.path, params), env=environment) def setSocket(self, clientSocket): """Called when an incoming connection has come""" self.__clientSocket = clientSocket self.state = STATE_RUNNING self.__connectSocket() self.__parseClientLine() # Send runnee the 'start' message self.__sendStart() def stop(self): """Kills the process""" self.__disconnectSocket() self.__kill() self.sigFinished.emit(self.procuuid, KILLED) def __connectSocket(self): """Connects the socket slots""" if self.__clientSocket: self.__clientSocket.readyRead.connect(self.__parseClientLine) self.__clientSocket.disconnected.connect(self.__disconnected) def __disconnectSocket(self): """Disconnects the socket related slots""" if self.__clientSocket: try: self.__clientSocket.readyRead.disconnect( self.__parseClientLine) self.__clientSocket.disconnected.disconnect( self.__disconnected) except: pass def __closeSocket(self): """Closes the client socket if so""" if self.__clientSocket: try: self.__clientSocket.close() except: pass self.__clientSocket = None def wait(self): """Waits for the process""" self.__closeSocket() if self.__proc is not None: try: self.__proc.wait() except: # E.g. wait timeout pass def waitDetached(self): """Needs to avoid zombies""" try: if self.__proc.poll() is not None: self.__proc.wait() return True except: return True return False def __kill(self): """Kills the process or checks there is no process in memory""" if self.__proc is not None: try: self.__proc.kill() except: pass childPID = self.__getChildPID() while childPID is not None: try: # Throws an exception if cannot kill the process killProcess(childPID) except: pass nextPID = self.__getChildPID() if nextPID == childPID: break childPID = nextPID # Here: the process killed self.wait() self.__proc = None def __getChildPID(self): """Provides the child process PID if redirected""" if self.__serverPort is None or self.procuuid is None: return None if self.kind == RUN: wrapper = os.path.join('client', 'client_cdm_run.py') elif self.kind == PROFILE: wrapper = os.path.join('client', 'client_cdm_profile.py') else: wrapper = os.path.join('client', 'client_cdm_dbg.py') for item in os.listdir("/proc"): if item.isdigit(): try: f = open("/proc/" + item + "/cmdline", "r") content = f.read() f.close() if wrapper in content: if '--port' in content: if str(self.__serverPort) in content: if '--procuuid' in content: if self.procuuid in content: return int(item) except: pass return None def __disconnected(self): """Triggered when the client closed the connection""" self.__kill() self.sigFinished.emit(self.procuuid, DISCONNECTED) def __sendStart(self): """Sends the start command to the runnee""" sendJSONCommand(self.__clientSocket, METHOD_PROLOGUE_CONTINUE, self.procuuid, None) def __sendExit(self): """sends the exit command to the runnee""" self.__disconnectSocket() sendJSONCommand(self.__clientSocket, METHOD_EPILOGUE_EXIT, self.procuuid, None) def sendJSONCommand(self, method, params): """Sends a command to the debuggee. Used by the debugger.""" sendJSONCommand(self.__clientSocket, method, self.procuuid, params) def __parseClientLine(self): """Parses a single line from the running client""" while self.__clientSocket and self.__clientSocket.canReadLine(): try: method, procuuid, params, jsonStr = getParsedJSONMessage( self.__clientSocket) del procuuid # unused if IDE_DEBUG: print("Process wrapper received: " + str(jsonStr)) if method == METHOD_EPILOGUE_EXIT_CODE: self.__sendExit() self.sigFinished.emit(self.procuuid, params['exitCode']) QApplication.processEvents() continue if method == METHOD_STDOUT: self.sigClientStdout.emit(self.procuuid, params['text']) QApplication.processEvents() continue if method == METHOD_STDERR: self.sigClientStderr.emit(self.procuuid, params['text']) QApplication.processEvents() continue if method == METHOD_STDIN: prompt = params['prompt'] echo = params['echo'] self.sigClientInput.emit(self.procuuid, prompt, echo) QApplication.processEvents() continue # The other messages may appear only when a code is debugged # so they are processed outside of a generic run manager self.sigIncomingMessage.emit(self.procuuid, method, params) except Exception as exc: logging.error('Failure to get a message ' 'from a remote process: ' + str(exc)) def userInput(self, collectedString): """Called when the user finished input""" if self.__clientSocket: sendJSONCommand(self.__clientSocket, METHOD_STDIN, self.procuuid, {'input': collectedString})
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 TextEditorTabWidget(QWidget): """Plain text editor tab widget""" sigReloadRequest = pyqtSignal() reloadAllNonModifiedRequest = pyqtSignal() sigTabRunChanged = pyqtSignal(bool) def __init__(self, parent, debugger): QWidget.__init__(self, parent) extendInstance(self, MainWindowTabWidgetBase) MainWindowTabWidgetBase.__init__(self) self.__navigationBar = None self.__editor = TextEditor(self, debugger) self.__fileName = "" self.__shortName = "" self.__createLayout() self.__editor.redoAvailable.connect(self.__redoAvailable) self.__editor.undoAvailable.connect(self.__undoAvailable) self.__editor.modificationChanged.connect(self.modificationChanged) self.__editor.sigCFlowSyncRequested.connect(self.cflowSyncRequested) self.__editor.languageChanged.connect(self.__languageChanged) self.__diskModTime = None self.__diskSize = None self.__reloadDlgShown = False self.__debugMode = False self.__vcsStatus = None def onTextZoomChanged(self): """Triggered when a text zoom is changed""" self.__editor.onTextZoomChanged() def onFlowZoomChanged(self): """Triggered when a flow zoom is changed""" self.__flowUI.onFlowZoomChanged() def getNavigationBar(self): """Provides a reference to the navigation bar""" return self.__navigationBar def shouldAcceptFocus(self): """True if it can accept the focus""" return self.__outsideChangesBar.isHidden() def readFile(self, fileName): """Reads the text from a file""" self.__editor.readFile(fileName) self.setFileName(fileName) self.__editor.restoreBreakpoints() # Memorize the modification date path = os.path.realpath(fileName) self.__diskModTime = os.path.getmtime(path) self.__diskSize = os.path.getsize(path) def writeFile(self, fileName): """Writes the text to a file""" if self.__editor.writeFile(fileName): # Memorize the modification date path = os.path.realpath(fileName) self.__diskModTime = os.path.getmtime(path) self.__diskSize = os.path.getsize(path) self.setFileName(fileName) self.__editor.restoreBreakpoints() return True return False def __createLayout(self): """Creates the toolbar and layout""" # Buttons printButton = QAction(getIcon('printer.png'), 'Print (Ctrl+P)', self) printButton.triggered.connect(self.__onPrint) printPreviewButton = QAction(getIcon('printpreview.png'), 'Print preview', self) printPreviewButton.triggered.connect(self.__onPrintPreview) printPreviewButton.setEnabled(False) printPreviewButton.setVisible(False) printSpacer = QWidget() printSpacer.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) printSpacer.setFixedHeight(8) # Imports diagram and its menu importsMenu = QMenu(self) importsDlgAct = importsMenu.addAction(getIcon('detailsdlg.png'), 'Fine tuned imports diagram') importsDlgAct.triggered.connect(self.onImportDgmTuned) self.importsDiagramButton = QToolButton(self) self.importsDiagramButton.setIcon(getIcon('importsdiagram.png')) self.importsDiagramButton.setToolTip('Generate imports diagram') self.importsDiagramButton.setPopupMode(QToolButton.DelayedPopup) self.importsDiagramButton.setMenu(importsMenu) self.importsDiagramButton.setFocusPolicy(Qt.NoFocus) self.importsDiagramButton.clicked.connect(self.onImportDgm) self.importsDiagramButton.setEnabled(False) # Run script and its menu runScriptMenu = QMenu(self) runScriptDlgAct = runScriptMenu.addAction(getIcon('detailsdlg.png'), 'Set run/debug parameters') runScriptDlgAct.triggered.connect(self.onRunScriptDlg) self.runScriptButton = QToolButton(self) self.runScriptButton.setIcon(getIcon('run.png')) self.runScriptButton.setToolTip('Run script') self.runScriptButton.setPopupMode(QToolButton.DelayedPopup) self.runScriptButton.setMenu(runScriptMenu) self.runScriptButton.setFocusPolicy(Qt.NoFocus) self.runScriptButton.clicked.connect(self.onRunScript) self.runScriptButton.setEnabled(False) # Profile script and its menu profileScriptMenu = QMenu(self) profileScriptDlgAct = profileScriptMenu.addAction( getIcon('detailsdlg.png'), 'Set profile parameters') profileScriptDlgAct.triggered.connect(self.onProfileScriptDlg) self.profileScriptButton = QToolButton(self) self.profileScriptButton.setIcon(getIcon('profile.png')) self.profileScriptButton.setToolTip('Profile script') self.profileScriptButton.setPopupMode(QToolButton.DelayedPopup) self.profileScriptButton.setMenu(profileScriptMenu) self.profileScriptButton.setFocusPolicy(Qt.NoFocus) self.profileScriptButton.clicked.connect(self.onProfileScript) self.profileScriptButton.setEnabled(False) # Debug script and its menu debugScriptMenu = QMenu(self) debugScriptDlgAct = debugScriptMenu.addAction( getIcon('detailsdlg.png'), 'Set run/debug parameters') debugScriptDlgAct.triggered.connect(self.onDebugScriptDlg) self.debugScriptButton = QToolButton(self) self.debugScriptButton.setIcon(getIcon('debugger.png')) self.debugScriptButton.setToolTip('Debug script') self.debugScriptButton.setPopupMode(QToolButton.DelayedPopup) self.debugScriptButton.setMenu(debugScriptMenu) self.debugScriptButton.setFocusPolicy(Qt.NoFocus) self.debugScriptButton.clicked.connect(self.onDebugScript) self.debugScriptButton.setEnabled(False) # Disassembling disasmScriptMenu = QMenu(self) disasmScriptMenu.addAction(getIcon(''), 'Disassembly (no optimization)', self.__editor._onDisasm0) disasmScriptMenu.addAction(getIcon(''), 'Disassembly (optimization level 1)', self.__editor._onDisasm1) disasmScriptMenu.addAction(getIcon(''), 'Disassembly (optimization level 2)', self.__editor._onDisasm2) self.disasmScriptButton = QToolButton(self) self.disasmScriptButton.setIcon(getIcon('disassembly.png')) self.disasmScriptButton.setToolTip('Disassembly script') self.disasmScriptButton.setPopupMode(QToolButton.DelayedPopup) self.disasmScriptButton.setMenu(disasmScriptMenu) self.disasmScriptButton.setFocusPolicy(Qt.NoFocus) self.disasmScriptButton.clicked.connect(self.__editor._onDisasm0) self.disasmScriptButton.setEnabled(False) # Dead code self.deadCodeScriptButton = QAction(getIcon('deadcode.png'), 'Find dead code', self) self.deadCodeScriptButton.triggered.connect(self.__onDeadCode) self.deadCodeScriptButton.setEnabled(False) undoSpacer = QWidget() undoSpacer.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) undoSpacer.setFixedHeight(8) self.__undoButton = QAction(getIcon('undo.png'), 'Undo (Ctrl+Z)', self) self.__undoButton.setShortcut('Ctrl+Z') self.__undoButton.triggered.connect(self.__editor.onUndo) self.__undoButton.setEnabled(False) self.__redoButton = QAction(getIcon('redo.png'), 'Redo (Ctrl+Y)', self) self.__redoButton.setShortcut('Ctrl+Y') self.__redoButton.triggered.connect(self.__editor.onRedo) self.__redoButton.setEnabled(False) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.removeTrailingSpacesButton = QAction(getIcon('trailingws.png'), 'Remove trailing spaces', self) self.removeTrailingSpacesButton.triggered.connect( self.onRemoveTrailingWS) self.expandTabsButton = QAction(getIcon('expandtabs.png'), 'Expand tabs (4 spaces)', self) self.expandTabsButton.triggered.connect(self.onExpandTabs) # The toolbar toolbar = QToolBar(self) toolbar.setOrientation(Qt.Vertical) toolbar.setMovable(False) toolbar.setAllowedAreas(Qt.RightToolBarArea) toolbar.setIconSize(QSize(16, 16)) toolbar.setFixedWidth(30) toolbar.setContentsMargins(0, 0, 0, 0) toolbar.addAction(printPreviewButton) toolbar.addAction(printButton) toolbar.addWidget(printSpacer) toolbar.addWidget(self.importsDiagramButton) toolbar.addWidget(self.runScriptButton) toolbar.addWidget(self.profileScriptButton) toolbar.addWidget(self.debugScriptButton) toolbar.addWidget(self.disasmScriptButton) toolbar.addAction(self.deadCodeScriptButton) toolbar.addWidget(undoSpacer) toolbar.addAction(self.__undoButton) toolbar.addAction(self.__redoButton) toolbar.addWidget(spacer) toolbar.addAction(self.removeTrailingSpacesButton) toolbar.addAction(self.expandTabsButton) self.importsBar = ImportListWidget(self.__editor) self.importsBar.hide() self.__outsideChangesBar = OutsideChangeWidget(self.__editor) self.__outsideChangesBar.sigReloadRequest.connect(self.__onReload) self.__outsideChangesBar.reloadAllNonModifiedRequest.connect( self.reloadAllNonModified) self.__outsideChangesBar.hide() hLayout = QHBoxLayout() hLayout.setContentsMargins(0, 0, 0, 0) hLayout.setSpacing(0) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) self.__navigationBar = NavigationBar(self.__editor, self) vLayout.addWidget(self.__navigationBar) vLayout.addWidget(self.__editor) hLayout.addLayout(vLayout) hLayout.addWidget(toolbar) widget = QWidget() widget.setLayout(hLayout) self.__splitter = QSplitter(Qt.Horizontal, self) self.__flowUI = FlowUIWidget(self.__editor, self) self.__mdView = MDWidget(self.__editor, self) self.__renderLayout = QVBoxLayout() self.__renderLayout.setContentsMargins(0, 0, 0, 0) self.__renderLayout.setSpacing(0) self.__renderLayout.addWidget(self.__flowUI) self.__renderLayout.addWidget(self.__mdView) self.__renderWidget = QWidget() self.__renderWidget.setLayout(self.__renderLayout) self.__splitter.addWidget(widget) self.__splitter.addWidget(self.__renderWidget) containerLayout = QHBoxLayout() containerLayout.setContentsMargins(0, 0, 0, 0) containerLayout.setSpacing(0) containerLayout.addWidget(self.__splitter) self.setLayout(containerLayout) self.__renderWidget.setVisible(False) self.__splitter.setSizes(Settings()['flowSplitterSizes']) self.__splitter.splitterMoved.connect(self.flowSplitterMoved) Settings().sigFlowSplitterChanged.connect(self.otherFlowSplitterMoved) def flowSplitterMoved(self, pos, index): """Splitter has been moved""" del pos # unused argument del index # unused argument Settings()['flowSplitterSizes'] = list(self.__splitter.sizes()) def otherFlowSplitterMoved(self): """Other window has changed the splitter position""" self.__splitter.setSizes(Settings()['flowSplitterSizes']) def updateStatus(self): """Updates the toolbar buttons status""" self.__updateRunDebugButtons() isPythonFile = isPythonMime(self.__editor.mime) self.importsDiagramButton.setEnabled( isPythonFile and GlobalData().graphvizAvailable) self.__editor.diagramsMenu.setEnabled( self.importsDiagramButton.isEnabled()) self.__editor.toolsMenu.setEnabled(self.runScriptButton.isEnabled()) def onNavigationBar(self): """Triggered when navigation bar focus is requested""" if self.__navigationBar.isVisible(): self.__navigationBar.setFocusToLastCombo() return True def __onPrint(self): """Triggered when the print button is pressed""" self.__editor._onShortcutPrint() def __onPrintPreview(self): """Triggered when the print preview button is pressed""" pass def __onDeadCode(self): """Triggered when vulture analysis is requested""" GlobalData().mainWindow.tabDeadCodeClicked() def __redoAvailable(self, available): """Reports redo ops available""" self.__redoButton.setEnabled(available) def __undoAvailable(self, available): """Reports undo ops available""" self.__undoButton.setEnabled(available) def __languageChanged(self, _=None): """Language changed""" isPython = self.__editor.isPythonBuffer() isMarkdown = self.__editor.isMarkdownBuffer() self.disasmScriptButton.setEnabled(isPython) self.__renderWidget.setVisible(not Settings()['floatingRenderer'] and (isPython or isMarkdown)) # Arguments: modified def modificationChanged(self, _=None): """Triggered when the content is changed""" self.__updateRunDebugButtons() def __updateRunDebugButtons(self): """Enables/disables the run and debug buttons as required""" enable = isPythonMime(self.__editor.mime) and \ not self.isModified() and \ not self.__debugMode and \ os.path.isabs(self.__fileName) if enable != self.runScriptButton.isEnabled(): self.runScriptButton.setEnabled(enable) self.profileScriptButton.setEnabled(enable) self.debugScriptButton.setEnabled(enable) self.deadCodeScriptButton.setEnabled(enable) self.sigTabRunChanged.emit(enable) def isTabRunEnabled(self): """Tells the status of run-like buttons""" return self.runScriptButton.isEnabled() def replaceAll(self, newText): """Replaces the current buffer content with a new text""" # Unfortunately, the setText() clears the undo history so it cannot be # used. The selectAll() and replacing selected text do not suite # because after undo the cursor does not jump to the previous position. # So, there is an ugly select -> replace manipulation below... with self.__editor: origLine, origPos = self.__editor.cursorPosition self.__editor.setSelection(0, 0, origLine, origPos) self.__editor.removeSelectedText() self.__editor.insert(newText) self.__editor.setCurrentPosition(len(newText)) line, pos = self.__editor.cursorPosition lastLine = self.__editor.lines() self.__editor.setSelection(line, pos, lastLine - 1, len(self.__editor.text(lastLine - 1))) self.__editor.removeSelectedText() self.__editor.cursorPosition = origLine, origPos # These two for the proper cursor positioning after redo self.__editor.insert("s") self.__editor.cursorPosition = origLine, origPos + 1 self.__editor.deleteBack() self.__editor.cursorPosition = origLine, origPos def onRemoveTrailingWS(self): """Triggers when the trailing spaces should be wiped out""" self.__editor.removeTrailingWhitespaces() def onExpandTabs(self): """Expands tabs if there are any""" self.__editor.expandTabs(4) def setFocus(self): """Overridden setFocus""" if self.__outsideChangesBar.isHidden(): self.__editor.setFocus() else: self.__outsideChangesBar.setFocus() def onImportDgmTuned(self): """Runs the settings dialog first""" if self.isModified(): what = ImportsDiagramDialog.SingleBuffer if not os.path.isabs(self.getFileName()): logging.warning("Imports diagram can only be generated for " "a file. Save the editor buffer " "and try again.") return else: what = ImportsDiagramDialog.SingleFile dlg = ImportsDiagramDialog(what, self.getFileName(), self) if dlg.exec_() == QDialog.Accepted: # Should proceed with the diagram generation self.__generateImportDiagram(what, dlg.options) # Arguments: action def onImportDgm(self, _=None): """Runs the generation process with default options""" if self.isModified(): what = ImportsDiagramDialog.SingleBuffer if not os.path.isabs(self.getFileName()): logging.warning("Imports diagram can only be generated for " "a file. Save the editor buffer " "and try again.") return else: what = ImportsDiagramDialog.SingleFile self.__generateImportDiagram(what, ImportDiagramOptions()) def __generateImportDiagram(self, what, options): """Show the generation progress and display the diagram""" if self.isModified(): progressDlg = ImportsDiagramProgress(what, options, self.getFileName(), self.__editor.text) tooltip = "Generated for modified buffer (" + \ self.getFileName() + ")" else: progressDlg = ImportsDiagramProgress(what, options, self.getFileName()) tooltip = "Generated for file " + self.getFileName() if progressDlg.exec_() == QDialog.Accepted: GlobalData().mainWindow.openDiagram(progressDlg.scene, tooltip) def onOpenImport(self): """Triggered when Ctrl+I is received""" if isPythonMime(self.__editor.mime): # Take all the file imports and resolve them fileImports = getImportsList(self.__editor.text) if not fileImports: GlobalData().mainWindow.showStatusBarMessage( "There are no imports") else: self.__onImportList(self.__fileName, fileImports) def __onImportList(self, fileName, imports): """Works with a list of imports""" # It has already been checked that the file is a Python one resolvedList, errors = resolveImports(fileName, imports) del errors # errors are OK here if resolvedList: # Display the import selection widget self.importsBar.showResolvedImports(resolvedList) else: GlobalData().mainWindow.showStatusBarMessage( "Could not resolve any imports") def resizeEvent(self, event): """Resizes the import selection dialogue if necessary""" self.__editor.hideCompleter() QWidget.resizeEvent(self, event) self.resizeBars() def resizeBars(self): """Resize the bars if they are shown""" if not self.importsBar.isHidden(): self.importsBar.resize() if not self.__outsideChangesBar.isHidden(): self.__outsideChangesBar.resize() self.__editor.resizeCalltip() def showOutsideChangesBar(self, allEnabled): """Shows the bar for the editor for the user to choose the action""" self.setReloadDialogShown(True) self.__outsideChangesBar.showChoice(self.isModified(), allEnabled) def __onReload(self): """Triggered when a request to reload the file is received""" self.sigReloadRequest.emit() def reload(self): """Called (from the editors manager) to reload the file""" # Re-read the file with updating the file timestamp self.readFile(self.__fileName) # Hide the bars, just in case both of them if not self.importsBar.isHidden(): self.importsBar.hide() if not self.__outsideChangesBar.isHidden(): self.__outsideChangesBar.hide() # Set the shown flag self.setReloadDialogShown(False) def reloadAllNonModified(self): """Request to reload all the non-modified files""" self.reloadAllNonModifiedRequest.emit() @staticmethod def onRunScript(action=None): """Runs the script""" del action # unused argument GlobalData().mainWindow.onRunTab() @staticmethod def onRunScriptDlg(): """Shows the run parameters dialogue""" GlobalData().mainWindow.onRunTabDlg() @staticmethod def onProfileScript(action=None): """Profiles the script""" del action # unused argument GlobalData().mainWindow.onProfileTab() @staticmethod def onProfileScriptDlg(): """Shows the profile parameters dialogue""" GlobalData().mainWindow.onProfileTabDlg() @staticmethod def onDebugScript(action=None): """Starts debugging""" del action # unused argument GlobalData().mainWindow.onDebugTab() @staticmethod def onDebugScriptDlg(): """Shows the debug parameters dialogue""" GlobalData().mainWindow.onDebugTabDlg() def getCFEditor(self): """Provides a reference to the control flow widget""" return self.__flowUI def cflowSyncRequested(self, absPos, line, pos): """Highlight the item closest to the absPos""" self.__flowUI.highlightAtAbsPos(absPos, line, pos) def passFocusToFlow(self): """Sets the focus to the graphics part""" if isPythonMime(self.__editor.mime): self.__flowUI.setFocus() return True return False def getMDView(self): """Provides a reference to the MD rendered view""" return self.__mdView # Mandatory interface part is below def getEditor(self): """Provides the editor widget""" return self.__editor def isModified(self): """Tells if the file is modified""" return self.__editor.document().isModified() def getRWMode(self): """Tells if the file is read only""" if not os.path.exists(self.__fileName): return None return 'RW' if QFileInfo(self.__fileName).isWritable() else 'RO' def getMime(self): """Provides the buffer mime""" return self.__editor.mime @staticmethod def getType(): """Tells the widget type""" return MainWindowTabWidgetBase.PlainTextEditor def getLanguage(self): """Tells the content language""" editorLanguage = self.__editor.language() if editorLanguage: return editorLanguage return self.__editor.mime if self.__editor.mime else 'n/a' def getFileName(self): """Tells what file name of the widget content""" return self.__fileName def setFileName(self, name): """Sets the file name""" self.__fileName = name self.__shortName = os.path.basename(name) def getEol(self): """Tells the EOL style""" return self.__editor.getEolIndicator() def getLine(self): """Tells the cursor line""" line, _ = self.__editor.cursorPosition return line def getPos(self): """Tells the cursor column""" _, pos = self.__editor.cursorPosition return pos def getEncoding(self): """Tells the content encoding""" if self.__editor.explicitUserEncoding: return self.__editor.explicitUserEncoding return self.__editor.encoding def getShortName(self): """Tells the display name""" return self.__shortName def setShortName(self, name): """Sets the display name""" self.__shortName = name def isDiskFileModified(self): """Return True if the loaded file is modified""" if not os.path.isabs(self.__fileName): return False if not os.path.exists(self.__fileName): return True path = os.path.realpath(self.__fileName) return self.__diskModTime != os.path.getmtime(path) or \ self.__diskSize != os.path.getsize(path) def doesFileExist(self): """Returns True if the loaded file still exists""" return os.path.exists(self.__fileName) def setReloadDialogShown(self, value=True): """Memorizes if the reloading dialogue has already been displayed""" self.__reloadDlgShown = value def getReloadDialogShown(self): """Tells if the reload dialog has already been shown""" return self.__reloadDlgShown and \ not self.__outsideChangesBar.isVisible() def updateModificationTime(self, fileName): """Updates the modification time""" path = os.path.realpath(fileName) self.__diskModTime = os.path.getmtime(path) self.__diskSize = os.path.getsize(path) def setDebugMode(self, debugOn, disableEditing): """Called to switch debug/development""" self.__debugMode = debugOn self.__editor.setDebugMode(debugOn, disableEditing) if debugOn: if disableEditing: # Undo/redo self.__undoButton.setEnabled(False) self.__redoButton.setEnabled(False) # Spaces/tabs/line self.removeTrailingSpacesButton.setEnabled(False) self.expandTabsButton.setEnabled(False) else: # Undo/redo self.__undoButton.setEnabled( self.__editor.document().isUndoAvailable()) self.__redoButton.setEnabled( self.__editor.document().isRedoAvailable()) # Spaces/tabs self.removeTrailingSpacesButton.setEnabled(True) self.expandTabsButton.setEnabled(True) # Run/debug buttons self.__updateRunDebugButtons() def isLineBreakable(self, line=None, enforceRecalc=False, enforceSure=False): """True if a breakpoint could be placed on the current line""" return self.__editor.isLineBreakable() def getVCSStatus(self): """Provides the VCS status""" return self.__vcsStatus def setVCSStatus(self, newStatus): """Sets the new VCS status""" self.__vcsStatus = newStatus # Floating renderer support def popRenderingWidgets(self): """Pops the rendering widgets""" self.__renderLayout.removeWidget(self.__flowUI) self.__renderLayout.removeWidget(self.__mdView) self.__renderWidget.setVisible(False) return [self.__flowUI, self.__mdView] def pushRenderingWidgets(self, widgets): """Returns back the rendering widgets""" for widget in widgets: self.__renderLayout.addWidget(widget) self.__languageChanged() # Sets the widget visibility
class PlantUMLCache(QObject): """The plantUML render cache""" sigRenderReady = pyqtSignal(str, str) # uuid, file def __init__(self, cacheDir): QObject.__init__(self) self.__md5ToFileName = {} self.__threads = {} self.__cacheDir = os.path.normpath(cacheDir) + os.path.sep if os.path.exists(self.__cacheDir): if os.path.isdir(self.__cacheDir): if os.access(self.__cacheDir, os.W_OK): self.__loadCache() self.__saveCache() else: logging.error('The plantUML render cache directory (' + self.__cacheDir + ') does not ' 'have write permissions. There will be no ' 'plantUML rendering') self.__cacheDir = None else: logging.error('The plantUML render cache directory path (' + self.__cacheDir + ') exists and ' 'is not a directory. There will be no pluntUML ' 'rendering') self.__cacheDir = None else: # Try to create the dir try: os.mkdir(self.__cacheDir) except Exception as exc: logging.error( 'Error creating pluntUML render cache directory ' + self.__cacheDir + ': ' + str(exc) + ' There will be no plantUML rendering') self.__cacheDir = None def __loadCache(self): """Loads the cache from the disk files""" if self.__cacheDir is None: return # Remove too old files now = datetime.datetime.now() limit = now - datetime.timedelta(days=1) limit = limit.timestamp() for item in os.listdir(self.__cacheDir): if item == CACHE_FILE_NAME: continue if os.path.isfile(self.__cacheDir + item): modtime = os.path.getmtime(self.__cacheDir + item) if modtime < limit: try: os.unlink(self.__cacheDir + item) except Exception as exc: logging.error('Error removing obsolete plantUML ' 'render file (' + self.__cacheDir + item + '): ' + str(exc)) if os.path.exists(self.__cacheDir + CACHE_FILE_NAME): prevCache = loadJSON(self.__cacheDir + CACHE_FILE_NAME, 'plantUML render cache map', None) for item in prevCache.items(): if os.path.exists(item[1]): self.__md5ToFileName[item[0]] = item[1] def __saveCache(self): """Saves the cache to the disk""" if self.__cacheDir is None: return dictToSave = {} for item in self.__md5ToFileName.items(): if item[1] is not None: dictToSave[item[0]] = item[1] saveJSON(self.__cacheDir + CACHE_FILE_NAME, dictToSave, 'plantUML render cache map') def onRenderOK(self, md5, uuid, fName): """Render saved successfully""" self.__md5ToFileName[md5] = fName self.__onThreadFinish(fName) self.__saveCache() self.sigRenderReady.emit(uuid, fName) def onRenderError(self, md5, fName): """Error rendering""" self.__md5ToFileName[md5] = None self.__onThreadFinish(fName) def __onThreadFinish(self, fName): """Cleans up after a retrieval thread""" thread = self.__threads.get(fName, None) if thread is not None: self.__disconnectThread(thread) thread.wait() self.__threads.pop(fName) def __connectThread(self, thread): """Connects the thread signals""" thread.sigFinishedOK.connect(self.onRenderOK) thread.sigFinishedError.connect(self.onRenderError) def __disconnectThread(self, thread): """Connects the thread signals""" try: thread.sigFinishedOK.disconnect(self.onRenderOK) thread.sigFinishedError.disconnect(self.onRenderError) except: pass def getResource(self, source, uuid): """Provides the rendered file name If None => no rendering will be done Otherwise the ready-to-use file or where the pic is expected """ if self.__cacheDir is None or JAR_PATH is None: return None normSource = normalizePlantumlSource(source) md5 = hashlib.md5(normSource.encode('utf-8')).hexdigest() if md5 in self.__md5ToFileName: return self.__md5ToFileName[md5] basename = md5 + '.png' fName = self.__cacheDir + basename if fName in self.__threads: # Reject double request return fName thread = PlantUMLRenderer() self.__threads[fName] = thread self.__connectThread(thread) thread.get(normSource, md5, fName, uuid) return fName
class SettingsWrapper(QObject, DebuggerEnvironment, SearchEnvironment, FileSystemEnvironment, RunParametersCache, FilePositions, FileEncodings, FlowUICollapsedGroups): """Provides settings singleton facility""" MAX_SMART_ZOOM = 3 sigRecentListChanged = pyqtSignal() sigFlowSplitterChanged = pyqtSignal() sigFlowZoomChanged = pyqtSignal() sigTextZoomChanged = pyqtSignal() sigHideDocstringsChanged = pyqtSignal() sigHideCommentsChanged = pyqtSignal() sigHideExceptsChanged = pyqtSignal() sigSmartZoomChanged = pyqtSignal() sigRecentFilesChanged = pyqtSignal() def __init__(self): QObject.__init__(self) DebuggerEnvironment.__init__(self) SearchEnvironment.__init__(self) FileSystemEnvironment.__init__(self) RunParametersCache.__init__(self) FilePositions.__init__(self) FileEncodings.__init__(self) FlowUICollapsedGroups.__init__(self) self.minTextZoom = None self.minCFlowZoom = None self.__values = deepcopy(_DEFAULT_SETTINGS) # make sure that the directory exists if not os.path.exists(SETTINGS_DIR): os.makedirs(SETTINGS_DIR) RunParametersCache.setup(self, SETTINGS_DIR) DebuggerEnvironment.setup(self, SETTINGS_DIR) SearchEnvironment.setup(self, SETTINGS_DIR) FileSystemEnvironment.setup(self, SETTINGS_DIR) FilePositions.setup(self, SETTINGS_DIR) FileEncodings.setup(self, SETTINGS_DIR) FlowUICollapsedGroups.setup(self, SETTINGS_DIR) self.webResourceCache = WebResourceCache(SETTINGS_DIR + os.path.sep + 'webresourcecache') self.plantUMLCache = PlantUMLCache(SETTINGS_DIR + os.path.sep + 'plantumlcache') # Save the config file name self.__fullFileName = SETTINGS_DIR + "settings.json" # Create file if does not exist if not os.path.exists(self.__fullFileName): # Save to file self.flush() return readErrors = [] # Load the previous session settings try: with open(self.__fullFileName, "r", encoding=SETTINGS_ENCODING) as diskfile: diskValues = json.load(diskfile, object_hook=settingsFromJSON) except Exception as exc: # Bad error - save default self.__saveErrors('Could not read setting from ' + self.__fullFileName + ': ' + str(exc) + 'Overwriting with the default settings...') self.flush() return for item, val in diskValues.items(): if item in self.__values: if type(self.__values[item]) != type(val): readErrors.append("Settings '" + item + "' type from the disk file " + self.__fullFileName + ' does not match the expected one. ' 'The default value is used.') else: self.__values[item] = val else: readErrors.append('Disk file ' + self.__fullFileName + " contains extra value '" + item + "'. It will be lost.") # If format is bad then overwrite the file if readErrors: self.__saveErrors("\n".join(readErrors)) self.flush() SearchEnvironment.setLimit(self, self.__values['maxSearchEntries']) FileSystemEnvironment.setLimit(self, self.__values['maxRecentFiles']) @staticmethod def __saveErrors(message): """Appends the message to the startup errors file""" try: with open(SETTINGS_DIR + 'startupmessages.log', 'a', encoding=SETTINGS_ENCODING) as diskfile: diskfile.write('------ Startup report at ' + str(datetime.datetime.now()) + '\n') diskfile.write(message) diskfile.write('\n------\n\n') except: # This is not that important pass def flush(self): """Writes the settings to the disk""" try: with open(self.__fullFileName, 'w', encoding=SETTINGS_ENCODING) as diskfile: json.dump(self.__values, diskfile, default=settingsToJSON, indent=4) except Exception as exc: logging.error('Error saving setting (to %s): %s', self.__fullFileName, str(exc)) def addRecentProject(self, projectFile, needFlush=True): """Adds the recent project to the list""" absProjectFile = os.path.realpath(projectFile) recentProjects = self.__values['recentProjects'] if absProjectFile in recentProjects: recentProjects.remove(absProjectFile) recentProjects.insert(0, absProjectFile) limit = self.__values['maxRecentProjects'] if len(recentProjects) > limit: recentProjects = recentProjects[0:limit] self.__values['recentProjects'] = recentProjects if needFlush: self.flush() self.sigRecentListChanged.emit() def deleteRecentProject(self, projectFile, needFlush=True): """Deletes the recent project from the list""" absProjectFile = os.path.realpath(projectFile) recentProjects = self.__values['recentProjects'] if absProjectFile in recentProjects: recentProjects.remove(absProjectFile) self.__values['recentProjects'] = recentProjects if needFlush: self.flush() self.sigRecentListChanged.emit() @staticmethod def getDefaultGeometry(): """Provides the default window size and location""" return _DEFAULT_SETTINGS['xpos'], _DEFAULT_SETTINGS['ypos'], \ _DEFAULT_SETTINGS['width'], _DEFAULT_SETTINGS['height'] @staticmethod def getDefaultRendererWindowGeometry(): """Provides the default renderer window size and location""" # A bit shifted down and half of the width of the main window return _DEFAULT_SETTINGS['rendererxpos'], \ _DEFAULT_SETTINGS['rendererypos'], \ _DEFAULT_SETTINGS['rendererwidth'], \ _DEFAULT_SETTINGS['rendererheight'] def getProfilerSettings(self): """Provides the profiler IDE-wide settings""" return self.__values['profilerLimits'] def setProfilerSettings(self, newValue, needFlush=True): """Updates the profiler settings""" if self.__values['profilerLimits'] != newValue: self.__values['profilerLimits'] = newValue if needFlush: self.flush() def getDebuggerSettings(self): """Provides the debugger IDE-wide settings""" return self.__values['debuggerSettings'] def setDebuggerSettings(self, newValue, needFlush=True): """Updates the debugger settings""" if self.__values['debuggerSettings'] != newValue: self.__values['debuggerSettings'] = newValue if needFlush: self.flush() def validateZoom(self, minTextZoom, minCFlowZoom): """Validates the min zoom values""" self.minTextZoom = minTextZoom self.minCFlowZoom = minCFlowZoom warnings = [] if self.__values['zoom'] < minTextZoom: warnings.append('The current text zoom (' + str(self.__values['zoom']) + ') will be adjusted to ' + str(minTextZoom) + ' due to it is less than min fonts allowed.') self.__values['zoom'] = minTextZoom if self.__values['flowZoom'] < minCFlowZoom: warnings.append('The current flow zoom (' + str(self.__values['flowZoom']) + ') will be adjusted to ' + str(minCFlowZoom) + ' due to it is less than min fonts allowed.') self.__values['flowZoom'] = minCFlowZoom if self.__values['smartZoom'] < 0: warnings.append('The current smart zoom (' + str(self.__values['smartZoom']) + ') will be adjusted to 0 due to it must be >= 0') self.__values['smartZoom'] = 0 elif self.__values['smartZoom'] > SettingsWrapper.MAX_SMART_ZOOM: warnings.append('The current smart zoom (' + str(self.__values['smartZoom']) + ') will be adjusted to ' + str(SettingsWrapper.MAX_SMART_ZOOM) + ' due to it is larger than max allowed.') self.__values['smartZoom'] = SettingsWrapper.MAX_SMART_ZOOM if warnings: self.flush() return warnings def __getitem__(self, key): return self.__values[key] def __setitem__(self, key, value): self.__values[key] = value if key == 'flowSplitterSizes': self.sigFlowSplitterChanged.emit() elif key == 'hidedocstrings': self.sigHideDocstringsChanged.emit() elif key == 'hidecomments': self.sigHideCommentsChanged.emit() elif key == 'hideexcepts': self.sigHideExceptsChanged.emit() self.flush() def onTextZoomIn(self): """Triggered when the text is zoomed in""" self.__values['zoom'] += 1 self.flush() self.sigTextZoomChanged.emit() def onTextZoomOut(self): """Triggered when the text is zoomed out""" if self.__values['zoom'] > self.minTextZoom: self.__values['zoom'] -= 1 self.flush() self.sigTextZoomChanged.emit() def onTextZoomReset(self): """Triggered when the text zoom is reset""" if self.__values['zoom'] != 0: self.__values['zoom'] = 0 self.flush() self.sigTextZoomChanged.emit() def onSmartZoomIn(self): """Triggered when the smart zoom is changed""" if self.__values['smartZoom'] < SettingsWrapper.MAX_SMART_ZOOM: self.__values['smartZoom'] += 1 self.flush() self.sigSmartZoomChanged.emit() def onSmartZoomOut(self): """Triggered when the smart zoom is changed""" if self.__values['smartZoom'] > 0: self.__values['smartZoom'] -= 1 self.flush() self.sigSmartZoomChanged.emit() def onFlowZoomIn(self): """Triggered when the flow is zoomed in""" self.__values['flowZoom'] += 1 self.flush() self.sigFlowZoomChanged.emit() def onFlowZoomOut(self): """Triggered when the flow is zoomed out""" if self.__values['flowZoom'] > self.minCFlowZoom: self.__values['flowZoom'] -= 1 self.flush() self.sigFlowZoomChanged.emit() def onFlowZoomReset(self): """Triggered when the flow zoom is reset""" if self.__values['flowZoom'] != 0: self.__values['flowZoom'] = 0 self.flush() self.sigFlowZoomChanged.emit() def addRecentFile(self, path): """Adds a recent file. True if a new file was inserted.""" ret = FileSystemEnvironment.addRecentFile(self, path) if ret: self.sigRecentFilesChanged.emit() return ret
class DiagramWidget(QGraphicsView): """Widget to show a generated diagram""" sigEscapePressed = pyqtSignal() def __init__(self, parent=None): QGraphicsView.__init__(self, parent) # self.setRenderHint(QPainter.Antialiasing) # self.setRenderHint(QPainter.TextAntialiasing) def keyPressEvent(self, event): """Handles the key press events""" if event.key() == Qt.Key_Escape: self.sigEscapePressed.emit() event.accept() elif event.key() == Qt.Key_C and \ event.modifiers() == Qt.ControlModifier: self.onCopy() event.accept() else: QGraphicsView.keyPressEvent(self, event) def setScene(self, scene): """Sets the scene to display""" scene.setBackgroundBrush(GlobalData().skin['nolexerPaper']) QGraphicsView.setScene(self, scene) def resetZoom(self): """Resets the zoom""" self.resetTransform() def zoomIn(self): """Zoom when a button clicked""" factor = 1.41**(120.0 / 240.0) self.scale(factor, factor) def zoomOut(self): """Zoom when a button clicked""" factor = 1.41**(-120.0 / 240.0) self.scale(factor, factor) def wheelEvent(self, event): """Mouse wheel event""" if QApplication.keyboardModifiers() == Qt.ControlModifier: if event.angleDelta().y() < 0: self.zoomOut() else: self.zoomIn() else: QGraphicsView.wheelEvent(self, event) def __getImage(self): """Renders the diagram to an image""" scene = self.scene() image = QImage(scene.width(), scene.height(), QImage.Format_ARGB32_Premultiplied) painter = QPainter(image) # If switched on then rectangles edges will not be sharp # painter.setRenderHint( QPainter.Antialiasing ) scene.render(painter) painter.end() return image def onCopy(self): """Copies the diagram to the exchange buffer""" QApplication.clipboard().setImage(self.__getImage()) def onSaveAs(self, fName): """Saves the rendered image to a file""" self.__getImage().save(fName, "PNG")
class QutepartWrapper(Qutepart): """Convenience qutepart wrapper""" sigHighlighted = pyqtSignal(str, int, int) # Shared between buffers matchesRegexp = None highlightOn = False def __init__(self, parent): Qutepart.__init__(self, parent) self.encoding = None self.explicitUserEncoding = None self.mime = None # Remove all the default margins self.getMargin('mark_area').setVisible(False) self.blockCountChanged.disconnect(self.getMargin('mark_area').update) self.delMargin('mark_area') self._markArea = None self.getMargin('line_numbers').setVisible(False) self.delMargin('line_numbers') self.completionEnabled = False self._completer.terminate() self.textChanged.disconnect(self._completer._onTextChanged) self.document().modificationChanged.disconnect( self._completer._onModificationChanged) # Search/replace support self.__matchesCache = None self.textChanged.connect(self.__resetMatchCache) def _dropUserExtraSelections(self): """Suppressing highlight removal when the text is changed""" pass def setPaper(self, paperColor): """Sets the new paper color""" palette = self.palette() palette.setColor(QPalette.Active, QPalette.Base, paperColor) palette.setColor(QPalette.Inactive, QPalette.Base, paperColor) self.setPalette(palette) def setColor(self, textColor): """Sets the new text color""" palette = self.palette() palette.setColor(QPalette.Active, QPalette.Text, textColor) palette.setColor(QPalette.Inactive, QPalette.Text, textColor) self.setPalette(palette) def onTextZoomChanged(self): """Triggered when a text zoom is changed""" self.setFont(getZoomedMonoFont()) for margin in self.getMargins(): if hasattr(margin, 'onTextZoomChanged'): margin.onTextZoomChanged() self._setSolidEdgeGeometry() def clearUndoRedoHistory(self): """Clears the undo/redo history""" self.document().clearUndoRedoStacks() def getEolIndicator(self): """Provides the eol indicator for the current eol mode""" if self.eol == '\r\n': return "CRLF" if self.eol == '\r': return 'CR' return 'LF' def firstVisibleLine(self): """Provides the first visible line. 0-based""" return self.firstVisibleBlock().blockNumber() def setFirstVisible(self, lineno): """Scrolls the editor to make sure the first visible line is lineno""" currentVisible = self.firstVisibleLine() if currentVisible == lineno: return # Initial setting self.verticalScrollBar().setValue(lineno) currentVisible = self.firstVisibleLine() while currentVisible != lineno: vbValue = self.verticalScrollBar().value() distance = lineno - currentVisible if distance > 0: distance = min(2, distance) else: distance = max(-2, distance) self.verticalScrollBar().setValue(vbValue + distance) vbValueAfter = self.verticalScrollBar().value() if vbValueAfter == vbValue: break currentVisible = self.firstVisibleLine() self.setHScrollOffset(0) def lastVisibleLine(self): """Provides the last visible line. 0-based""" editorHeight = self.height() hBar = self.horizontalScrollBar() if hBar: if hBar.isVisible(): editorHeight -= hBar.height() block = self.firstVisibleBlock() lastVisible = block.blockNumber() blocksHeight = 0.0 while block.isValid(): if not block.isValid(): break blocksHeight += self.blockBoundingRect(block).height() if blocksHeight > editorHeight: break lastVisible = block.blockNumber() block = block.next() return lastVisible def isLineOnScreen(self, line): """True if the line is on screen. line is 0-based.""" if line < self.firstVisibleLine(): return False return line <= self.lastVisibleLine() def ensureLineOnScreen(self, line): """Makes sure the line is visible on screen. line is 0-based.""" # Prerequisite: the cursor has to be on the desired position if not self.isLineOnScreen(line): self.ensureCursorVisible() def setHScrollOffset(self, value): """Sets the new horizontal scroll bar value""" hBar = self.horizontalScrollBar() if hBar: hBar.setValue(value) def moveToLineEnd(self): """Moves the cursor to the end of the line""" line, _ = self.cursorPosition self.cursorPosition = line, len(self.lines[line]) @staticmethod def firstNonSpaceIndex(text): """Provides a pos (0-based of a first non-space char in the text""" lStripped = text.lstrip() if lStripped: return len(text) - len(lStripped) return None def __getNewHomePos(self, toFirstNonSpace): """Provides the new cursor position for a HOME click""" line, pos = self.cursorPosition newPos = 0 if toFirstNonSpace: lStripped = self.lines[line].lstrip() if lStripped: calcPos = len(self.lines[line]) - len(lStripped) newPos = 0 if pos <= calcPos else calcPos return line, newPos def moveToLineBegin(self, toFirstNonSpace): """Jumps to the first non-space or to position 0""" newLine, newPos = self.__getNewHomePos(toFirstNonSpace) self.cursorPosition = newLine, newPos def selectTillLineBegin(self, toFirstNonSpace): """Selects consistently with HOME behavior""" newLine, newPos = self.__getNewHomePos(toFirstNonSpace) cursor = self.textCursor() cursor.setPosition(self.mapToAbsPosition(newLine, newPos), QTextCursor.KeepAnchor) self.setTextCursor(cursor) def onHome(self): """Triggered when HOME is received""" self.moveToLineBegin(Settings()['jumpToFirstNonSpace']) def onShiftHome(self): """Triggered when Shift+HOME is received""" self.selectTillLineBegin(Settings()['jumpToFirstNonSpace']) def onShiftEnd(self): """Selects till the end of line""" cursor = self.textCursor() cursor.movePosition(QTextCursor.EndOfLine, QTextCursor.KeepAnchor) self.setTextCursor(cursor) def onShiftDel(self): """Triggered when Shift+Del is received""" if self.selectedText: QApplication.clipboard().setText(self.selectedText) self.selectedText = '' else: line, _ = self.cursorPosition if self.lines[line]: QApplication.clipboard().setText(self.lines[line] + '\n') del self.lines[line] def onCtrlC(self): """Handles copying""" if self.selectedText: QApplication.clipboard().setText(self.selectedText) else: line, _ = self.cursorPosition if self.lines[line]: QApplication.clipboard().setText(self.lines[line] + '\n') def openAsFile(self): """Opens a selection or a current tag as a file""" path = self.selectedText.strip() if path == "" or '\n' in path or '\r' in path: return # Now the processing if os.path.isabs(path): GlobalData().mainWindow.detectTypeAndOpenFile(path) return # This is not an absolute path but could be a relative path for the # current buffer file. Let's try it. fileName = self._parent.getFileName() if fileName != "": # There is a file name fName = os.path.dirname(fileName) + os.path.sep + path fName = os.path.abspath(os.path.realpath(fName)) if os.path.exists(fName): GlobalData().mainWindow.detectTypeAndOpenFile(fName) return if GlobalData().project.isLoaded(): # Try it as a relative path to the project prjFile = GlobalData().project.fileName fName = os.path.dirname(prjFile) + os.path.sep + path fName = os.path.abspath(os.path.realpath(fName)) if os.path.exists(fName): GlobalData().mainWindow.detectTypeAndOpenFile(fName) return # The last hope - open as is if os.path.exists(path): path = os.path.abspath(os.path.realpath(path)) GlobalData().mainWindow.detectTypeAndOpenFile(path) return logging.error("Cannot find '" + path + "' to open") def downloadAndShow(self): """Triggered when the user wants to download and see the file""" url = self.selectedText.strip() if url.lower().startswith("www."): url = "http://" + url oldTimeout = socket.getdefaulttimeout() newTimeout = 5 # Otherwise the pause is too long socket.setdefaulttimeout(newTimeout) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: response = urllib.request.urlopen(url) content = decodeURLContent(response.read()) # The content has been read sucessfully mainWindow = GlobalData().mainWindow mainWindow.editorsManager().newTabClicked(content, os.path.basename(url)) except Exception as exc: logging.error("Error downloading '" + url + "'\n" + str(exc)) QApplication.restoreOverrideCursor() socket.setdefaulttimeout(oldTimeout) def openInBrowser(self): """Triggered when a selected URL should be opened in a browser""" url = self.selectedText.strip() if url.lower().startswith("www."): url = "http://" + url QDesktopServices.openUrl(QUrl(url)) def printUserData(self): """Debug purpose member to print the highlight data""" line, pos = self.cursorPosition if self._highlighter is None: print(str(line + 1) + ":" + str(pos + 1) + " no highlight") return block = self.document().findBlockByNumber(line) data = block.userData() if data is None: print(str(line + 1) + ":" + str(pos + 1) + " None") return print(str(line + 1) + ":" + str(pos + 1) + " " + repr(data.data)) def removeTrailingWhitespaces(self): """Removes trailing whitespaces""" # Note: two loops are for the case when there is nothing to expand. # The 'with' statement leads to a detection of a changed position # which triggers the other actions. index = 0 for index, line in enumerate(self.lines): stripped = line.rstrip() if stripped != line: break else: self.__showStatusBarMessage('No trailing spaces found') return with self: lastIndex = len(self.lines) - 1 while index <= lastIndex: stripped = self.lines[index].rstrip() if stripped != self.lines[index]: self.lines[index] = stripped index += 1 def expandTabs(self, tabsize): """Expands tabs if needed""" # Note: two loops are for the case when there is nothing to expand. # The 'with' statement leads to a detection of a changed position # which triggers the other actions. index = 0 for index, line in enumerate(self.lines): expanded = line.expandtabs(tabsize) if expanded != line: break else: self.__showStatusBarMessage('No tabs found') return with self: lastIndex = len(self.lines) - 1 while index <= lastIndex: expanded = self.lines[index].expandtabs(tabsize) if expanded != self.lines[index]: self.lines[index] = expanded index += 1 def getEncoding(self): """Provides the encoding""" if self.explicitUserEncoding: return self.explicitUserEncoding return self.encoding def isCommentLine(self, line): """True if it is a comment line. line is 0-based""" if line >= len(self.lines): return False txt = self.lines[line] nonSpaceIndex = self.firstNonSpaceIndex(txt) if nonSpaceIndex is None: return False if txt[nonSpaceIndex] != '#': return False return not self.isStringLiteral(line, nonSpaceIndex) def isLineEmpty(self, line): """Returns True if the line is empty. Line is 0 based""" return self.lines[line].strip() == "" def isStringLiteral(self, line, pos): """True if it is a string literal""" if self._highlighter is None: return False if pos < 0: # May happened if the line is empty return False block = self.document().findBlockByNumber(line) data = block.userData() if data is None: return False try: return self._highlighter._syntax._getTextType(data.data, pos) == 's' except IndexError: return False # Search supporting members def resetHighlight(self): """Resets the highlight if so""" self.resetMatchCache() self.setExtraSelections([]) QutepartWrapper.highlightOn = False def __resetMatchCache(self): """Resets the matches cache when the text is modified""" # The highlight does not need to be switched off self.__matchesCache = None def resetMatchCache(self): """Resets the matches cache. Happens when a search criteria changed in the otehr editors. """ if not self.isVisible(): if self._userExtraSelections: self.setExtraSelections([]) self.__matchesCache = None def __searchInText(self, regExp, startPoint, forward): """Search in text and return the nearest match""" self.findAllMatches(regExp) if self.__matchesCache: if forward: for match in self.__matchesCache: if match.start() >= startPoint: break else: # wrap, search from start match = self.__matchesCache[0] else: # reverse search for match in self.__matchesCache[::-1]: if match.start() <= startPoint: break else: # wrap, search from end match = self.__matchesCache[-1] return match return None def isCursorOnMatch(self): """True if the cursor is on the first pos of any match""" if self.__matchesCache: pos = self.absCursorPosition for match in self.__matchesCache: if match.start() == pos: return True return False def getCurrentMatchesCount(self): """Provides the number of the current matches""" if self.__matchesCache: return len(self.__matchesCache) return 0 def getMatchesInfo(self): """Returns match number or None and total number of matches""" matchNumber = None totalMatches = None if self.__matchesCache: pos = self.absCursorPosition totalMatches = 0 for match in self.__matchesCache: totalMatches += 1 if match.start() == pos: matchNumber = totalMatches return matchNumber, totalMatches def getCurrentOrSelection(self): """Provides what should be used for search. Returns a tuple: - word - True if it was a selection - start abs pos - end abs pos """ cursor = self.textCursor() if cursor.hasSelection(): word = cursor.selectedText() if '\r' not in word and '\n' not in word: return word, True, cursor.anchor(), cursor.position() cursor.select(QTextCursor.WordUnderCursor) return cursor.selectedText(), False, cursor.anchor(), cursor.position() def getCurrentWord(self): """Provides the current word if so""" cursor = self.textCursor() cursor.select(QTextCursor.WordUnderCursor) return cursor.selectedText() def getWordBeforeCursor(self): """Provides a word before the cursor""" cursor = self.textCursor() textBeforeCursor = cursor.block().text()[:cursor.positionInBlock()] match = WORD_AT_END_REGEXP.search(textBeforeCursor) return match.group(0) if match else '' def getWordAfterCursor(self): """Provides a word after cursor""" cursor = self.textCursor() textAfterCursor = cursor.block().text()[cursor.positionInBlock():] match = WORD_AT_START_REGEXP.search(textAfterCursor) return match.group(0) if match else '' def onFirstChar(self): """Jump to the first character in the buffer""" self.cursorPosition = 0, 0 self.ensureLineOnScreen(0) self.setHScrollOffset(0) def onLastChar(self): """Jump to the last char""" line = len(self.lines) if line != 0: line -= 1 pos = len(self.lines[line]) self.cursorPosition = line, pos self.ensureLineOnScreen(line) self.setHScrollOffset(0) def clearSelection(self): """Clears the current selection if so""" cursor = self.textCursor() cursor.clearSelection() def findAllMatches(self, regExp): """Find all matches of regExp""" if QutepartWrapper.matchesRegexp != regExp or \ self.__matchesCache is None: QutepartWrapper.matchesRegexp = regExp self.__matchesCache = [ match for match in regExp.finditer(self.text) ] QutepartWrapper.highlightOn = True return self.__matchesCache def updateFoundItemsHighlighting(self, regExp): """Updates the highlight. Returns False if there were too many.""" matches = self.findAllMatches(regExp) count = len(matches) if count > Settings()['maxHighlightedMatches']: self.setExtraSelections([]) return False self.setExtraSelections([(match.start(), len(match.group(0))) for match in matches]) return True def highlightRegexp(self, regExp, searchPos, forward, needMessage=True): """Highlights the matches, moves cursor, displays message""" highlighted = self.updateFoundItemsHighlighting(regExp) match = self.__searchInText(regExp, searchPos, forward) if match is not None: matchIndex = self.__matchesCache.index(match) + 1 totalMatches = len(self.__matchesCache) self.absCursorPosition = match.start() self.ensureCursorVisible() if needMessage: if highlighted: if self.__matchesCache: msg = 'match %d of %d' % (matchIndex, totalMatches) else: msg = 'no matches' else: msg = 'match %d of %d (too many to highlight, ' \ 'exceeds the limit of %d)' % \ (matchIndex, totalMatches, Settings()['maxHighlightedMatches']) self.__showStatusBarMessage(msg) return len(self.__matchesCache) def clearSearchIndicators(self): """Hides the search indicator""" self.resetHighlight() GlobalData().mainWindow.clearStatusBarMessage() def onHighlight(self): """Triggered when Ctrl+' is clicked""" if self.isCursorOnHighlight(): return self.onNextHighlight() word, wasSelection, _, absEnd = self.getCurrentOrSelection() if not word or '\r' in word or '\n' in word: return 0 # Reset match cashe in all the buffers # Otherwise they keep an old highlight when the user switches to them mainWindow = GlobalData().mainWindow mainWindow.editorsManager().resetTextSearchMatchCache() wordFlag = 0 if wasSelection: regExp = re.compile('%s' % re.escape(word), re.IGNORECASE) else: regExp = re.compile('\\b%s\\b' % re.escape(word), re.IGNORECASE) wordFlag = 1 count = self.highlightRegexp(regExp, absEnd, False) self.sigHighlighted.emit(word, wordFlag, count) return count def mouseDoubleClickEvent(self, event): """Highlight the current word keeping the selection""" Qutepart.mouseDoubleClickEvent(self, event) if self.selectedText: selectedPos = self.absSelectedPosition self.onHighlight() self.absSelectedPosition = selectedPos def onNextHighlight(self): """Triggered when Ctrl+. is clicked""" if QutepartWrapper.matchesRegexp is None: return self.onHighlight() if QutepartWrapper.highlightOn: # The current highlight is on return self.highlightRegexp(QutepartWrapper.matchesRegexp, self.absCursorPosition + 1, True) # Not highlighted. If within the currently matched word, then the # cursor should stay on it. wordBefore = self.getWordBeforeCursor() return self.highlightRegexp(QutepartWrapper.matchesRegexp, self.absCursorPosition - len(wordBefore), True) def onPrevHighlight(self): """Triggered when Ctrl+, is clicked""" if QutepartWrapper.matchesRegexp is None: return self.onHighlight() if QutepartWrapper.highlightOn: # The current highlight is on return self.highlightRegexp(QutepartWrapper.matchesRegexp, self.absCursorPosition - 1, False) # Not highlighted. If within the currently matched word, then the # cursor should stay on it. return self.highlightRegexp(QutepartWrapper.matchesRegexp, self.absCursorPosition, False) def replaceAllMatches(self, replaceText): """Replaces all the current matches with the other text""" if not self.__matchesCache: return replaceCount = 0 noReplaceCount = 0 for match in self.__matchesCache[::-1]: textToReplace = self.text[match.start():match.start() + len(match.group(0))] if textToReplace == replaceText: noReplaceCount += 1 else: replaceCount += 1 if replaceCount > 0: cursorPos = None delta = 0 regExp = QutepartWrapper.matchesRegexp with self: # reverse order, because replacement may move indexes for match in self.__matchesCache[::-1]: textToReplace = self.text[match.start():match.start() + len(match.group(0))] if textToReplace != replaceText: self.replaceText(match.start(), len(match.group(0)), replaceText) if cursorPos is None: cursorPos = self.absCursorPosition else: delta += len(replaceText) - len(textToReplace) self.resetHighlight() self.updateFoundItemsHighlighting(regExp) self.absCursorPosition = cursorPos + delta if replaceCount == 1: msg = '1 match replaced' else: msg = '%d matches replaced' % replaceCount if noReplaceCount > 0: msg += '; %d skipped ' \ '(the highlight matches replacement)' % noReplaceCount self.__showStatusBarMessage(msg) def replaceMatch(self, replaceText): """Replaces the match on which the cursor is""" if self.__matchesCache: pos = self.absCursorPosition for match in self.__matchesCache: if match.start() == pos: regExp = QutepartWrapper.matchesRegexp textToReplace = self.text[match.start():match.start() + len(match.group(0))] if textToReplace == replaceText: msg = "no replace: the highlight matches replacement" else: self.replaceText(match.start(), len(match.group(0)), replaceText) self.__matchesCache = None self.updateFoundItemsHighlighting(regExp) msg = "1 match replaced" self.__showStatusBarMessage(msg) break def isCursorOnHighlight(self): """True if the current cursor position is on the highlighted text""" if QutepartWrapper.highlightOn: pos = self.absCursorPosition self.findAllMatches(QutepartWrapper.matchesRegexp) for match in self.__matchesCache: if pos >= match.start() and pos < match.end(): return True return False def onTabChanged(self): """Called by find/replace widget""" if QutepartWrapper.highlightOn: # Highlight the current regexp without changing the cursor position self.updateFoundItemsHighlighting(QutepartWrapper.matchesRegexp) else: self.resetHighlight() @staticmethod def __showStatusBarMessage(msg): """Shows a main window status bar message""" mainWindow = GlobalData().mainWindow mainWindow.showStatusBarMessage(msg, 8000) def getEndPosition(self): """Provides the end position, 0 based""" line = len(self.lines) - 1 return (line, len(self.lines[line])) def append(self, text): """Appends the given text to the end""" if not text: return # Tail separator could be stripped otherwise; also there are so many # separators: # https://docs.python.org/3.5/library/stdtypes.html#str.splitlines partsNoEnd = text.splitlines() partsWithEnd = text.splitlines(True) lastIndex = len(partsNoEnd) - 1 with self: for index, value in enumerate(partsNoEnd): if value: self.lines[-1] += value if index == lastIndex: if value != partsWithEnd[index]: self.lines.append('') else: self.lines.append('') def gotoLine(self, line, pos=None, firstVisible=None): """Jumps to the given position and scrolls if needed. line and pos and firstVisible are 1-based """ # Normalize editor line and pos editorLine = line - 1 if editorLine < 0: editorLine = 0 if pos is None or pos <= 0: editorPos = 0 else: editorPos = pos - 1 if self.isLineOnScreen(editorLine): if firstVisible is None: self.cursorPosition = editorLine, editorPos return self.ensureLineOnScreen(editorLine) # Otherwise we would deal with scrolling any way, so normalize # the first visible line if firstVisible is None: editorFirstVisible = editorLine - 1 else: editorFirstVisible = firstVisible - 1 if editorFirstVisible < 0: editorFirstVisible = 0 self.cursorPosition = editorLine, editorPos self.setFirstVisible(editorFirstVisible) def openAsFileAvailable(self): """True if there is something to try to open as a file""" selectedText = self.selectedText.strip() if selectedText: return '\n' not in selectedText and '\r' not in selectedText return False def downloadAndShowAvailable(self): """True if download and show available""" selectedText = self.selectedText.strip() if '\n' not in selectedText and '\r' not in selectedText: return selectedText.lower().startswith('http://') or \ selectedText.lower().startswith('www.') return False
class SubversionPlugin(SVNMenuMixin, SVNInfoMixin, SVNAddMixin, SVNCommitMixin, SVNDeleteMixin, SVNDiffMixin, SVNRevertMixin, SVNUpdateMixin, SVNAnnotateMixin, SVNStatusMixin, SVNLogMixin, SVNPropsMixin, VersionControlSystemInterface): """Codimension subversion plugin""" PathChanged = pyqtSignal(str) def __init__(self): VersionControlSystemInterface.__init__(self) SVNInfoMixin.__init__(self) SVNAddMixin.__init__(self) SVNCommitMixin.__init__(self) SVNDeleteMixin.__init__(self) SVNDiffMixin.__init__(self) SVNRevertMixin.__init__(self) SVNUpdateMixin.__init__(self) SVNAnnotateMixin.__init__(self) SVNStatusMixin.__init__(self) SVNLogMixin.__init__(self) SVNPropsMixin.__init__(self) SVNMenuMixin.__init__(self) self.projectSettings = None self.ideWideSettings = None self.__settingsLock = QMutex() self.fileParentMenu = None self.dirParentMenu = None @staticmethod def isIDEVersionCompatible(ideVersion): """SVN Plugin is compatible with any IDE version""" return True @staticmethod def getVCSName(): """Should provide the specific version control name, e.g. SVN""" return "SVN" def activate(self, ideSettings, ideGlobalData): """Called when the plugin is activated""" VersionControlSystemInterface.activate(self, ideSettings, ideGlobalData) # Read the settings self.ideWideSettings = getSettings(self.__getIDEConfigFile()) if self.ide.project.isLoaded(): self.projectSettings = getSettings(self.__getProjectConfigFile()) self.ide.project.sigProjectChanged.connect(self.__onProjectChanged) def deactivate(self): """Called when the plugin is deactivated""" self.ide.project.sigProjectChanged.disconnect(self.__onProjectChanged) self.projectSettings = None self.ideWideSettings = None VersionControlSystemInterface.deactivate(self) def getConfigFunction(self): """SVN plugin requires configuring""" return self.configure def __getIDEConfigFile(self): """Provides a name of the IDE wide config file""" return self.ide.settingsDir + "svn.plugin.conf" def __getProjectConfigFile(self): """Provides a name of the project config file""" return self.ide.projectSettingsDir + "svn.plugin.conf" def configure(self): " Configures the SVN plugin " dlg = SVNPluginConfigDialog(self.ideWideSettings, self.projectSettings) if dlg.exec_() == QDialog.Accepted: # Save the settings if they have changed self.__settingsLock.lock() if self.ideWideSettings != dlg.ideWideSettings: self.ideWideSettings = dlg.ideWideSettings saveSVNSettings(self.ideWideSettings, self.__getIDEConfigFile()) if self.projectSettings is not None: if self.projectSettings != dlg.projectSettings: self.projectSettings = dlg.projectSettings saveSVNSettings(self.projectSettings, self.__getProjectConfigFile()) self.__settingsLock.unlock() def notifyPathChanged(self, path): """Sends notifications to the IDE that a path was changed""" self.PathChanged.emit(path) def __onProjectChanged(self, what): """Triggers when a project has changed""" if what != self.ide.project.CompleteProject: return if self.ide.project.isLoaded(): self.__settingsLock.lock() self.projectSettings = getSettings(self.__getProjectConfigFile()) self.__settingsLock.unlock() else: self.__settingsLock.lock() self.projectSettings = None self.__settingsLock.unlock() def getCustomIndicators(self): """Provides custom indicators if needed""" return IND_DESCRIPTION def getSettings(self): """Thread safe settings copy""" if self.ide.project.isLoaded(): self.__settingsLock.lock() settings = deepcopy(self.projectSettings) self.__settingsLock.unlock() return settings self.__settingsLock.lock() settings = deepcopy(self.ideWideSettings) self.__settingsLock.unlock() return settings def getSVNClient(self, settings): """Creates the SVN client object""" client = pysvn.Client() client.exception_style = 1 # In order to get error codes client.callback_get_login = self._getLoginCallback return client def _getLoginCallback(self, realm, username, may_save): """SVN client calls it when authorization is requested""" settings = self.getSettings() if settings.authKind == AUTH_PASSWD: return (True, settings.userName, settings.password, False) return (False, "", "", False) def convertSVNStatus(self, status): """Converts the status between the SVN and the plugin supported values""" if status.text_status == pysvn.wc_status_kind.added: return IND_ADDED if status.text_status == pysvn.wc_status_kind.deleted: return IND_DELETED if status.text_status == pysvn.wc_status_kind.ignored: return IND_IGNORED if status.text_status == pysvn.wc_status_kind.merged: return IND_MERGED if status.text_status == pysvn.wc_status_kind.modified: if status.repos_text_status == pysvn.wc_status_kind.modified or \ status.repos_prop_status == pysvn.wc_status_kind.modified: return IND_MODIFIED_LR return IND_MODIFIED_L if status.text_status == pysvn.wc_status_kind.normal: if status.repos_text_status == pysvn.wc_status_kind.modified or \ status.repos_prop_status == pysvn.wc_status_kind.modified: return IND_MODIFIED_R if status.prop_status == pysvn.wc_status_kind.modified: return IND_MODIFIED_L return IND_UPTODATE if status.text_status == pysvn.wc_status_kind.replaced: return IND_REPLACED if status.text_status == pysvn.wc_status_kind.conflicted: return IND_CONFLICTED if status.text_status == pysvn.wc_status_kind.external: return IND_EXTERNAL if status.text_status == pysvn.wc_status_kind.incomplete: return IND_INCOMPLETE if status.text_status == pysvn.wc_status_kind.missing: return IND_MISSING if status.text_status == pysvn.wc_status_kind.none: return self.NOT_UNDER_VCS if status.text_status == pysvn.wc_status_kind.obstructed: return IND_OBSTRUCTED if status.text_status == pysvn.wc_status_kind.unversioned: return self.NOT_UNDER_VCS return IND_UNKNOWN def getStatus(self, path, flag): """Provides VCS statuses for the path""" settings = self.getSettings() client = self.getSVNClient(settings) clientUpdate = settings.statusKind != STATUS_LOCAL_ONLY if flag == self.REQUEST_RECURSIVE: clientDepth = pysvn.depth.infinity elif flag == self.REQUEST_ITEM_ONLY: # Heck! By some reasons if depth empty AND update is True # the request returns nothing if path.endswith(os.path.sep): clientDepth = pysvn.depth.empty else: clientDepth = pysvn.depth.unknown else: clientDepth = pysvn.depth.files try: statusList = client.status(path, update=clientUpdate, depth=clientDepth) # Another heck! If a directory is not under VCS and the depth is # not empty then the result set is empty! I have no ideas why. if not statusList: # Try again, may be it is because the depth statusList = client.status(path, update=clientUpdate, depth=pysvn.depth.empty) # And another heck! If a directory is not under VCS even empty # depth may not help. Sometimes an empty list is returned because # update is set to True. Try without update as the last resort. if not statusList and clientUpdate: statusList = client.status(path, update=False, depth=pysvn.depth.empty) result = [] for status in statusList: reportPath = status.path if not status.path.endswith(os.path.sep): if status.entry is None: if os.path.isdir(status.path): reportPath += os.path.sep elif status.entry.kind == pysvn.node_kind.dir: reportPath += os.path.sep result.append((reportPath.replace(path, ""), self.convertSVNStatus(status), None)) return result except pysvn.ClientError as exc: errorCode = exc.args[1][0][1] if errorCode == pysvn.svn_err.wc_not_working_copy: return (("", self.NOT_UNDER_VCS, None),) message = exc.args[0] return (("", IND_ERROR, message),) except Exception as exc: return (("", IND_ERROR, "Error: " + str(exc)),) except: return (("", IND_ERROR, "Unknown error"),) def getLocalStatus(self, path, pDepth=None): """Provides quick local SVN status for the item itself""" client = self.getSVNClient(self.getSettings()) try: statusList = client.status(path, update=False, depth=pDepth) if pDepth != pysvn.depth.empty and len(statusList) == 0: statusList = client.status(path, update=False, depth=pysvn.depth.empty) statusCount = len(statusList) if pDepth == pysvn.depth.empty and statusCount != 1: return IND_ERROR if statusCount == 1: return self.convertSVNStatus(statusList[0]) # It is a list of statuses res = [] for status in statusList: res.append((status.path, self.convertSVNStatus(status))) return res except pysvn.ClientError as exc: errorCode = exc.args[1][0][1] if errorCode == pysvn.svn_err.wc_not_working_copy: return self.NOT_UNDER_VCS # message = exc.args[0] return IND_ERROR except Exception as exc: return IND_ERROR except: return IND_ERROR
class ImportDgmTabWidget(QWidget, MainWindowTabWidgetBase): """Widget for an editors manager""" sigEscapePressed = pyqtSignal() def __init__(self, parent=None): MainWindowTabWidgetBase.__init__(self) QWidget.__init__(self, parent) self.__viewer = DiagramWidget(self) self.__viewer.sigEscapePressed.connect(self.__onEsc) self.__createLayout() def __createLayout(self): """Creates the toolbar and layout""" # Buttons printButton = QAction(getIcon('printer.png'), 'Print', self) # printButton.setShortcut('Ctrl+') printButton.triggered.connect(self.__onPrint) printPreviewButton = QAction(getIcon('printpreview.png'), 'Print preview', self) # printPreviewButton.setShortcut('Ctrl+') printPreviewButton.triggered.connect(self.__onPrintPreview) fixedSpacer = QWidget() fixedSpacer.setFixedHeight(16) zoomInButton = QAction(getIcon('zoomin.png'), 'Zoom in (Ctrl+=)', self) zoomInButton.setShortcut('Ctrl+=') zoomInButton.triggered.connect(self.onZoomIn) zoomOutButton = QAction(getIcon('zoomout.png'), 'Zoom out (Ctrl+-)', self) zoomOutButton.setShortcut('Ctrl+-') zoomOutButton.triggered.connect(self.onZoomOut) zoomResetButton = QAction(getIcon('zoomreset.png'), 'Zoom reset (Ctrl+0)', self) zoomResetButton.setShortcut('Ctrl+0') zoomResetButton.triggered.connect(self.onZoomReset) # Toolbar toolbar = QToolBar(self) toolbar.setOrientation(Qt.Vertical) toolbar.setMovable(False) toolbar.setAllowedAreas(Qt.RightToolBarArea) toolbar.setIconSize(QSize(16, 16)) toolbar.setFixedWidth(28) toolbar.setContentsMargins(0, 0, 0, 0) # toolbar.addAction(printPreviewButton) # toolbar.addAction(printButton) # toolbar.addWidget(fixedSpacer) toolbar.addAction(zoomInButton) toolbar.addAction(zoomOutButton) toolbar.addAction(zoomResetButton) hLayout = QHBoxLayout() hLayout.setContentsMargins(0, 0, 0, 0) hLayout.setSpacing(0) hLayout.addWidget(self.__viewer) hLayout.addWidget(toolbar) self.setLayout(hLayout) def setFocus(self): """Overridden setFocus""" self.__viewer.setFocus() def setScene(self, scene): """Sets the graphics scene to display""" self.__viewer.setScene(scene) def __onPrint(self): """Triggered on the 'print' button""" pass def __onPrintPreview(self): """Triggered on the 'print preview' button""" pass def onZoomIn(self): """Triggered on the 'zoom in' button""" self.__viewer.zoomIn() def onZoomOut(self): """Triggered on the 'zoom out' button""" self.__viewer.zoomOut() def onZoomReset(self): """Triggered on the 'zoom reset' button""" self.__viewer.resetZoom() def __onEsc(self): """Triggered when Esc is pressed""" self.sigEscapePressed.emit() def onCopy(self): """Copies the diagram to the exchange buffer""" self.__viewer.onCopy() def onSaveAs(self, fName): """Saves the diagram into the given file""" self.__viewer.onSaveAs(fName) # Mandatory interface part is below def isModified(self): """Tells if the file is modified""" return False def getRWMode(self): """Tells if the file is read only""" return "RO" def getType(self): """Tells the widget type""" return MainWindowTabWidgetBase.GeneratedDiagram def getLanguage(self): """Tells the content language""" return "Diagram" def setFileName(self, name): """Sets the file name - not applicable""" raise Exception("Setting a file name for a diagram is not applicable") def setEncoding(self, newEncoding): """Sets the new encoding - not applicable for the diagram viewer""" return def getShortName(self): """Tells the display name""" return "Imports diagram" def setShortName(self, name): """Sets the display name - not applicable""" raise Exception("Setting a file name for a diagram is not applicable")
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 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)