Exemplo n.º 1
0
class FlowUIWidget(QWidget):

    """The widget which goes along with the text editor"""

    def __init__(self, editor, parent):
        QWidget.__init__(self, parent)

        # It is always not visible at the beginning because there is no
        # editor content at the start
        self.setVisible(False)

        self.__editor = editor
        self.__parentWidget = parent
        self.__connected = False
        self.__needPathUpdate = False

        self.cflowSettings = getCflowSettings(self)
        self.__displayProps = (self.cflowSettings.hidedocstrings,
                               self.cflowSettings.hidecomments,
                               self.cflowSettings.hideexcepts,
                               Settings()['smartZoom'])

        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.__navBar = None
        self.__cf = None
        self.__canvas = None
        self.__validGroups = []
        self.__allGroupId = set()

        # Create the update timer
        self.__updateTimer = QTimer(self)
        self.__updateTimer.setSingleShot(True)
        self.__updateTimer.timeout.connect(self.process)

        vLayout.addWidget(self.__createNavigationBar())
        vLayout.addWidget(self.__createStackedViews())

        hLayout.addLayout(vLayout)
        hLayout.addWidget(self.__createToolbar())
        self.setLayout(hLayout)

        self.updateSettings()

        # Connect to the change file type signal
        self.__mainWindow = GlobalData().mainWindow
        editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager
        editorsManager.sigFileTypeChanged.connect(self.__onFileTypeChanged)

        Settings().sigHideDocstringsChanged.connect(
            self.__onHideDocstringsChanged)
        Settings().sigHideCommentsChanged.connect(self.__onHideCommentsChanged)
        Settings().sigHideExceptsChanged.connect(self.__onHideExceptsChanged)
        Settings().sigSmartZoomChanged.connect(self.__onSmartZoomChanged)

        self.setSmartZoomLevel(Settings()['smartZoom'])

    def getParentWidget(self):
        return self.__parentWidget

    def view(self):
        """Provides a reference to the current view"""
        return self.smartViews.currentWidget()

    def scene(self):
        """Provides a reference to the current scene"""
        return self.view().scene

    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)

        # Buttons
        saveAsMenu = QMenu(self)
        saveAsSVGAct = saveAsMenu.addAction(getIcon('filesvg.png'),
                                            'Save as SVG...')
        saveAsSVGAct.triggered.connect(self.onSaveAsSVG)

        saveAsPDFAct = saveAsMenu.addAction(getIcon('filepdf.png'),
                                            'Save as PDF...')
        saveAsPDFAct.triggered.connect(self.onSaveAsPDF)
        saveAsPNGAct = saveAsMenu.addAction(getIcon('filepixmap.png'),
                                            'Save as PNG...')
        saveAsPNGAct.triggered.connect(self.onSaveAsPNG)
        saveAsMenu.addSeparator()
        saveAsCopyToClipboardAct = saveAsMenu.addAction(
            getIcon('copymenu.png'), 'Copy to clipboard')
        saveAsCopyToClipboardAct.triggered.connect(self.copyToClipboard)

        self.__saveAsButton = QToolButton(self)
        self.__saveAsButton.setIcon(getIcon('saveasmenu.png'))
        self.__saveAsButton.setToolTip('Save as')
        self.__saveAsButton.setPopupMode(QToolButton.InstantPopup)
        self.__saveAsButton.setMenu(saveAsMenu)
        self.__saveAsButton.setFocusPolicy(Qt.NoFocus)

        self.__levelUpButton = QToolButton(self)
        self.__levelUpButton.setFocusPolicy(Qt.NoFocus)
        self.__levelUpButton.setIcon(getIcon('levelup.png'))
        self.__levelUpButton.setToolTip('Smart zoom level up (Shift+wheel)')
        self.__levelUpButton.clicked.connect(self.onSmartZoomLevelUp)
        self.__levelIndicator = QLabel('<b>0</b>', self)
        self.__levelIndicator.setAlignment(Qt.AlignCenter)
        self.__levelDownButton = QToolButton(self)
        self.__levelDownButton.setFocusPolicy(Qt.NoFocus)
        self.__levelDownButton.setIcon(getIcon('leveldown.png'))
        self.__levelDownButton.setToolTip('Smart zoom level down (Shift+wheel)')
        self.__levelDownButton.clicked.connect(self.onSmartZoomLevelDown)

        fixedSpacer = QWidget()
        fixedSpacer.setFixedHeight(10)

        self.__hideDocstrings = QToolButton(self)
        self.__hideDocstrings.setCheckable(True)
        self.__hideDocstrings.setIcon(getIcon('hidedocstrings.png'))
        self.__hideDocstrings.setToolTip('Show/hide docstrings')
        self.__hideDocstrings.setFocusPolicy(Qt.NoFocus)
        self.__hideDocstrings.setChecked(Settings()['hidedocstrings'])
        self.__hideDocstrings.clicked.connect(self.__onHideDocstrings)
        self.__hideComments = QToolButton(self)
        self.__hideComments.setCheckable(True)
        self.__hideComments.setIcon(getIcon('hidecomments.png'))
        self.__hideComments.setToolTip('Show/hide comments')
        self.__hideComments.setFocusPolicy(Qt.NoFocus)
        self.__hideComments.setChecked(Settings()['hidecomments'])
        self.__hideComments.clicked.connect(self.__onHideComments)
        self.__hideExcepts = QToolButton(self)
        self.__hideExcepts.setCheckable(True)
        self.__hideExcepts.setIcon(getIcon('hideexcepts.png'))
        self.__hideExcepts.setToolTip('Show/hide except blocks')
        self.__hideExcepts.setFocusPolicy(Qt.NoFocus)
        self.__hideExcepts.setChecked(Settings()['hideexcepts'])
        self.__hideExcepts.clicked.connect(self.__onHideExcepts)

        spacer = QWidget()
        spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)

        self.__toolbar.addWidget(self.__saveAsButton)
        self.__toolbar.addWidget(spacer)
        self.__toolbar.addWidget(self.__levelUpButton)
        self.__toolbar.addWidget(self.__levelIndicator)
        self.__toolbar.addWidget(self.__levelDownButton)
        self.__toolbar.addWidget(fixedSpacer)
        self.__toolbar.addWidget(self.__hideDocstrings)
        self.__toolbar.addWidget(self.__hideComments)
        self.__toolbar.addWidget(self.__hideExcepts)
        return self.__toolbar

    def __createNavigationBar(self):
        """Creates the navigation bar"""
        self.__navBar = ControlFlowNavigationBar(self)
        return self.__navBar

    def __createStackedViews(self):
        """Creates the graphics view"""
        self.smartViews = QStackedWidget(self)
        self.smartViews.setContentsMargins(0, 0, 0, 0)

        self.smartViews.addWidget(CFGraphicsView(self.__navBar, self))
        self.smartViews.addWidget(CFGraphicsView(self.__navBar, self))
        return self.smartViews

    def process(self):
        """Parses the content and displays the results"""
        if not self.__connected:
            self.__connectEditorSignals()

        start = timer()
        cf = getControlFlowFromMemory(self.__editor.text)
        end = timer()
        if cf.errors:
            self.__navBar.updateInfoIcon(self.__navBar.STATE_BROKEN_UTD)
            errors = []
            for err in cf.errors:
                if err[0] == -1 and err[1] == -1:
                    errors.append(err[2])
                elif err[1] == -1:
                    errors.append('[' + str(err[0]) + ':] ' + err[2])
                elif err[0] == -1:
                    errors.append('[:' + str(err[1]) + '] ' + err[2])
                else:
                    errors.append('[' + str(err[0]) + ':' +
                                  str(err[1]) + '] ' + err[2])
            self.__navBar.setErrors(errors)
            return

        self.__cf = cf
        if self.isDebugMode():
            logging.info('Parsed file: %s', formatFlow(str(self.__cf)))
            logging.info('Parse timing: %f', end - start)

        # Collect warnings (parser + CML warnings) and valid groups
        self.__validGroups = []
        self.__allGroupId = set()
        allWarnings = self.__cf.warnings + \
                      CMLVersion.validateCMLComments(self.__cf,
                                                     self.__validGroups,
                                                     self.__allGroupId)

        # That will clear the error tooltip as well
        self.__navBar.updateInfoIcon(self.__navBar.STATE_OK_UTD)

        if allWarnings:
            warnings = []
            for warn in allWarnings:
                if warn[0] == -1 and warn[1] == -1:
                    warnings.append(warn[2])
                elif warn[1] == -1:
                    warnings.append('[' + str(warn[0]) + ':] ' + warn[2])
                elif warn[0] == -1:
                    warnings.append('[:' + str(warn[1]) + '] ' + warn[2])
                else:
                    warnings.append('[' + str(warn[0]) + ':' +
                                    str(warn[1]) + '] ' + warn[2])
            self.__navBar.setWarnings(warnings)
        else:
            self.__navBar.clearWarnings()

        self.redrawScene()

    def __cleanupCanvas(self):
        """Cleans up the canvas"""
        if self.__canvas is not None:
            self.__canvas.cleanup()
            self.__canvas = None
        for item in self.scene().items():
            item.cleanup()
        self.scene().clear()

    def redrawScene(self):
        """Redraws the scene"""
        smartZoomLevel = Settings()['smartZoom']
        self.cflowSettings = getCflowSettings(self)
        if self.dirty():
            self.__displayProps = (self.cflowSettings.hidedocstrings,
                                   self.cflowSettings.hidecomments,
                                   self.cflowSettings.hideexcepts,
                                   smartZoomLevel)
        self.cflowSettings.itemID = 0
        self.cflowSettings = tweakSmartSettings(self.cflowSettings,
                                                smartZoomLevel)

        try:
            fileName = self.__parentWidget.getFileName()
            if not fileName:
                fileName = self.__parentWidget.getShortName()
            collapsedGroups = getCollapsedGroups(fileName)

            # Top level canvas has no adress and no parent canvas
            self.__cleanupCanvas()
            self.__canvas = VirtualCanvas(self.cflowSettings, None, None,
                                          self.__validGroups, collapsedGroups,
                                          None)
            lStart = timer()
            self.__canvas.layoutModule(self.__cf)
            lEnd = timer()
            self.__canvas.setEditor(self.__editor)
            width, height = self.__canvas.render()
            rEnd = timer()
            self.scene().setSceneRect(0, 0, width, height)
            self.__canvas.draw(self.scene(), 0, 0)
            dEnd = timer()
            if self.isDebugMode():
                logging.info('Redrawing is done. Size: %d x %d', width, height)
                logging.info('Layout timing: %f', lEnd - lStart)
                logging.info('Render timing: %f', rEnd - lEnd)
                logging.info('Draw timing: %f', dEnd - rEnd)
        except Exception as exc:
            logging.error(str(exc))
            raise

    def onFlowZoomChanged(self):
        """Triggered when a flow zoom is changed"""
        if self.__cf:
            selection = self.scene().serializeSelection()
            firstOnScreen = self.scene().getFirstLogicalItem()
            self.cflowSettings.onFlowZoomChanged()
            self.redrawScene()
            self.updateNavigationToolbar('')
            self.scene().restoreSelectionByID(selection)
            self.__restoreScroll(firstOnScreen)

    def __onFileTypeChanged(self, fileName, uuid, newFileType):
        """Triggered when a buffer content type has changed"""
        if self.__parentWidget.getUUID() != uuid:
            return

        if not isPythonMime(newFileType):
            self.__disconnectEditorSignals()
            self.__updateTimer.stop()
            self.__cleanupCanvas()
            self.__cf = None
            self.__validGroups = []
            self.setVisible(False)
            self.__navBar.updateInfoIcon(self.__navBar.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
        _, _, _, cflowHPos, cflowVPos = getFilePosition(fileName)
        self.setScrollbarPositions(cflowHPos, cflowVPos)

    def terminate(self):
        """Called when a tab is closed"""
        if self.__updateTimer.isActive():
            self.__updateTimer.stop()
        self.__updateTimer.deleteLater()

        self.__disconnectEditorSignals()

        self.__mainWindow = GlobalData().mainWindow
        editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager
        editorsManager.sigFileTypeChanged.disconnect(self.__onFileTypeChanged)

        Settings().sigHideDocstringsChanged.disconnect(
            self.__onHideDocstringsChanged)
        Settings().sigHideCommentsChanged.disconnect(self.__onHideCommentsChanged)
        Settings().sigHideExceptsChanged.disconnect(self.__onHideExceptsChanged)
        Settings().sigSmartZoomChanged.disconnect(self.__onSmartZoomChanged)

        # Helps GC to collect more
        self.__cleanupCanvas()
        for index in range(self.smartViews.count()):
            self.smartViews.widget(index).terminate()
            self.smartViews.widget(index).deleteLater()

        self.smartViews.deleteLater()
        self.__navBar.deleteLater()
        self.__cf = None

        self.__saveAsButton.menu().deleteLater()
        self.__saveAsButton.deleteLater()

        self.__levelUpButton.clicked.disconnect(self.onSmartZoomLevelUp)
        self.__levelUpButton.deleteLater()

        self.__levelDownButton.clicked.disconnect(self.onSmartZoomLevelDown)
        self.__levelDownButton.deleteLater()

        self.__hideDocstrings.clicked.disconnect(self.__onHideDocstrings)
        self.__hideDocstrings.deleteLater()

        self.__hideComments.clicked.disconnect(self.__onHideComments)
        self.__hideComments.deleteLater()

        self.__hideExcepts.clicked.disconnect(self.__onHideExcepts)
        self.__hideExcepts.deleteLater()

        self.__toolbar.deleteLater()

        self.__editor = None
        self.__parentWidget = None
        self.cflowSettings = None
        self.__displayProps = 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.__navBar.getCurrentState() in [self.__navBar.STATE_OK_UTD,
                                               self.__navBar.STATE_OK_CHN,
                                               self.__navBar.STATE_UNKNOWN]:
            self.__navBar.updateInfoIcon(self.__navBar.STATE_OK_CHN)
        else:
            self.__navBar.updateInfoIcon(self.__navBar.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 generateNewGroupId(self):
        """Generates a new group ID (string)"""
        # It can also consider the current set of the groups: valid + invalid
        # and generate an integer id which is shorter
        for vacantGroupId in range(1000):
            groupId = str(vacantGroupId)
            if not groupId in self.__allGroupId:
                return groupId
        # Last resort
        return str(uuid.uuid1())

    def updateNavigationToolbar(self, text):
        """Updates the toolbar text"""
        if self.__needPathUpdate:
            self.__navBar.setPath(text)

    def updateSettings(self):
        """Updates settings"""
        self.__needPathUpdate = Settings()['showCFNavigationBar']
        self.__navBar.setPathVisible(self.__needPathUpdate)
        self.__navBar.setPath('')

    def highlightAtAbsPos(self, absPos, line, pos):
        """Scrolls the view to the item closest to absPos and selects it.

        line and pos are 1-based
        """
        item, _ = self.scene().getNearestItem(absPos, line, pos)
        if item:
            GlobalData().mainWindow.setFocusToFloatingRenderer()
            self.scene().clearSelection()
            item.setSelected(True)
            self.view().scrollTo(item)
            self.setFocus()

    def setFocus(self):
        """Sets the focus"""
        self.view().setFocus()

    @staticmethod
    def __getDefaultSaveDir():
        """Provides the default directory to save files to"""
        project = GlobalData().project
        if project.isLoaded():
            return project.getProjectDir()
        return QDir.currentPath()

    def __selectFile(self, extension):
        """Picks a file of a certain extension"""
        dialog = QFileDialog(self, 'Save flowchart as')
        dialog.setFileMode(QFileDialog.AnyFile)
        dialog.setLabelText(QFileDialog.Accept, "Save")
        dialog.setNameFilter(extension.upper() + " files (*." +
                             extension.lower() + ")")
        urls = []
        for dname in QDir.drives():
            urls.append(QUrl.fromLocalFile(dname.absoluteFilePath()))
        urls.append(QUrl.fromLocalFile(QDir.homePath()))
        project = GlobalData().project
        if project.isLoaded():
            urls.append(QUrl.fromLocalFile(project.getProjectDir()))
        dialog.setSidebarUrls(urls)

        suggestedFName = self.__parentWidget.getFileName()
        if '.' in suggestedFName:
            dotIndex = suggestedFName.rindex('.')
            suggestedFName = suggestedFName[:dotIndex]

        dialog.setDirectory(self.__getDefaultSaveDir())
        dialog.selectFile(suggestedFName + "." + extension.lower())
        dialog.setOption(QFileDialog.DontConfirmOverwrite, False)
        dialog.setOption(QFileDialog.DontUseNativeDialog, True)
        if dialog.exec_() != QDialog.Accepted:
            return None

        fileNames = dialog.selectedFiles()
        fileName = os.path.abspath(str(fileNames[0]))
        if os.path.isdir(fileName):
            logging.error("A file must be selected")
            return None

        if "." not in fileName:
            fileName += "." + extension.lower()

        # Check permissions to write into the file or to a directory
        if os.path.exists(fileName):
            # Check write permissions for the file
            if not os.access(fileName, os.W_OK):
                logging.error("There is no write permissions for " + fileName)
                return None
        else:
            # Check write permissions to the directory
            dirName = os.path.dirname(fileName)
            if not os.access(dirName, os.W_OK):
                logging.error("There is no write permissions for the "
                              "directory " + dirName)
                return None

        if os.path.exists(fileName):
            res = QMessageBox.warning(
                self, "Save flowchart as",
                "<p>The file <b>" + fileName + "</b> already exists.</p>",
                QMessageBox.StandardButtons(QMessageBox.Abort |
                                            QMessageBox.Save),
                QMessageBox.Abort)
            if res == QMessageBox.Abort or res == QMessageBox.Cancel:
                return None

        # All prerequisites are checked, return a file name
        return fileName

    def onSaveAsSVG(self):
        """Triggered on the 'Save as SVG' button"""
        fileName = self.__selectFile("svg")
        if fileName is None:
            return False

        try:
            self.__saveAsSVG(fileName)
        except Exception as excpt:
            logging.error(str(excpt))
            return False
        return True

    def __saveAsSVG(self, fileName):
        """Saves the flowchart as an SVG file"""
        generator = QSvgGenerator()
        generator.setFileName(fileName)
        generator.setSize(QSize(self.scene().width(), self.scene().height()))
        painter = QPainter(generator)
        self.scene().render(painter)
        painter.end()

    def onSaveAsPDF(self):
        """Triggered on the 'Save as PDF' button"""
        fileName = self.__selectFile("pdf")
        if fileName is None:
            return False

        try:
            self.__saveAsPDF(fileName)
        except Exception as excpt:
            logging.error(str(excpt))
            return False
        return True

    def __saveAsPDF(self, fileName):
        """Saves the flowchart as an PDF file"""
        printer = QPrinter()
        printer.setOutputFormat(QPrinter.PdfFormat)
        printer.setPaperSize(QSizeF(self.scene().width(),
                                    self.scene().height()), QPrinter.Point)
        printer.setFullPage(True)
        printer.setOutputFileName(fileName)

        painter = QPainter(printer)
        self.scene().render(painter)
        painter.end()

    def onSaveAsPNG(self):
        """Triggered on the 'Save as PNG' button"""
        fileName = self.__selectFile("png")
        if fileName is None:
            return False

        try:
            self.__saveAsPNG(fileName)
        except Exception as excpt:
            logging.error(str(excpt))
            return False
        return True

    def __getPNG(self):
        """Renders the scene as PNG"""
        image = QImage(self.scene().width(), self.scene().height(),
                       QImage.Format_ARGB32_Premultiplied)
        painter = QPainter(image)
        # It seems that the better results are without antialiasing
        # painter.setRenderHint( QPainter.Antialiasing )
        self.scene().render(painter)
        painter.end()
        return image

    def __saveAsPNG(self, fileName):
        """Saves the flowchart as an PNG file"""
        image = self.__getPNG()
        image.save(fileName, "PNG")

    def copyToClipboard(self):
        """Copies the rendered scene to the clipboard as an image"""
        image = self.__getPNG()
        clip = QApplication.clipboard()
        clip.setImage(image)

    def getScrollbarPositions(self):
        """Provides the scrollbar positions"""
        hScrollBar = self.view().horizontalScrollBar()
        vScrollBar = self.view().verticalScrollBar()
        return hScrollBar.value(), vScrollBar.value()

    def setScrollbarPositions(self, hPos, vPos):
        """Sets the scrollbar positions for the view"""
        self.view().horizontalScrollBar().setValue(hPos)
        self.view().verticalScrollBar().setValue(vPos)

    def __onHideDocstrings(self):
        """Triggered when a hide docstring button is pressed"""
        Settings()['hidedocstrings'] = not Settings()['hidedocstrings']

    def __onHideDocstringsChanged(self):
        """Signalled by settings"""
        selection = self.scene().serializeSelection()
        firstOnScreen = self.scene().getFirstLogicalItem()
        settings = Settings()
        self.__hideDocstrings.setChecked(settings['hidedocstrings'])
        if self.__checkNeedRedraw():
            self.scene().restoreSelectionByID(selection)
            self.__restoreScroll(firstOnScreen)

    def __onHideComments(self):
        """Triggered when a hide comments button is pressed"""
        Settings()['hidecomments'] = not Settings()['hidecomments']

    def __onHideCommentsChanged(self):
        """Signalled by settings"""
        selection = self.scene().serializeSelection()
        firstOnScreen = self.scene().getFirstLogicalItem()
        settings = Settings()
        self.__hideComments.setChecked(settings['hidecomments'])
        if self.__checkNeedRedraw():
            self.scene().restoreSelectionByID(selection)
            self.__restoreScroll(firstOnScreen)

    def __onHideExcepts(self):
        """Triggered when a hide except blocks button is pressed"""
        Settings()['hideexcepts'] = not Settings()['hideexcepts']

    def __onHideExceptsChanged(self):
        """Signalled by settings"""
        selection = self.scene().serializeSelection()
        firstOnScreen = self.scene().getFirstLogicalItem()
        settings = Settings()
        self.__hideExcepts.setChecked(settings['hideexcepts'])
        if self.__checkNeedRedraw():
            self.scene().restoreSelectionByTooltip(selection)
            self.__restoreScroll(firstOnScreen)

    def __checkNeedRedraw(self):
        """Redraws the scene if necessary when a display setting is changed"""
        editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager
        if self.__parentWidget == editorsManager.currentWidget():
            self.updateNavigationToolbar('')
            self.process()
            return True
        return False

    def dirty(self):
        """True if some other tab has switched display settings"""
        settings = Settings()
        return self.__displayProps[0] != settings['hidedocstrings'] or \
            self.__displayProps[1] != settings['hidecomments'] or \
            self.__displayProps[2] != settings['hideexcepts'] or \
            self.__displayProps[3] != settings['smartZoom']

    def onSmartZoomLevelUp(self):
        """Triggered when an upper smart zoom level was requested"""
        Settings().onSmartZoomIn()

    def onSmartZoomLevelDown(self):
        """Triggered when an lower smart zoom level was requested"""
        Settings().onSmartZoomOut()

    def setSmartZoomLevel(self, smartZoomLevel):
        """Sets the new smart zoom level"""
        maxSmartZoom = Settings().MAX_SMART_ZOOM
        if smartZoomLevel < 0 or smartZoomLevel > maxSmartZoom:
            return

        self.__levelIndicator.setText('<b>' + str(smartZoomLevel) + '</b>')
        self.__levelIndicator.setToolTip(
            getSmartZoomDescription(smartZoomLevel))
        self.__levelUpButton.setEnabled(smartZoomLevel < maxSmartZoom)
        self.__levelDownButton.setEnabled(smartZoomLevel > 0)
        self.smartViews.setCurrentIndex(smartZoomLevel)

    def __onSmartZoomChanged(self):
        """Triggered when a smart zoom changed"""
        selection = self.scene().serializeSelection()
        firstOnScreen = self.scene().getFirstLogicalItem()
        self.setSmartZoomLevel(Settings()['smartZoom'])
        if self.__checkNeedRedraw():
            self.scene().restoreSelectionByTooltip(selection)
            self.__restoreScroll(firstOnScreen)

    def __restoreScroll(self, toItem):
        """Restores the view scrolling to the best possible position"""
        if toItem:
            lineRange = toItem.getLineRange()
            absPosRange = toItem.getAbsPosRange()
            item, _ = self.scene().getNearestItem(absPosRange[0],
                                                  lineRange[0], 0)
            if item:
                self.view().scrollTo(item, True)
                self.view().horizontalScrollBar().setValue(0)

    def validateCollapsedGroups(self, fileName):
        """Checks that there are no collapsed groups which are invalid"""
        if self.__navBar.getCurrentState() != self.__navBar.STATE_OK_UTD:
            return

        collapsedGroups = getCollapsedGroups(fileName)
        if collapsedGroups:
            toBeDeleted = []
            for groupId in collapsedGroups:
                for validId, start, end in self.__validGroups:
                    del start
                    del end
                    if validId == groupId:
                        break
                else:
                    toBeDeleted.append(groupId)

            if toBeDeleted:
                for groupId in toBeDeleted:
                    collapsedGroups.remove(groupId)
                setCollapsedGroups(fileName, collapsedGroups)
        else:
            setCollapsedGroups(fileName, [])

    def getDocItemByAnchor(self, anchor):
        """Provides the graphics item for the given anchor if so"""
        return self.scene().getDocItemByAnchor(anchor)

    @staticmethod
    def isDebugMode():
        """True if it is a debug mode"""
        return GlobalData().skin['debug']
Exemplo n.º 2
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()
Exemplo n.º 3
0
class MDWidget(QWidget):
    """The MD rendered content widget which goes along with the text editor"""
    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.editorsManagerWidget.editorsManager
        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

        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)
        return self.mdView

    def process(self):
        """Parses the content and displays the results"""
        if not self.__connected:
            self.__connectEditorSignals()

        renderedText, errors, warnings = renderMarkdown(self.__editor.text)
        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 morkdown 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.__parentWidget.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 __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"""
        hScrollBar = self.mdView.horizontalScrollBar()
        hsbValue = hScrollBar.value() if hScrollBar else 0

        vScrollBar = self.mdView.verticalScrollBar()
        vsbValue = vScrollBar.value() if vScrollBar else 0
        return hsbValue, vsbValue

    def setScrollbarPositions(self, hPos, vPos):
        """Sets the scrollbar positions for the view"""
        hsb = self.mdView.horizontalScrollBar()
        if hsb:
            hsb.setValue(hPos)
        vsb = self.mdView.verticalScrollBar()
        if vsb:
            vsb.setValue(vPos)

    def getFileName(self):
        return self.__parentWidget.getFileName()
Exemplo n.º 4
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)