예제 #1
0
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)
예제 #2
0
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)
예제 #3
0
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
예제 #4
0
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()
예제 #5
0
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)
예제 #6
0
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)
예제 #7
0
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)
예제 #8
0
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))
예제 #9
0
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()
예제 #10
0
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()
예제 #11
0
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()
예제 #12
0
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")
예제 #13
0
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
예제 #14
0
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)
예제 #15
0
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()
예제 #16
0
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))
예제 #17
0
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)
예제 #18
0
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))
예제 #19
0
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
예제 #20
0
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})
예제 #21
0
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)
예제 #22
0
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
예제 #23
0
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
예제 #24
0
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")
예제 #26
0
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
예제 #27
0
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")
예제 #29
0
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)
예제 #30
0
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)