Пример #1
0
 def showPathLabelContextMenu(self, pos):
     """Triggered when a context menu is requested for the path label"""
     contextMenu = QMenu(self)
     contextMenu.addAction(getIcon('copymenu.png'),
                           'Copy full path to clipboard (double click)',
                           self.onPathLabelDoubleClick)
     contextMenu.addSeparator()
     contextMenu.addAction(getIcon(''), 'Copy directory path to clipboard',
                           self.onCopyDirToClipboard)
     contextMenu.addAction(getIcon(''), 'Copy file name to clipboard',
                           self.onCopyFileNameToClipboard)
     contextMenu.popup(self.__fileLabel.mapToGlobal(pos))
Пример #2
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)
Пример #3
0
class VariablesViewer(QWidget):
    """Implements the variables viewer for a debugger"""

    # First group of filters
    FilterGlobalAndLocal = 0
    FilterGlobalOnly = 1
    FilterLocalOnly = 2

    def __init__(self, debugger, parent=None):
        QWidget.__init__(self, parent)

        self.__debugger = debugger
        self.__browser = VariablesBrowser(debugger, self)
        self.__createLayout()

        self.setTabOrder(self.__browser, self.__execStatement)
        self.setTabOrder(self.__execStatement, self.__execButton)

        self.__updateFilter()

    def __createLayout(self):
        """Creates the widget layout"""
        verticalLayout = QVBoxLayout(self)
        verticalLayout.setContentsMargins(0, 0, 0, 0)
        verticalLayout.setSpacing(0)

        self.__headerLabel = HeaderFitLabel(self)
        self.__headerLabel.setText('Variables')
        self.__headerLabel.setSizePolicy(QSizePolicy.Expanding,
                                         QSizePolicy.Fixed)
        self.__headerLabel.setMinimumWidth(10)

        self.__filterMenu = QMenu(self)
        self.__showAllAct = self.__filterMenu.addAction('Show all variables')
        self.__showAllAct.setData('showall')
        self.__filterMenu.addSeparator()
        self.__filters = []
        for title, settingName, _ in VARIABLE_FILTERS:
            action = self.__filterMenu.addAction(title)
            action.setCheckable(True)
            action.setData(settingName)
            self.__filters.append(action)
        self.__filterMenu.aboutToShow.connect(self.__filterMenuAboutToShow)
        self.__filterMenu.triggered.connect(self.__filterMenuTriggered)

        self.__filterButton = QToolButton(self)
        self.__filterButton.setIcon(getIcon('dbgvarflt.png'))
        self.__filterButton.setToolTip('Variable filter')
        self.__filterButton.setPopupMode(QToolButton.InstantPopup)
        self.__filterButton.setMenu(self.__filterMenu)
        self.__filterButton.setFocusPolicy(Qt.NoFocus)
        self.__filterButton.setFixedSize(self.__headerLabel.height(),
                                         self.__headerLabel.height())

        self.__execStatement = CDMComboBox(True)
        self.__execStatement.setSizePolicy(QSizePolicy.Expanding,
                                           QSizePolicy.Expanding)
        self.__execStatement.lineEdit().setToolTip("Execute statement")
        self.__execStatement.setFixedHeight(26)
        self.__execStatement.editTextChanged.connect(
            self.__execStatementChanged)
        self.__execStatement.enterClicked.connect(self.__onEnterInExec)
        self.__execButton = QPushButton("Exec")
        self.__execButton.setEnabled(False)
        self.__execButton.setFixedHeight(26)
        self.__execButton.clicked.connect(self.__onExec)

        self.headerToolbar = QToolBar(self)
        self.headerToolbar.setIconSize(QSize(18, 18))
        self.headerToolbar.setContentsMargins(1, 1, 1, 1)
        self.headerToolbar.addWidget(self.__headerLabel)
        self.headerToolbar.addWidget(self.__filterButton)

        execLayout = QGridLayout()
        execLayout.setContentsMargins(1, 1, 1, 1)
        execLayout.setSpacing(1)
        execLayout.addWidget(self.__execStatement, 0, 0)
        execLayout.addWidget(self.__execButton, 0, 1)

        verticalLayout.addWidget(self.headerToolbar)
        verticalLayout.addWidget(self.__browser)
        verticalLayout.addLayout(execLayout)

    def __filterMenuAboutToShow(self):
        """Debug variable filter menu is about to show"""
        for flt in self.__filters:
            flt.setChecked(Settings()[flt.data()])

    def __filterMenuTriggered(self, act):
        """A filter has been changed"""
        name = act.data()
        if name == 'showall':
            for _, settingName, _ in VARIABLE_FILTERS:
                Settings()[settingName] = True
        else:
            Settings()[name] = not Settings()[name]
        self.__updateFilter()

    def updateVariables(self, areGlobals, frameNumber, variables):
        """Triggered when a new set of variables is received"""
        self.__browser.showVariables(areGlobals, variables, frameNumber)
        self.__updateHeaderLabel()

    def updateVariable(self, areGlobals, variables):
        """Triggered when a new variable has been received"""
        self.__browser.showVariable(areGlobals, variables)
        self.__updateHeaderLabel()

    def __updateHeaderLabel(self):
        """Updates the header text"""
        shown, total = self.__browser.getShownAndTotalCounts()
        if shown == 0 and total == 0:
            self.__headerLabel.setText("Variables")
        else:
            self.__headerLabel.setText("Variables (" + str(shown) + " of " +
                                       str(total) + ")")

    def __updateFilter(self):
        """Updates the current filter"""
        self.__browser.filterChanged()
        self.__updateHeaderLabel()

    def clear(self):
        """Clears the content"""
        self.__browser.clear()
        self.__updateHeaderLabel()

    def clearAll(self):
        """Clears everything including the history"""
        self.clear()
        self.__execStatement.lineEdit().setText("")
        self.__execStatement.clear()

    def __execStatementChanged(self, text):
        """Triggered when a exec statement is changed"""
        text = str(text).strip()
        self.__execButton.setEnabled(text != "")

    def __onEnterInExec(self):
        """Enter/return clicked in exec"""
        self.__onExec()

    def __onExec(self):
        """Triggered when the Exec button is clicked"""
        text = self.__execStatement.currentText().strip()
        if text != "":
            currentFrame = GlobalData().mainWindow.getCurrentFrameNumber()
            self.__debugger.remoteExecuteStatement(text, currentFrame)
            self.__debugger.remoteClientVariables(1, currentFrame)  # globals
            self.__debugger.remoteClientVariables(0, currentFrame)  # locals

    def switchControl(self, isInIDE):
        """Switches the UI depending where the control flow is"""
        self.__browser.setEnabled(isInIDE)
        self.__filterButton.setEnabled(isInIDE)

        self.__execStatement.setEnabled(isInIDE)
        if isInIDE:
            text = self.__execStatement.currentText().strip()
            self.__execButton.setEnabled(text != "")
        else:
            self.__execButton.setEnabled(False)
Пример #4
0
    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
Пример #5
0
class EditorContextMenuMixin:
    """Encapsulates the context menu handling"""
    def __init__(self):
        self.encodingReloadMenu = QMenu("Set &encoding and reload")
        self.encodingReloadActGrp = QActionGroup(self)
        self.encodingWriteMenu = QMenu("Set encodin&g")
        self.encodingWriteActGrp = QActionGroup(self)

        mainWindow = GlobalData().mainWindow
        editorsManager = mainWindow.editorsManagerWidget.editorsManager

        self._menu = QMenu(self)
        self.__menuUndo = self._menu.addAction(getIcon('undo.png'), '&Undo',
                                               self.onUndo, "Ctrl+Z")
        self.__menuRedo = self._menu.addAction(getIcon('redo.png'), '&Redo',
                                               self.onRedo, "Ctrl+Y")
        self._menu.addSeparator()

        self.__menuCut = self._menu.addAction(getIcon('cutmenu.png'), 'Cu&t',
                                              self.onShiftDel, "Ctrl+X")
        self.__menuCopy = self._menu.addAction(getIcon('copymenu.png'),
                                               '&Copy', self.onCtrlC, "Ctrl+C")
        self.__menuPaste = self._menu.addAction(getIcon('pastemenu.png'),
                                                '&Paste', self.paste, "Ctrl+V")
        self.__menuSelectAll = self._menu.addAction(
            getIcon('selectallmenu.png'), 'Select &all', self.selectAll,
            "Ctrl+A")
        self._menu.addSeparator()

        self.__initReloadEncodingMenu()
        self.encodingReloadMenu.setIcon(getIcon('textencoding.png'))
        self._menu.addMenu(self.encodingReloadMenu)
        self.__initWriteEncodingMenu()
        self.encodingWriteMenu.setIcon(getIcon('textencoding.png'))
        menu = self._menu.addMenu(self.encodingWriteMenu)
        self.__menuClearEncoding = self._menu.addAction(
            getIcon('clearmenu.png'), 'Clear explicit encoding',
            self.__onClearEncoding)
        self._menu.addSeparator()

        menu = self._menu.addMenu(self.__initToolsMenu())
        menu.setIcon(getIcon('toolsmenu.png'))
        self._menu.addSeparator()

        menu = self._menu.addMenu(self.__initDiagramsMenu())
        menu.setIcon(getIcon('diagramsmenu.png'))
        self._menu.addSeparator()

        self.__menuOpenAsFile = self._menu.addAction(getIcon('filemenu.png'),
                                                     'O&pen as file',
                                                     self.openAsFile)
        self.__menuDownloadAndShow = self._menu.addAction(
            getIcon('filemenu.png'), 'Do&wnload and show',
            self.downloadAndShow)
        self.__menuOpenInBrowser = self._menu.addAction(
            getIcon('homepagemenu.png'), 'Open in browser', self.openInBrowser)
        self._menu.addSeparator()

        self.__menuHighlightInPrj = self._menu.addAction(
            getIcon("highlightmenu.png"), "&Highlight in project browser",
            editorsManager.onHighlightInPrj)
        self.__menuHighlightInFS = self._menu.addAction(
            getIcon("highlightmenu.png"), "H&ighlight in file system browser",
            editorsManager.onHighlightInFS)
        self._menuHighlightInOutline = self._menu.addAction(
            getIcon("highlightmenu.png"), "Highlight in &outline browser",
            self.highlightInOutline, 'Ctrl+B')

        # Plugins support
        self.__pluginMenuSeparator = self._menu.addSeparator()
        mainWindow = GlobalData().mainWindow
        editorsManager = mainWindow.editorsManagerWidget.editorsManager
        registeredMenus = editorsManager.getPluginMenus()
        if registeredMenus:
            for path in registeredMenus:
                self._menu.addMenu(registeredMenus[path])
        else:
            self.__pluginMenuSeparator.setVisible(False)

        editorsManager.sigPluginContextMenuAdded.connect(
            self.__onPluginMenuAdded)
        editorsManager.sigPluginContextMenuRemoved.connect(
            self.__onPluginMenuRemoved)

    def __initReloadEncodingMenu(self):
        """Creates the encoding menu for reloading the existing file"""
        for encoding in sorted(SUPPORTED_CODECS):
            act = self.encodingReloadMenu.addAction(encoding)
            act.setCheckable(True)
            act.setData(encoding)
            self.encodingReloadActGrp.addAction(act)
        self.encodingReloadMenu.triggered.connect(self.__onReloadWithEncoding)

    def __initWriteEncodingMenu(self):
        """Creates the encoding menu for further read/write operations"""
        for encoding in sorted(SUPPORTED_CODECS):
            act = self.encodingWriteMenu.addAction(encoding)
            act.setCheckable(True)
            act.setData(encoding)
            self.encodingWriteActGrp.addAction(act)
        self.encodingWriteMenu.triggered.connect(self.__onReadWriteEncoding)

    def __initToolsMenu(self):
        """Creates the tools menu"""
        self.toolsMenu = QMenu('Python too&ls')
        self.runAct = self.toolsMenu.addAction(getIcon('run.png'),
                                               'Run script',
                                               self._parent.onRunScript)
        self.runParamAct = self.toolsMenu.addAction(
            getIcon('paramsmenu.png'), 'Set parameters and run',
            self._parent.onRunScriptDlg)
        self.toolsMenu.addSeparator()
        self.profileAct = self.toolsMenu.addAction(
            getIcon('profile.png'), 'Profile script',
            self._parent.onProfileScript)
        self.profileParamAct = self.toolsMenu.addAction(
            getIcon('paramsmenu.png'), 'Set parameters and profile',
            self._parent.onProfileScriptDlg)
        self.toolsMenu.addSeparator()
        self.disasmMenu = QMenu('Disassembly')
        self.disasmMenu.setIcon(getIcon('disassembly.png'))
        self.disasmAct0 = self.disasmMenu.addAction(
            getIcon(''), 'Disassembly (no optimization)', self._onDisasm0)
        self.disasmAct1 = self.disasmMenu.addAction(
            getIcon(''), 'Disassembly (optimization level 1)', self._onDisasm1)
        self.disasmAct2 = self.disasmMenu.addAction(
            getIcon(''), 'Disassembly (optimization level 2)', self._onDisasm2)
        self.toolsMenu.addMenu(self.disasmMenu)
        return self.toolsMenu

    def __initDiagramsMenu(self):
        """Creates the diagrams menu"""
        self.diagramsMenu = QMenu("&Diagrams")
        self.importsDgmAct = self.diagramsMenu.addAction(
            getIcon('importsdiagram.png'), 'Imports diagram',
            self._parent.onImportDgm)
        self.importsDgmParamAct = self.diagramsMenu.addAction(
            getIcon('paramsmenu.png'), 'Fine tuned imports diagram',
            self._parent.onImportDgmTuned)
        return self.diagramsMenu

    def contextMenuEvent(self, event):
        """Called just before showing a context menu"""
        # Accepting needs to suppress the native menu
        event.accept()

        isPython = self.isPythonBuffer()
        readOnly = self.isReadOnly()
        self.__menuUndo.setEnabled(self.document().isUndoAvailable())
        self.__menuRedo.setEnabled(self.document().isRedoAvailable())
        self.__menuCut.setEnabled(not readOnly)
        self.__menuPaste.setEnabled(QApplication.clipboard().text() != ""
                                    and not readOnly)

        fileName = self._parent.getFileName()
        absFileName = os.path.isabs(fileName)
        self.__menuOpenAsFile.setEnabled(self.openAsFileAvailable())
        self.__menuDownloadAndShow.setEnabled(self.downloadAndShowAvailable())
        self.__menuOpenInBrowser.setEnabled(self.downloadAndShowAvailable())
        self.__menuHighlightInPrj.setEnabled(
            absFileName and GlobalData().project.isProjectFile(fileName))
        self.__menuHighlightInFS.setEnabled(absFileName)
        self._menuHighlightInOutline.setEnabled(isPython)
        self._menuHighlightInOutline.setEnabled(isPython)

        self.toolsMenu.setEnabled(isPython)
        if isPython:
            runEnabled = self._parent.runScriptButton.isEnabled()
            self.runAct.setEnabled(runEnabled)
            self.runParamAct.setEnabled(runEnabled)
            self.profileAct.setEnabled(runEnabled)
            self.profileParamAct.setEnabled(runEnabled)

        if absFileName:
            self.__menuClearEncoding.setEnabled(
                getFileEncoding(fileName) is not None)
        else:
            self.__menuClearEncoding.setEnabled(
                self.explicitUserEncoding is not None)

        # Check the proper encoding in the menu
        encoding = 'undefined'
        if absFileName:
            enc = getFileEncoding(fileName)
            if enc:
                encoding = enc
        else:
            if self.explicitUserEncoding:
                encoding = self.explicitUserEncoding
        encoding = getNormalizedEncoding(encoding, False)
        if absFileName:
            for act in self.encodingReloadActGrp.actions():
                act.setChecked(encoding == getNormalizedEncoding(act.data()))
        else:
            self.encodingReloadMenu.setEnabled(False)
        for act in self.encodingWriteActGrp.actions():
            act.setChecked(encoding == getNormalizedEncoding(act.data()))

        # Show the menu
        self._menu.popup(event.globalPos())

    def __isSameEncodingAsCurrent(self, enc):
        """True if the same encoding has already been set"""
        fileName = self._parent.getFileName()
        if not os.path.isabs(fileName):
            # New unsaved yet file
            if not self.explicitUserEncoding:
                return False
            return getNormalizedEncoding(enc) == getNormalizedEncoding(
                self.explicitUserEncoding)

        # Existed before or just saved new file
        currentEnc = getFileEncoding(fileName)
        if not currentEnc:
            return False
        return getNormalizedEncoding(currentEnc) == getNormalizedEncoding(enc)

    def __onReloadWithEncoding(self, act):
        """Triggered when encoding is selected"""
        # The method is called only for the existing disk files
        encoding = act.data()
        if self.__isSameEncodingAsCurrent(encoding):
            return

        if self.document().isModified():
            res = QMessageBox.warning(
                self, 'Continue loosing changes',
                '<p>The buffer has unsaved changes. Are you sure to continue '
                'reloading the content using ' + encoding + ' encoding and '
                'loosing the changes?</p>',
                QMessageBox.StandardButtons(QMessageBox.Cancel
                                            | QMessageBox.Yes),
                QMessageBox.Cancel)
            if res == QMessageBox.Cancel:
                return

        # Do the reload
        fileName = self._parent.getFileName()
        setFileEncoding(fileName, encoding)
        self.__updateFilePosition()
        self.readFile(fileName)
        self.__restoreFilePosition()
        self.__updateMainWindowStatusBar()

    def __onReadWriteEncoding(self, act):
        """Sets explicit encoding for further read/write ops"""
        encoding = act.data()
        if self.__isSameEncodingAsCurrent(encoding):
            return

        # fileName = self._parent.getFileName()
        # absFileName = os.path.isabs(fileName)
        self.document().setModified(True)
        self.explicitUserEncoding = encoding
        self.__updateMainWindowStatusBar()

    def __onClearEncoding(self):
        """Clears the explicitly set encoding"""
        self.explicitUserEncoding = None
        fileName = self._parent.getFileName()
        absFileName = os.path.isabs(fileName)
        if absFileName:
            setFileEncoding(fileName, None)
            self.encoding = detectEncodingOnClearExplicit(fileName, self.text)
        self.__updateMainWindowStatusBar()

    def onUndo(self):
        """Undo implementation"""
        if self.document().isUndoAvailable():
            self.undo()
            self._parent.modificationChanged()

    def onRedo(self):
        """Redo implementation"""
        if self.document().isRedoAvailable():
            self.redo()
            self._parent.modificationChanged()

    def __onPluginMenuAdded(self, menu, count):
        """Triggered when a new menu was added"""
        del count  # unused argument
        self._menu.addMenu(menu)
        self.__pluginMenuSeparator.setVisible(True)

    def __onDisasm(self, optimization):
        """Common implementation"""
        if self.isPythonBuffer():
            if os.path.isabs(self._parent.getFileName()):
                if not self._parent.isModified():
                    GlobalData().mainWindow.showFileDisassembly(
                        self._parent.getFileName(), optimization)
                    return
            fileName = self._parent.getFileName()
            if not fileName:
                fileName = self._parent.getShortName()
            encoding = self.encoding
            if not encoding:
                encoding = detectNewFileWriteEncoding(self, fileName)
            GlobalData().mainWindow.showBufferDisassembly(
                self.text, encoding, fileName, optimization)

    def _onDisasm0(self):
        """Triggered to disassemble the buffer without optimization"""
        self.__onDisasm(OPT_NO_OPTIMIZATION)

    def _onDisasm1(self):
        """Triggered to disassemble the buffer with optimization level 1"""
        self.__onDisasm(OPT_OPTIMIZE_ASSERT)

    def _onDisasm2(self):
        """Triggered to disassemble the buffer with optimization level 2"""
        self.__onDisasm(OPT_OPTIMIZE_DOCSTRINGS)

    def __onPluginMenuRemoved(self, menu, count):
        """Triggered when a menu was deleted"""
        self._menu.removeAction(menu.menuAction())
        self.__pluginMenuSeparator.setVisible(count != 0)

    def highlightInOutline(self):
        """Triggered when highlight in outline browser is requested"""
        if self.isPythonBuffer():
            info = getBriefModuleInfoFromMemory(self.text)
            context = getContext(self, info, True, False)
            line, _ = self.cursorPosition
            GlobalData().mainWindow.highlightInOutline(context, int(line) + 1)
            self.setFocus()

    @staticmethod
    def __updateMainWindowStatusBar():
        """Updates the main window status bar"""
        mainWindow = GlobalData().mainWindow
        editorsManager = mainWindow.editorsManagerWidget.editorsManager
        editorsManager.updateStatusBar()

    @staticmethod
    def __updateFilePosition():
        """Updates the position in a file"""
        mainWindow = GlobalData().mainWindow
        editorsManager = mainWindow.editorsManagerWidget.editorsManager
        editorsManager.updateFilePosition(None)

    @staticmethod
    def __restoreFilePosition():
        """Restores the position in a file"""
        mainWindow = GlobalData().mainWindow
        editorsManager = mainWindow.editorsManagerWidget.editorsManager
        editorsManager.restoreFilePosition(None)
Пример #6
0
class CFSceneContextMenuMixin:

    """Encapsulates the context menu handling"""

    def __init__(self):
        self.menu = None
        self.individualMenus = {}

        # Scene menu preparation
        self.sceneMenu = QMenu()
        self.sceneMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...',
                                 self.parent().onSaveAsSVG)
        self.sceneMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...',
                                 self.parent().onSaveAsPDF)
        self.sceneMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...',
                                 self.parent().onSaveAsPNG)
        self.sceneMenu.addSeparator()
        self.sceneMenu.addAction(getIcon('copymenu.png'),
                                 'Copy image to clipboard',
                                 self.parent().copyToClipboard)

        # Common menu for all the individually selected items
        self.commonMenu = QMenu()
        self.__ccSubmenuAction = self.commonMenu.addMenu(
            self.__initCustomColorsContextMenu())
        self.__rtSubmenuAction = self.commonMenu.addMenu(
            self.__initReplaceTextContextMenu())
        self.__docSubmenuAction = self.commonMenu.addMenu(
            self.__initDocContextMenu())
        self.__groupAction = self.commonMenu.addAction(
            getIcon("cfgroup.png"), "Group...", self.onGroup)

        #self.commonMenu.addSeparator()
        #self.__cutAction = self.commonMenu.addAction(
        #    getIcon("cutmenu.png"), "Cut (specific for graphics pane)",
        #    self.onCut)
        #self.__copyAction = self.commonMenu.addAction(
        #    getIcon("copymenu.png"), "Copy (specific for graphics pane)",
        #    self.onCopy)
        #self.commonMenu.addSeparator()
        #self.commonMenu.addAction(
        #    getIcon("trash.png"), "Delete", self.onDelete)

        # Individual items specific menu: begin
        ifContextMenu = QMenu()
        ifContextMenu.addAction(
            getIcon("switchbranches.png"), "Switch branch layout",
            self.onSwitchIfBranch)

        self.individualMenus[IfCell] = ifContextMenu
        self.individualMenus[OpenedGroupBegin] = self.__initOpenGroupContextMenu()
        self.individualMenus[CollapsedGroup] = self.__initCloseGroupContextMenu()
        self.individualMenus[EmptyGroup] = self.__initEmptyGroupContextMenu()

        # Individual items specific menu: end

        # Menu for a group of selected items
        self.groupMenu = QMenu()

    def __initOpenGroupContextMenu(self):
        """Creates the open group context menu"""
        ogMenu = QMenu()
        ogMenu.addAction(getIcon("collapse.png"), "Collapse", self.onGroupCollapse)
        ogMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle)
        ogMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup)
        return ogMenu

    def __initCloseGroupContextMenu(self):
        """Creates the closed group context menu"""
        cgMenu = QMenu()
        cgMenu.addAction(getIcon("expand.png"), "Expand", self.onGroupExpand)
        cgMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle)
        cgMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup)
        return cgMenu

    def __initEmptyGroupContextMenu(self):
        """Creates the empty group context menu"""
        egMenu = QMenu()
        egMenu.addAction(getIcon("replacetitle.png"), "Edit title...", self.onGroupEditTitle)
        egMenu.addAction(getIcon("ungroup.png"), "Ungroup", self.onGroupUngroup)
        return egMenu

    def __initCustomColorsContextMenu(self):
        """Create the custom colors submenu"""
        self.__customColorsSubmenu = QMenu('Custom colors')
        self.__customColorsSubmenu.setIcon(getIcon('customcolorsmenu.png'))
        self.__ccAction = self.__customColorsSubmenu.addAction(
            getIcon("customcolors.png"), "Custom colors...",
            self.onCustomColors)
        self.__customColorsSubmenu.addSeparator()
        self.__removeCCAction = self.__customColorsSubmenu.addAction(
            getIcon('trash.png'), 'Remove custom colors',
            self.onRemoveCustomColors)
        return self.__customColorsSubmenu

    def __initReplaceTextContextMenu(self):
        """Create the Replace text submenu"""
        self.__replaceTextSubmenu = QMenu('Replace text')
        self.__replaceTextSubmenu.setIcon(getIcon('replacetextmenu.png'))
        self.__rtAction = self.__replaceTextSubmenu.addAction(
            getIcon("replacetitle.png"), "Replace text...",
            self.onReplaceText)
        self.__replaceTextSubmenu.addSeparator()
        self.__removeRTAction = self.__replaceTextSubmenu.addAction(
            getIcon('trash.png'), 'Remove replacement text',
            self.onRemoveReplacementText)
        return self.__replaceTextSubmenu

    def __initDocContextMenu(self):
        """Create the Documentation submenu"""
        self.__docSubmenu = QMenu('Documentation')
        self.__docSubmenu.setIcon(getIcon('markdown.png'))
        self.__editDocAction = self.__docSubmenu.addAction(
            getIcon('replacetitle.png'), 'Add/edit doc link/anchor...',
            self.onEditDoc)
        self.__autoDocActon = self.__docSubmenu.addAction(
            getIcon('createdoc.png'),
            'Create doc file, add link and open for editing',
            self.onAutoAddDoc)
        self.__docSubmenu.addSeparator()
        self.__removeDocAction = self.__docSubmenu.addAction(
            getIcon('trash.png'), 'Remove doc link/anchor',
            self.onRemoveDoc)
        return self.__docSubmenu

    def onContextMenu(self, event):
        """Triggered when a context menu should be shown"""
        selectedItems = self.selectedItems()
        selectionCount = len(selectedItems)
        if selectionCount == 0:
            self.sceneMenu.popup(event.screenPos())
            return

        if selectionCount == 1:
            self.__buildIndividualMenu(selectedItems[0])
        else:
            self.__buildGroupMenu(selectedItems)
        self.menu.popup(event.screenPos())

    def __buildIndividualMenu(self, item):
        """Builds a context menu for the given item"""
        self.menu = QMenu()
        if type(item) in self.individualMenus:
            individualPart = self.individualMenus[type(item)]
            self.menu.addActions(individualPart.actions())
            self.menu.addSeparator()
        self.menu.addActions(self.commonMenu.actions())

        # Note: if certain items need to be disabled then it should be done
        #       here
        self.__disableMenuItems()

    def __buildGroupMenu(self, items):
        """Builds a context menu for the group of items"""
        self.menu = QMenu()
        if type(items[0]) in self.individualMenus:
            if self.areSelectedOfTypes([[items[0].kind, items[0].subKind]]):
                individualPart = self.individualMenus[type(items[0])]
                self.menu.addActions(individualPart.actions())
                self.menu.addSeparator()
        self.menu.addActions(self.commonMenu.actions())
        if not self.groupMenu.isEmpty():
            self.menu.addSeparator()
            self.menu.addActions(self.groupMenu.actions())

        # Note: if certain items need to be disabled then it should be done
        #       here
        self.__disableMenuItems()

    def __disableMenuItems(self):
        """Disables the common menu items as needed"""
        totalComments = self.countComments()
        hasComment = totalComments > 0
        hasDocstring = self.isDocstringInSelection()
        hasMinimizedExcepts = self.isInSelected([(CellElement.EXCEPT_MINIMIZED,
                                                  None)])
        # Doc links are considered comments as well
        totalDocLinks = self.countInSelected([(CellElement.INDEPENDENT_DOC, None),
                                              (CellElement.LEADING_DOC, None),
                                              (CellElement.ABOVE_DOC, None)])
        totalNonDocComments = totalComments - totalDocLinks

        totalGroups = sum(self.countGroups())
        count = len(self.selectedItems())
        totalCCGroups = sum(self.countGroupsWithCustomColors())
        totalCCDocs = self.countDocWithCustomColors()

        self.__ccAction.setEnabled(totalNonDocComments == 0 and
                                   not hasMinimizedExcepts)
        self.__removeCCAction.setEnabled(
            self.countItemsWithCML(CMLcc) + totalCCGroups + totalCCDocs == count)
        self.__customColorsSubmenu.setEnabled(self.__ccAction.isEnabled() or
                                              self.__removeCCAction.isEnabled())

        self.__rtAction.setEnabled(not hasComment and
                                   not hasDocstring and
                                   not hasMinimizedExcepts and
                                   totalDocLinks == 0 and
                                   totalGroups == 0)
        self.__removeRTAction.setEnabled(
            self.countItemsWithCML(CMLrt) == count)
        self.__replaceTextSubmenu.setEnabled(self.__rtAction.isEnabled() or
                                             self.__removeRTAction.isEnabled())

        self.__groupAction.setEnabled(self.__canBeGrouped())

        itemsWithDocCML = self.countItemsWithCML(CMLdoc)
        self.__removeDocAction.setEnabled(totalDocLinks + itemsWithDocCML == count)
        if count != 1 or totalNonDocComments != 0 or hasDocstring or totalGroups != 0:
            self.__editDocAction.setEnabled(False)
            self.__autoDocActon.setEnabled(False)
        else:
            self.__editDocAction.setEnabled(True)
            fileName = None
            editor = self.selectedItems()[0].getEditor()
            if editor:
                fileName = editor._parent.getFileName()
            self.__autoDocActon.setEnabled(
                    fileName and totalDocLinks + itemsWithDocCML == 0)
        self.__docSubmenu.setEnabled(self.__removeDocAction.isEnabled() or
                                     self.__editDocAction.isEnabled() or
                                     self.__autoDocActon.isEnabled())

        #self.__cutAction.setEnabled(count == 1)
        #self.__copyAction.setEnabled(count == 1)

    def __actionPrerequisites(self):
        """True if an editor related action can be done"""
        selectedItems = self.selectedItems()
        if not selectedItems:
            return False
        editor = selectedItems[0].getEditor()
        if editor is None:
            return False
        return True

    def onSwitchIfBranch(self):
        """If primitive should switch the branches"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.IF:
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLsw)
                    if cmlComment is None:
                        # Did not exist, so needs to be generated
                        line = CMLsw.generate(item.ref.body.beginPos)
                        lineNo = item.getFirstLine()
                        editor.insertLines(line, lineNo)
                    else:
                        # Existed, so it just needs to be deleted
                        cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def onCustomColors(self):
        """Custom background and foreground colors"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        bgcolor, fgcolor, bordercolor = self.selectedItems()[0].getColors()
        hasDocstring = self.isDocstringInSelection()
        dlg = CustomColorsDialog(bgcolor, fgcolor,
                                 None if hasDocstring else bordercolor,
                                 self.parent())
        if dlg.exec_():
            bgcolor = dlg.backgroundColor()
            fgcolor = dlg.foregroundColor()
            bordercolor = dlg.borderColor()

            editor = self.selectedItems()[0].getEditor()
            with editor:
                # Add colors is done via delete/insert for the Doc and group
                # items. So it is safer to do first because the cc comment may be
                # in a set of selected which is inserted before the doc cml and
                # thus breaks the line numbering
                for item in self.selectedItems():
                    if item.isCMLDoc():
                        # The doc always exists so just add/change the colors
                        item.cmlRef.updateCustomColors(editor, bgcolor,
                                                       fgcolor, bordercolor)
                        continue
                    if item.isGroupItem():
                        # The group always exists so just add/change the colors
                        item.groupBeginCMLRef.updateCustomColors(editor,
                                                                 bgcolor,
                                                                 fgcolor,
                                                                 bordercolor)

                for item in self.sortSelectedReverse():
                    if item.isCMLDoc() or item.isGroupItem():
                        continue
                    if item.isDocstring():
                        cmlComment = CMLVersion.find(
                            item.ref.docstring.leadingCMLComments,
                            CMLcc)
                    else:
                        cmlComment = CMLVersion.find(
                            item.ref.leadingCMLComments, CMLcc)
                    if cmlComment is not None:
                        # Existed, so remove the old one first
                        lineNo = cmlComment.ref.beginLine
                        cmlComment.removeFromText(editor)
                    else:
                        lineNo = item.getFirstLine()

                    pos = item.ref.body.beginPos
                    if item.isDocstring():
                        pos = item.ref.docstring.beginPos
                    line = CMLcc.generate(bgcolor, fgcolor, bordercolor, pos)
                    editor.insertLines(line, lineNo)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onReplaceText(self):
        """Replace the code with a title"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        dlg = ReplaceTextDialog('Replace text', 'Item caption:', self.parent())

        # If it was one item selection and there was a previous text then
        # set it for editing
        if len(self.selectedItems()) == 1:
            cmlComment = CMLVersion.find(
                self.selectedItems()[0].ref.leadingCMLComments, CMLrt)
            if cmlComment is not None:
                dlg.setText(cmlComment.getText())

        if dlg.exec_():
            replacementText = dlg.text()
            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    cmlComment = CMLVersion.find(
                        item.ref.leadingCMLComments, CMLrt)
                    if cmlComment is not None:
                        # Existed, so remove the old one first
                        lineNo = cmlComment.ref.beginLine
                        cmlComment.removeFromText(editor)
                    else:
                        lineNo = item.getFirstLine()

                    line = CMLrt.generate(replacementText,
                                          item.ref.body.beginPos)
                    editor.insertLines(line, lineNo)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onGroupCollapse(self):
        """Collapses the selected group"""
        if not self.__actionPrerequisites():
            return

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.OPENED_GROUP_BEGIN:
                    fileName = editor._parent.getFileName()
                    if not fileName:
                        fileName = editor._parent.getShortName()
                    addCollapsedGroup(fileName, item.getGroupId())

        QApplication.processEvents()
        self.parent().redrawNow()

    def onGroupExpand(self):
        """Expands the selected group"""
        if not self.__actionPrerequisites():
            return

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.COLLAPSED_GROUP:
                    fileName = editor._parent.getFileName()
                    if not fileName:
                        fileName = editor._parent.getShortName()
                    removeCollapsedGroup(fileName, item.getGroupId())

        QApplication.processEvents()
        self.parent().redrawNow()

    def onGroupEditTitle(self):
        """Edit (or view) the group title"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent())

        # If it was one item selection and there was a previous text then
        # set it for editing
        if len(self.selectedItems()) == 1:
            title = self.selectedItems()[0].getTitle()
            if title:
                dlg.setText(title)

        if dlg.exec_():
            newTitle = dlg.text()
            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    item.groupBeginCMLRef.updateTitle(editor, newTitle)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onGroupUngroup(self):
        """Ungroups the items"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                item.groupEndCMLRef.removeFromText(editor)
                item.groupBeginCMLRef.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByTooltip(selection)

    def onDelete(self):
        """Delete the item"""
        print("Delete")

    def onGroup(self):
        """Groups items into a single one"""
        dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent())

        if dlg.exec_():
            groupTitle = dlg.text()
            selected = self.__extendSelectionForGrouping()
            selected = self.sortSelected(selected)
            editor = selected[0].getEditor()

            firstLine, lastLine, pos = self.__getLineRange(selected)

            groupid = self.parent().generateNewGroupId()
            beginComment = CMLgb.generate(groupid, groupTitle,
                                          None, None, None, pos)
            endComment = CMLge.generate(groupid, pos)

            with editor:
                editor.insertLines(endComment, lastLine + 1)
                editor.insertLines(beginComment, firstLine)

            # Redraw the group collapsed
            fileName = editor._parent.getFileName()
            if not fileName:
                fileName = editor._parent.getShortName()
            addCollapsedGroup(fileName, groupid)

            QApplication.processEvents()
            self.parent().redrawNow()

    def onCopy(self):
        """Copying..."""
        selectedItems = self.selectedItems()
        if selectedItems:
            if len(selectedItems) > 1:
                print('Copying multiple items has not been implemented yet')
                return
            selectedItems[0].copyToClipboard()

    def onCut(self):
        """Cutting..."""
        print("Cut")

    def onRemoveCustomColors(self):
        """Removing the previously set custom colors"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        editor = self.selectedItems()[0].getEditor()
        with editor:
            # Remove colors is done via delete/insert for the Doc and group
            # items. So it is safer to do first because the cc comment may be
            # in a set of selected which is inserted before the doc cml and
            # thus breaks the line numbering
            for item in self.selectedItems():
                # The doc always exists
                if item.isCMLDoc():
                    item.cmlRef.removeCustomColors(editor)
                    continue
                # The group always exists
                if item.isGroupItem():
                    item.groupBeginCMLRef.removeCustomColors(editor)

            # Now handle the rest of items
            for item in self.sortSelectedReverse():
                if item.isCMLDoc() or item.isGroupItem():
                    continue
                if item.isDocstring():
                    cmlComment = CMLVersion.find(
                        item.ref.docstring.leadingCMLComments, CMLcc)
                else:
                    cmlComment = CMLVersion.find(
                        item.ref.leadingCMLComments, CMLcc)
                if cmlComment is not None:
                    cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def onRemoveReplacementText(self):
        """Removing replacement text"""
        if self.__actionPrerequisites():
            # Memorize the current selection
            selection = self.serializeSelection()

            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLrt)
                    if cmlComment is not None:
                        cmlComment.removeFromText(editor)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def areSelectedOfTypes(self, matchList):
        """Checks if the selected items belong to the match"""
        # match is a list of pairs [kind, subKind]
        #   None would mean 'match any'
        selectedItems = self.selectedItems()
        if selectedItems:
            for selectedItem in selectedItems:
                for kind, subKind in matchList:
                    match = True
                    if kind is not None:
                        if kind != selectedItem.kind:
                            match = False
                    if subKind is not None:
                        if subKind != selectedItem.subKind:
                            match = False
                    if match:
                        break
                else:
                    return False
            return True
        return False

    def __createDocFile(self, link, fromFile):
        """Creates the doc file if needed"""
        fName, _, errMsg = preResolveLinkPath(link, fromFile, True)
        if errMsg:
            logging.error(errMsg)
            return None

        if os.path.exists(fName):
            return fName

        try:
            os.makedirs(os.path.dirname(fName), exist_ok=True)
            with open(fName, 'w') as f:
                pass
        except Exception as exc:
            logging.error('Error creating the documentation file ' +
                          fName + ': ' + str(exc))
            return None
        return fName

    def onEditDoc(self):
        """Editing the CML doc comment"""
        if not self.__actionPrerequisites():
            return

        selectedItem = self.selectedItems()[0]  # Exactly one is selected
        editor = selectedItem.getEditor()
        fileName = editor._parent.getFileName()
        if not fileName:
            fileName = editor._parent.getShortName()

        # It could be a CML doc or an item which has a CML doc
        if selectedItem.isComment():
            cmlRef = selectedItem.cmlRef
        else:
            # If not found then it means the doc link needs to be created
            cmlRef = self.__findCMLinItem(selectedItem, CMLdoc)

        dlg = DocLinkAnchorDialog('Add' if cmlRef is None else 'Edit',
                                  cmlRef, fileName, self.parent())
        if dlg.exec_():
            link = dlg.linkEdit.text().strip()
            anchor = dlg.anchorEdit.text().strip()
            title = dlg.title()
            needToCreate = dlg.needToCreate()

            # First create a file if asked
            if needToCreate:
                docFileName = self.__createDocFile(link, fileName)
                if not docFileName:
                    return

            selection = self.serializeSelection()
            with editor:
                # Now insert a new cml comment or update existing
                if cmlRef:
                    # It is editing, the comment exists
                    lineNo = cmlRef.ref.beginLine
                    pos = cmlRef.ref.beginPos
                    cmlRef.removeFromText(editor)
                    bgColor = cmlRef.bgColor
                    fgColor = cmlRef.fgColor
                    border = cmlRef.border
                else:
                    # It is a new doc link
                    lineNo = selectedItem.getFirstLine()
                    pos = selectedItem.ref.body.beginPos
                    bgColor = None
                    fgColor = None
                    border = None

                line = CMLdoc.generate(link, anchor, title,
                                       bgColor, fgColor, border, pos)
                editor.insertLines(line, lineNo)

            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    @staticmethod
    def __getAutoDocFileName(fileName):
        """Forms the auto doc file name"""
        # Markdown is used as a default documentation format
        fBaseName = os.path.basename(fileName)
        if '.' in fBaseName:
            fileExtension = fBaseName.split('.')[-1]
            fBaseName = fBaseName[:-len(fileExtension)] + 'md'
        else:
            fBaseName += '.md'

        project = GlobalData().project
        if project.isProjectFile(fileName):
            projectDir = project.getProjectDir()
            relativePath = fileName[len(projectDir):]
            projectName = project.getProjectName()
            if relativePath.startswith(projectName):
                relativePath = relativePath.replace(projectName, '', 1)
            return os.path.normpath(
                os.path.sep.join([projectDir + 'doc',
                                  os.path.dirname(relativePath),
                                  fBaseName]))
        return os.path.normpath(
            os.path.sep.join([os.path.dirname(fileName),
                              'doc',
                               fBaseName]))

    def onAutoAddDoc(self):
        """Create a doc file, add a link and open for editing"""
        if not self.__actionPrerequisites():
            return

        selectedItem = self.selectedItems()[0]  # Exactly one is selected
        editor = selectedItem.getEditor()
        fileName = editor._parent.getFileName()
        if not fileName:
            logging.error('Save file before invoking auto doc')
            return

        needContent = False
        newAnchor = 'doc' + str(uuid.uuid4().fields[-1])[-6:]

        docFileName = self.__getAutoDocFileName(fileName)
        if not os.path.exists(docFileName):
            # Create file and populate with the default content
            try:
                os.makedirs(os.path.dirname(docFileName), exist_ok=True)
                with open(docFileName, 'w') as f:
                    pass
            except Exception as exc:
                logging.error('Error creating the documentation file ' +
                              docFileName + ': ' + str(exc))
                return
            needContent = True

        project = GlobalData().project
        if project.isProjectFile(docFileName):
            link = project.getRelativePath(docFileName)
        else:
            link = os.path.relpath(docFileName, fileName)

        # Insert a doc link
        with editor:
            lineNo = selectedItem.getFirstLine()
            line = CMLdoc.generate(link, newAnchor, 'See documentation',
                                   None, None, None,
                                   selectedItem.ref.body.beginPos)
            editor.insertLines(line, lineNo)

            QApplication.processEvents()
            self.parent().redrawNow()

        # Open the file
        if GlobalData().mainWindow.openFile(docFileName, -1):
            if needContent:
                widget = GlobalData().mainWindow.em.getWidgetForFileName(docFileName)
                editor = widget.getEditor()
                editor.text = getDefaultFileDoc(fileName, newAnchor)
                editor.document().setModified(False)

    def onRemoveDoc(self):
        """Removing the CML doc comment"""
        if not self.__actionPrerequisites():
            return

        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                             CMLdoc)
                if cmlComment is not None:
                    cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()

    def countInSelected(self, matchList):
        """Counts the number of matching items in selection"""
        # match is a list of pairs [kind, subKind]
        #   None would mean 'match any'
        count = 0
        for selectedItem in self.selectedItems():
            for kind, subKind in matchList:
                match = True
                if kind is not None:
                    if kind != selectedItem.kind:
                        match = False
                if subKind is not None:
                    if subKind != selectedItem.subKind:
                        match = False
                if match:
                    count += 1
        return count

    def isInSelected(self, matchList):
        """Checks if any if the match list items is in the selection"""
        return self.countInSelected(matchList) > 0

    def isDocstringInSelection(self):
        """True if a docstring item in the selection"""
        for item in self.selectedItems():
            if item.isDocstring():
                return True
        return False

    def countComments(self):
        """Count comments in selection"""
        count = 0
        for item in self.selectedItems():
            if item.isComment():
                count += 1
        return count

    def isCommentInSelection(self):
        """True if a comment item in the selection"""
        return self.countComments() > 0

    def countItemsWithCML(self, cmlType):
        """Counts items with have a certain type of a CML comment"""
        count = 0
        for item in self.selectedItems():
            if self.__findCMLinItem(item, cmlType) is not None:
                count += 1
        return count

    def __findCMLinItem(self, item, cmlType):
        """Finds a related CML item"""
        if item.isComment():
            # Doc links are comments so they are skipped here
            return None
        if item.isDocstring():
            # Side comments for docstrings? Nonesense! So they are ignored
            # even if they are collected
            cml = CMLVersion.find(item.ref.docstring.leadingCMLComments, cmlType)
            if cml is not None:
                return cml

        if hasattr(item.ref, 'leadingCMLComments'):
            cml = CMLVersion.find(item.ref.leadingCMLComments, cmlType)
            if cml is not None:
                return cml
        if hasattr(item.ref, 'sideCMLComments'):
            cml = CMLVersion.find(item.ref.sideCMLComments, cmlType)
            if cml is not None:
                return cml
        return None

    def countGroups(self):
        """Counts empty, close and open groups"""
        emptyCount = 0
        closeCount = 0
        openCount = 0
        for item in self.selectedItems():
            if item.kind == CellElement.EMPTY_GROUP:
                emptyCount += 1
            elif item.kind == CellElement.COLLAPSED_GROUP:
                closeCount += 1
            elif item.kind == CellElement.OPENED_GROUP_BEGIN:
                openCount += 1
        return emptyCount, closeCount, openCount

    def countGroupsWithCustomColors(self):
        """Counts the groups with any color defined"""
        emptyCount = 0
        closeCount = 0
        openCount = 0
        for item in self.selectedItems():
            if item.kind in [CellElement.EMPTY_GROUP,
                             CellElement.COLLAPSED_GROUP,
                             CellElement.OPENED_GROUP_BEGIN]:
                if item.groupBeginCMLRef.bgColor is not None or \
                   item.groupBeginCMLRef.fgColor is not None or \
                   item.groupBeginCMLRef.border is not None:
                    if item.kind == CellElement.EMPTY_GROUP:
                        emptyCount += 1
                    elif item.kind == CellElement.COLLAPSED_GROUP:
                        closeCount += 1
                    else:
                        openCount += 1
        return emptyCount, closeCount, openCount

    def countDocWithCustomColors(self):
        count = 0
        for item in self.selectedItems():
            if item.isCMLDoc():
                if item.cmlRef.bgColor is not None or \
                   item.cmlRef.fgColor is not None or \
                   item.cmlRef.border is not None:
                    count += 1
        return count

    def sortSelectedReverse(self):
        """Sorts the selected items in reverse order"""
        result = []
        for item in self.selectedItems():
            itemBegin = item.getAbsPosRange()[0]
            for index in range(len(result)):
                if itemBegin > result[index].getAbsPosRange()[0]:
                    result.insert(index, item)
                    break
            else:
                result.append(item)
        return result

    def sortSelected(self, selected):
        """Sorts the selected items in direct order"""
        result = []
        for item in selected:
            itemBegin = item.getAbsPosRange()[0]
            for index in range(len(result)):
                if itemBegin < result[index].getAbsPosRange()[0]:
                    result.insert(index, item)
                    break
            else:
                result.append(item)
        return result

    def __canBeGrouped(self):
        """True if the selected items can be grouped"""
        # Cannot import it at the top...
        from .flowuiwidget import SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT

        if Settings()['smartZoom'] not in [SMART_ZOOM_ALL,
                                           SMART_ZOOM_NO_CONTENT]:
            return False
        if self.__areAllSelectedComments():
            return False
        if self.__areScopeDocstringOrCommentSelected():
            return False
        if self.__isModuleSelected():
            return False

        # Extend the selection with all the selected items comments
        selected = self.__extendSelectionForGrouping()

        if self.__areLoneCommentsSelected(selected):
            return False

        if self.__areIncompleteScopeSelected(selected):
            return False

        scopeCoveredRegions = self.__getSelectedScopeRegions(selected)

        # The __areIfFullySelected() also updates the regions with
        # fully selected if regions
        if not self.__areIfFullySelected(selected, scopeCoveredRegions):
            return False

        selected = self.sortSelected(selected)
        begin = selected[0].getAbsPosRange()[0]
        end = selected[-1].getAbsPosRange()[1]

        if not self.__isSelectionContinuous(selected, scopeCoveredRegions,
                                            begin, end):
            return False

        if self.__moreThanOneIfBranchSelected(selected, scopeCoveredRegions):
            return False
        return True

    def __areAllSelectedComments(self):
        """True if all selected items are comments"""
        for item in self.selectedItems():
            if not item.isComment():
                return False
        return True

    def __areScopeDocstringOrCommentSelected(self):
        for item in self.selectedItems():
            if item.scopedItem():
                if item.subKind in [ScopeCellElement.SIDE_COMMENT,
                                    ScopeCellElement.DOCSTRING]:
                    return True
        return False

    def __isModuleSelected(self):
        """True if the whole module is selected"""
        for item in self.selectedItems():
            if item.kind == CellElement.FILE_SCOPE:
                return True
        return False

    def __areIncompleteScopeSelected(self, selected):
        """True if an incomplete scope selected"""
        for item in selected:
            if item.kind in [CellElement.FOR_SCOPE,
                             CellElement.WHILE_SCOPE]:
                if item.ref.elsePart:
                    for relatedItem in self.findItemsForRef(item.ref.elsePart):
                        if relatedItem not in selected:
                            return True
            elif item.kind in [CellElement.TRY_SCOPE]:
                # It could be that the exception blocks are hidden, so there
                # will be exactly one more item instead of many and that item
                # will have a ref which matches the try scope.
                exceptPartCount = 0
                for exceptPart in item.ref.exceptParts:
                    for relatedItem in self.findItemsForRef(exceptPart):
                        exceptPartCount += 1
                        if relatedItem not in selected:
                            return True
                if exceptPartCount == 0:
                    # here: no except blocks on the diagram, they are collapsed
                    tryItems = self.findItemsForRef(item.ref)
                    for tryItem in tryItems:
                        if tryItem.kind == CellElement.EXCEPT_MINIMIZED:
                            if not tryItem.isSelected():
                                return True
                            break
                    else:
                        # The minimized except is not selected
                        return True
                if item.ref.elsePart:
                    for relatedItem in self.findItemsForRef(item.ref.elsePart):
                        if relatedItem not in selected:
                            return True
                if item.ref.finallyPart:
                    for relatedItem in self.findItemsForRef(item.ref.finallyPart):
                        if relatedItem not in selected:
                            return True
            elif item.kind in [CellElement.ELSE_SCOPE,
                               CellElement.EXCEPT_SCOPE,
                               CellElement.FINALLY_SCOPE]:
                for relatedItem in self.findItemsForRef(item.leaderRef):
                    if relatedItem not in selected:
                        return True
            elif item.kind == CellElement.EXCEPT_MINIMIZED:
                # here: no except blocks on the diagram, they are collapsed
                tryItems = self.findItemsForRef(item.ref)
                for tryItem in tryItems:
                    if tryItem.kind == CellElement.TRY_SCOPE:
                        if tryItem.subKind == ScopeCellElement.TOP_LEFT:
                            if not tryItem.isSelected():
                                return True
                            break
                else:
                    # The try is not selected
                    return True
        return False

    def __extendSelectionForGrouping(self):
        """Extends the selection with the leading and side comments"""
        boundComments = []
        selected = self.selectedItems()
        for item in selected:
            if not item.isComment() and not self.isOpenGroupItem(item):
                for relatedItem in self.findItemsForRef(item.ref):
                    if relatedItem not in selected:
                        boundComments.append(relatedItem)
        return selected + boundComments

    def __areLoneCommentsSelected(self, selected):
        """True if there are comments selected which have no main item selected"""
        for item in selected:
            if item.isComment():
                if item.kind in [CellElement.SIDE_COMMENT,
                                 CellElement.LEADING_COMMENT,
                                 CellElement.ABOVE_COMMENT]:
                    for relatedItem in self.findItemsForRef(item.ref):
                        if relatedItem not in selected:
                            return True
        return False

    def __getLineRange(self, selected):
        first = selected[0]
        last = selected[-1]

        if first.kind == CellElement.OPENED_GROUP_BEGIN:
            firstLine = first.groupBeginCMLRef.ref.parts[0].beginLine
            pos = first.groupBeginCMLRef.ref.parts[0].beginPos
        else:
            firstLine = first.getLineRange()[0]
            pos = first.ref.beginPos

        if last.scopedItem():
            lastLine = last.ref.endLine
        elif last.kind == CellElement.OPENED_GROUP_BEGIN:
            lastLine = last.groupEndCMLRef.ref.parts[-1].endLine
        else:
            lastLine = last.getLineRange()[1]
        return firstLine, lastLine, pos

    def __getSelectedScopeRegions(self, selected):
        """Provides the regions of the selected scope items"""
        coveredRegions = []
        for item in selected:
            if item.scopedItem():
                if item.subKind in [ScopeCellElement.TOP_LEFT]:
                    if item.ref.leadingComment:
                        coveredRegions.append((item.ref.leadingComment.begin,
                                               item.ref.end))
                    else:
                        coveredRegions.append((item.ref.begin, item.ref.end))
            elif item.kind == CellElement.OPENED_GROUP_BEGIN:
                coveredRegions.append(item.getAbsPosRange())
        return coveredRegions

    def __areIfFullySelected(self, selected, regions):
        """Checks if selected IFs are fully selected"""
        for item in selected:
            if item.kind == CellElement.IF:
                ifBegin = item.ref.begin
                ifEnd = item.ref.end
                for item in self.items():
                    if item.isProxyItem():
                        continue
                    if item.scopedItem():
                        if item.subKind not in [ScopeCellElement.TOP_LEFT,
                                                ScopeCellElement.DOCSTRING,
                                                ScopeCellElement.SIDE_COMMENT]:
                            continue
                    if item in selected:
                        continue
                    itemRange = item.getAbsPosRange()
                    if self.isInRegion(itemRange[0], itemRange[1], regions):
                        continue
                    if itemRange[0] > ifBegin and itemRange[0] < ifEnd:
                        return False
                    if itemRange[1] > ifBegin and itemRange[1] < ifEnd:
                        return False
                regions.append([ifBegin, ifEnd])
        return True

    @staticmethod
    def isInRegion(start, finish, regions):
        for region in regions:
            if start >= region[0] and finish <= region[1]:
                return True
        return False

    def __isSelectionContinuous(self, selected, regions, begin, end):
        """Checks if the selection is continuous"""
        for item in self.items():
            if item.isProxyItem():
                continue
            if item.scopedItem():
                if item.subKind not in [ScopeCellElement.TOP_LEFT,
                                        ScopeCellElement.DOCSTRING,
                                        ScopeCellElement.SIDE_COMMENT]:
                    continue
            if item in selected:
                continue
            itemRange = item.getAbsPosRange()
            if self.isInRegion(itemRange[0], itemRange[1], regions):
                continue

            # It is important to keep < and > instead of <= and >=
            # This is because the scopes start with the first statement
            if itemRange[0] > begin and itemRange[0] < end:
                return False
            if itemRange[1] > begin and itemRange[1] < end:
                return False
        return True

    def __moreThanOneIfBranchSelected(self, selected, regions):
        """Checks that the continuous selected items belong to more than one
        not selected IF statements
        """
        ifRef = None
        for item in selected:
            if item.kind != CellElement.IF:
                itemRange = item.getAbsPosRange()
                if item.kind != CellElement.OPENED_GROUP_BEGIN:
                    if self.isInRegion(itemRange[0], itemRange[1], regions):
                        # Here: an item is in a selected scope item, in a selected
                        #       open group or in a fully selected if
                        continue
                # Test if an item belongs to an if statement branch
                if item.kind in [CellElement.OPENED_GROUP_BEGIN,
                                 CellElement.EMPTY_GROUP,
                                 CellElement.COLLAPSED_GROUP]:
                    branchId = item.groupBeginCMLRef.ref.getParentIfID()
                elif item.kind in [CellElement.INDEPENDENT_DOC,
                                   CellElement.LEADING_DOC,
                                   CellElement.ABOVE_DOC]:
                    branchId = item.cmlRef.ref.getParentIfID()
                else:
                    branchId = item.ref.getParentIfID()
                if branchId is not None:
                    if ifRef is None:
                        ifRef = branchId
                    else:
                        if branchId != ifRef:
                            # Selected items belong to more than one branch
                            return True
        return False
Пример #7
0
class CFSceneContextMenuMixin:
    """Encapsulates the context menu handling"""
    def __init__(self):
        self.menu = None
        self.individualMenus = {}

        # Scene menu preparation
        self.sceneMenu = QMenu()
        self.sceneMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...',
                                 self.parent().onSaveAsSVG)
        self.sceneMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...',
                                 self.parent().onSaveAsPDF)
        self.sceneMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...',
                                 self.parent().onSaveAsPNG)
        self.sceneMenu.addSeparator()
        self.sceneMenu.addAction(getIcon('copymenu.png'),
                                 'Copy image to clipboard',
                                 self.parent().copyToClipboard)

        # Common menu for all the individually selected items
        self.commonMenu = QMenu()
        self.__ccAction = self.commonMenu.addAction(
            getIcon("customcolors.png"), "Custom colors...",
            self.onCustomColors)
        self.__rtAction = self.commonMenu.addAction(
            getIcon("replacetitle.png"), "Replace text...", self.onReplaceText)
        self.__groupAction = self.commonMenu.addAction(getIcon("cfgroup.png"),
                                                       "Group...",
                                                       self.onGroup)
        self.commonMenu.addSeparator()
        self.__removeCCAction = self.commonMenu.addAction(
            getIcon('trash.png'), 'Remove custom colors',
            self.onRemoveCustomColors)
        self.__removeRTAction = self.commonMenu.addAction(
            getIcon('trash.png'), 'Remove replacement text',
            self.onRemoveReplacementText)
        #self.commonMenu.addSeparator()
        #self.__cutAction = self.commonMenu.addAction(
        #    getIcon("cutmenu.png"), "Cut (specific for graphics pane)",
        #    self.onCut)
        #self.__copyAction = self.commonMenu.addAction(
        #    getIcon("copymenu.png"), "Copy (specific for graphics pane)",
        #    self.onCopy)
        #self.commonMenu.addSeparator()
        #self.commonMenu.addAction(
        #    getIcon("trash.png"), "Delete", self.onDelete)

        # Individual items specific menu: begin
        ifContextMenu = QMenu()
        ifContextMenu.addAction(getIcon("switchbranches.png"),
                                "Switch branch layout", self.onSwitchIfBranch)

        openGroupContextMenu = QMenu()
        openGroupContextMenu.addAction(getIcon("collapse.png"), "Collapse",
                                       self.onGroupCollapse)
        openGroupContextMenu.addAction(getIcon("replacetitle.png"),
                                       "Edit title...", self.onGroupEditTitle)
        openGroupContextMenu.addAction(getIcon("ungroup.png"), "Ungroup",
                                       self.onGroupUngroup)

        closeGroupContextMenu = QMenu()
        closeGroupContextMenu.addAction(getIcon("expand.png"), "Expand",
                                        self.onGroupExpand)
        closeGroupContextMenu.addAction(getIcon("replacetitle.png"),
                                        "Edit title...", self.onGroupEditTitle)
        closeGroupContextMenu.addAction(getIcon("ungroup.png"), "Ungroup",
                                        self.onGroupUngroup)

        emptyGroupContextMenu = QMenu()
        emptyGroupContextMenu.addAction(getIcon("replacetitle.png"),
                                        "Edit title...", self.onGroupEditTitle)
        emptyGroupContextMenu.addAction(getIcon("ungroup.png"), "Ungroup",
                                        self.onGroupUngroup)

        self.individualMenus[IfCell] = ifContextMenu
        self.individualMenus[OpenedGroupBegin] = openGroupContextMenu
        self.individualMenus[CollapsedGroup] = closeGroupContextMenu
        self.individualMenus[EmptyGroup] = emptyGroupContextMenu
        # Individual items specific menu: end

        # Menu for a group of selected items
        self.groupMenu = QMenu()

    def onContextMenu(self, event):
        """Triggered when a context menu should be shown"""
        selectedItems = self.selectedItems()
        selectionCount = len(selectedItems)
        if selectionCount == 0:
            self.sceneMenu.popup(event.screenPos())
            return

        if selectionCount == 1:
            self.__buildIndividualMenu(selectedItems[0])
        else:
            self.__buildGroupMenu(selectedItems)
        self.menu.popup(event.screenPos())

    def __buildIndividualMenu(self, item):
        """Builds a context menu for the given item"""
        self.menu = QMenu()
        if type(item) in self.individualMenus:
            individualPart = self.individualMenus[type(item)]
            self.menu.addActions(individualPart.actions())
            self.menu.addSeparator()
        self.menu.addActions(self.commonMenu.actions())

        # Note: if certain items need to be disabled then it should be done
        #       here
        self.__disableMenuItems()

    def __buildGroupMenu(self, items):
        """Builds a context menu for the group of items"""
        self.menu = QMenu()
        if type(items[0]) in self.individualMenus:
            if self.areSelectedOfTypes([[items[0].kind, items[0].subKind]]):
                individualPart = self.individualMenus[type(items[0])]
                self.menu.addActions(individualPart.actions())
                self.menu.addSeparator()
        self.menu.addActions(self.commonMenu.actions())
        if not self.groupMenu.isEmpty():
            self.menu.addSeparator()
            self.menu.addActions(self.groupMenu.actions())

        # Note: if certain items need to be disabled then it should be done
        #       here
        self.__disableMenuItems()

    def __disableMenuItems(self):
        """Disables the common menu items as needed"""
        hasComment = self.isCommentInSelection()
        hasDocstring = self.isDocstringInSelection()
        hasMinimizedExcepts = self.isInSelected([(CellElement.EXCEPT_MINIMIZED,
                                                  None)])
        totalGroups = sum(self.countGroups())
        count = len(self.selectedItems())

        self.__ccAction.setEnabled(not hasComment and not hasMinimizedExcepts)
        self.__rtAction.setEnabled(not hasComment and not hasDocstring
                                   and not hasMinimizedExcepts
                                   and totalGroups == 0)

        totalCCGroups = sum(self.countGroupsWithCustomColors())
        self.__removeCCAction.setEnabled(
            self.countItemsWithCML(CMLcc) + totalCCGroups == count)
        self.__removeRTAction.setEnabled(
            self.countItemsWithCML(CMLrt) == count)
        self.__groupAction.setEnabled(self.__canBeGrouped())
        #self.__cutAction.setEnabled(count == 1)
        #self.__copyAction.setEnabled(count == 1)

    def __actionPrerequisites(self):
        """True if an editor related action can be done"""
        selectedItems = self.selectedItems()
        if not selectedItems:
            return False
        editor = selectedItems[0].getEditor()
        if editor is None:
            return False
        return True

    def onSwitchIfBranch(self):
        """If primitive should switch the branches"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.IF:
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLsw)
                    if cmlComment is None:
                        # Did not exist, so needs to be generated
                        line = CMLsw.generate(item.ref.body.beginPos)
                        lineNo = item.getFirstLine()
                        editor.insertLines(line, lineNo)
                    else:
                        # Existed, so it just needs to be deleted
                        cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def onCustomColors(self):
        """Custom background and foreground colors"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        bgcolor, fgcolor, bordercolor = self.selectedItems()[0].getColors()
        hasDocstring = self.isDocstringInSelection()
        dlg = CustomColorsDialog(bgcolor, fgcolor,
                                 None if hasDocstring else bordercolor,
                                 self.parent())
        if dlg.exec_():
            bgcolor = dlg.backgroundColor()
            fgcolor = dlg.foregroundColor()
            bordercolor = dlg.borderColor()

            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    if item.kind in [
                            CellElement.OPENED_GROUP_BEGIN,
                            CellElement.COLLAPSED_GROUP,
                            CellElement.EMPTY_GROUP
                    ]:
                        # The group always exists so just add/change the colors
                        item.groupBeginCMLRef.updateCustomColors(
                            editor, bgcolor, fgcolor, bordercolor)
                        continue
                    if item.isDocstring():
                        cmlComment = CMLVersion.find(
                            item.ref.docstring.leadingCMLComments, CMLcc)
                    else:
                        cmlComment = CMLVersion.find(
                            item.ref.leadingCMLComments, CMLcc)
                    if cmlComment is not None:
                        # Existed, so remove the old one first
                        lineNo = cmlComment.ref.beginLine
                        cmlComment.removeFromText(editor)
                    else:
                        lineNo = item.getFirstLine()

                    pos = item.ref.body.beginPos
                    if item.isDocstring():
                        pos = item.ref.docstring.beginPos
                    line = CMLcc.generate(bgcolor, fgcolor, bordercolor, pos)
                    editor.insertLines(line, lineNo)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onReplaceText(self):
        """Replace the code with a title"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        dlg = ReplaceTextDialog('Replace text', 'Item caption:', self.parent())

        # If it was one item selection and there was a previous text then
        # set it for editing
        if len(self.selectedItems()) == 1:
            cmlComment = CMLVersion.find(
                self.selectedItems()[0].ref.leadingCMLComments, CMLrt)
            if cmlComment is not None:
                dlg.setText(cmlComment.getText())

        if dlg.exec_():
            replacementText = dlg.text()
            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLrt)
                    if cmlComment is not None:
                        # Existed, so remove the old one first
                        lineNo = cmlComment.ref.beginLine
                        cmlComment.removeFromText(editor)
                    else:
                        lineNo = item.getFirstLine()

                    line = CMLrt.generate(replacementText,
                                          item.ref.body.beginPos)
                    editor.insertLines(line, lineNo)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onGroupCollapse(self):
        """Collapses the selected group"""
        if not self.__actionPrerequisites():
            return

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.OPENED_GROUP_BEGIN:
                    fileName = editor._parent.getFileName()
                    if not fileName:
                        fileName = editor._parent.getShortName()
                    addCollapsedGroup(fileName, item.getGroupId())

        QApplication.processEvents()
        self.parent().redrawNow()

    def onGroupExpand(self):
        """Expands the selected group"""
        if not self.__actionPrerequisites():
            return

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.COLLAPSED_GROUP:
                    fileName = editor._parent.getFileName()
                    if not fileName:
                        fileName = editor._parent.getShortName()
                    removeCollapsedGroup(fileName, item.getGroupId())

        QApplication.processEvents()
        self.parent().redrawNow()

    def onGroupEditTitle(self):
        """Edit (or view) the group title"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent())

        # If it was one item selection and there was a previous text then
        # set it for editing
        if len(self.selectedItems()) == 1:
            title = self.selectedItems()[0].getTitle()
            if title:
                dlg.setText(title)

        if dlg.exec_():
            newTitle = dlg.text()
            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    item.groupBeginCMLRef.updateTitle(editor, newTitle)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onGroupUngroup(self):
        """Ungroups the items"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                item.groupEndCMLRef.removeFromText(editor)
                item.groupBeginCMLRef.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByTooltip(selection)

    def onDelete(self):
        """Delete the item"""
        print("Delete")

    def onGroup(self):
        """Groups items into a single one"""
        dlg = ReplaceTextDialog('Group title', 'Group title:', self.parent())

        if dlg.exec_():
            groupTitle = dlg.text()
            selected = self.__extendSelectionForGrouping()
            selected = self.sortSelected(selected)
            editor = selected[0].getEditor()

            firstLine, lastLine, pos = self.__getLineRange(selected)

            groupid = self.parent().generateNewGroupId()
            beginComment = CMLgb.generate(groupid, groupTitle, None, None,
                                          None, pos)
            endComment = CMLge.generate(groupid, pos)

            with editor:
                editor.insertLines(endComment, lastLine + 1)
                editor.insertLines(beginComment, firstLine)

            # Redraw the group collapsed
            fileName = editor._parent.getFileName()
            if not fileName:
                fileName = editor._parent.getShortName()
            addCollapsedGroup(fileName, groupid)

            QApplication.processEvents()
            self.parent().redrawNow()

    def onCopy(self):
        """Copying..."""
        selectedItems = self.selectedItems()
        if selectedItems:
            if len(selectedItems) > 1:
                print('Copying multiple items has not been implemented yet')
                return
            selectedItems[0].copyToClipboard()

    def onCut(self):
        """Cutting..."""
        print("Cut")

    def onRemoveCustomColors(self):
        """Removing the previously set custom colors"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind in [
                        CellElement.OPENED_GROUP_BEGIN,
                        CellElement.COLLAPSED_GROUP, CellElement.EMPTY_GROUP
                ]:
                    item.groupBeginCMLRef.removeCustomColors(editor)
                    continue
                if item.isDocstring():
                    cmlComment = CMLVersion.find(
                        item.ref.docstring.leadingCMLComments, CMLcc)
                else:
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLcc)
                if cmlComment is not None:
                    cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def onRemoveReplacementText(self):
        """Removing replacement text"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                             CMLrt)
                if cmlComment is not None:
                    cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def areSelectedOfTypes(self, matchList):
        """Checks if the selected items belong to the match"""
        # match is a list of pairs [kind, subKind]
        #   None would mean 'match any'
        selectedItems = self.selectedItems()
        if selectedItems:
            for selectedItem in selectedItems:
                for kind, subKind in matchList:
                    match = True
                    if kind is not None:
                        if kind != selectedItem.kind:
                            match = False
                    if subKind is not None:
                        if subKind != selectedItem.subKind:
                            match = False
                    if match:
                        break
                else:
                    return False
            return True
        return False

    def isInSelected(self, matchList):
        """Checks if any if the match list items is in the selection"""
        # match is a list of pairs [kind, subKind]
        #   None would mean 'match any'
        for selectedItem in self.selectedItems():
            for kind, subKind in matchList:
                match = True
                if kind is not None:
                    if kind != selectedItem.kind:
                        match = False
                if subKind is not None:
                    if subKind != selectedItem.subKind:
                        match = False
                if match:
                    return True
        return False

    def isDocstringInSelection(self):
        """True if a docstring item in the selection"""
        for item in self.selectedItems():
            if item.isDocstring():
                return True
        return False

    def isCommentInSelection(self):
        """True if a comment item in the selection"""
        for item in self.selectedItems():
            if item.isComment():
                return True
        return False

    def countItemsWithCML(self, cmlType):
        """Counts items with have a certain type of a CML comment"""
        count = 0
        for item in self.selectedItems():
            if item.isComment():
                continue
            if item.isDocstring():
                # Side comments for docstrings? Nonesense! So they are ignored
                # even if they are collected
                if CMLVersion.find(item.ref.docstring.leadingCMLComments,
                                   cmlType) is not None:
                    count += 1
                continue

            if hasattr(item.ref, 'leadingCMLComments'):
                if CMLVersion.find(item.ref.leadingCMLComments,
                                   cmlType) is not None:
                    count += 1
                    continue
            if hasattr(item.ref, 'sideCMLComments'):
                if CMLVersion.find(item.ref.sideCMLComments,
                                   cmlType) is not None:
                    count += 1
        return count

    def countGroups(self):
        """Counts empty, close and open groups"""
        emptyCount = 0
        closeCount = 0
        openCount = 0
        for item in self.selectedItems():
            if item.kind == CellElement.EMPTY_GROUP:
                emptyCount += 1
            elif item.kind == CellElement.COLLAPSED_GROUP:
                closeCount += 1
            elif item.kind == CellElement.OPENED_GROUP_BEGIN:
                openCount += 1
        return emptyCount, closeCount, openCount

    def countGroupsWithCustomColors(self):
        """Counts the groups with any color defined"""
        emptyCount = 0
        closeCount = 0
        openCount = 0
        for item in self.selectedItems():
            if item.kind in [
                    CellElement.EMPTY_GROUP, CellElement.COLLAPSED_GROUP,
                    CellElement.OPENED_GROUP_BEGIN
            ]:
                if item.groupBeginCMLRef.bgColor is not None or \
                   item.groupBeginCMLRef.fgColor is not None or \
                   item.groupBeginCMLRef.border is not None:
                    if item.kind == CellElement.EMPTY_GROUP:
                        emptyCount += 1
                    elif item.kind == CellElement.COLLAPSED_GROUP:
                        closeCount += 1
                    else:
                        openCount += 1
        return emptyCount, closeCount, openCount

    def sortSelectedReverse(self):
        """Sorts the selected items in reverse order"""
        result = []
        for item in self.selectedItems():
            itemBegin = item.getAbsPosRange()[0]
            for index in range(len(result)):
                if itemBegin > result[index].getAbsPosRange()[0]:
                    result.insert(index, item)
                    break
            else:
                result.append(item)
        return result

    def sortSelected(self, selected):
        """Sorts the selected items in direct order"""
        result = []
        for item in selected:
            itemBegin = item.getAbsPosRange()[0]
            for index in range(len(result)):
                if itemBegin < result[index].getAbsPosRange()[0]:
                    result.insert(index, item)
                    break
            else:
                result.append(item)
        return result

    def __canBeGrouped(self):
        """True if the selected items can be grouped"""
        # Cannot import it at the top...
        from .flowuiwidget import SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT

        if Settings()['smartZoom'] not in [
                SMART_ZOOM_ALL, SMART_ZOOM_NO_CONTENT
        ]:
            return False
        if self.__areAllSelectedComments():
            return False
        if self.__areScopeDocstringOrCommentSelected():
            return False
        if self.__isModuleSelected():
            return False

        # Extend the selection with all the selected items comments
        selected = self.__extendSelectionForGrouping()

        if self.__areLoneCommentsSelected(selected):
            return False

        if self.__areIncompleteScopeSelected(selected):
            return False

        scopeCoveredRegions = self.__getSelectedScopeRegions(selected)

        # The __areIfFullySelected() also updates the regions with
        # fully selected if regions
        if not self.__areIfFullySelected(selected, scopeCoveredRegions):
            return False

        selected = self.sortSelected(selected)
        begin = selected[0].getAbsPosRange()[0]
        end = selected[-1].getAbsPosRange()[1]

        if not self.__isSelectionContinuous(selected, scopeCoveredRegions,
                                            begin, end):
            return False

        if self.__moreThanOneIfBranchSelected(selected, scopeCoveredRegions):
            return False
        return True

    def __areAllSelectedComments(self):
        """True if all selected items are comments"""
        for item in self.selectedItems():
            if not item.isComment():
                return False
        return True

    def __areScopeDocstringOrCommentSelected(self):
        for item in self.selectedItems():
            if item.scopedItem():
                if item.subKind in [
                        ScopeCellElement.SIDE_COMMENT,
                        ScopeCellElement.DOCSTRING
                ]:
                    return True
        return False

    def __isModuleSelected(self):
        """True if the whole module is selected"""
        for item in self.selectedItems():
            if item.kind == CellElement.FILE_SCOPE:
                return True
        return False

    def __areIncompleteScopeSelected(self, selected):
        """True if an incomplete scope selected"""
        for item in selected:
            if item.kind in [CellElement.FOR_SCOPE, CellElement.WHILE_SCOPE]:
                if item.ref.elsePart:
                    for relatedItem in self.findItemsForRef(item.ref.elsePart):
                        if relatedItem not in selected:
                            return True
            elif item.kind in [CellElement.TRY_SCOPE]:
                # It could be that the exception blocks are hidden, so there
                # will be exactly one more item instead of many and that item
                # will have a ref which matches the try scope.
                exceptPartCount = 0
                for exceptPart in item.ref.exceptParts:
                    for relatedItem in self.findItemsForRef(exceptPart):
                        exceptPartCount += 1
                        if relatedItem not in selected:
                            return True
                if exceptPartCount == 0:
                    # here: no except blocks on the diagram, they are collapsed
                    tryItems = self.findItemsForRef(item.ref)
                    for tryItem in tryItems:
                        if tryItem.kind == CellElement.EXCEPT_MINIMIZED:
                            if not tryItem.isSelected():
                                return True
                            break
                    else:
                        # The minimized except is not selected
                        return True
                if item.ref.elsePart:
                    for relatedItem in self.findItemsForRef(item.ref.elsePart):
                        if relatedItem not in selected:
                            return True
                if item.ref.finallyPart:
                    for relatedItem in self.findItemsForRef(
                            item.ref.finallyPart):
                        if relatedItem not in selected:
                            return True
            elif item.kind in [
                    CellElement.ELSE_SCOPE, CellElement.EXCEPT_SCOPE,
                    CellElement.FINALLY_SCOPE
            ]:
                for relatedItem in self.findItemsForRef(item.leaderRef):
                    if relatedItem not in selected:
                        return True
            elif item.kind == CellElement.EXCEPT_MINIMIZED:
                # here: no except blocks on the diagram, they are collapsed
                tryItems = self.findItemsForRef(item.ref)
                for tryItem in tryItems:
                    if tryItem.kind == CellElement.TRY_SCOPE:
                        if tryItem.subKind == ScopeCellElement.TOP_LEFT:
                            if not tryItem.isSelected():
                                return True
                            break
                else:
                    # The try is not selected
                    return True
        return False

    def __extendSelectionForGrouping(self):
        """Extends the selection with the leading and side comments"""
        boundComments = []
        selected = self.selectedItems()
        for item in selected:
            if not item.isComment() and not self.isOpenGroupItem(item):
                for relatedItem in self.findItemsForRef(item.ref):
                    if relatedItem not in selected:
                        boundComments.append(relatedItem)
        return selected + boundComments

    def __areLoneCommentsSelected(self, selected):
        """True if there are comments selected which have no main item selected"""
        for item in selected:
            if item.isComment():
                if item.kind in [
                        CellElement.SIDE_COMMENT, CellElement.LEADING_COMMENT,
                        CellElement.ABOVE_COMMENT
                ]:
                    for relatedItem in self.findItemsForRef(item.ref):
                        if relatedItem not in selected:
                            return True
        return False

    def __getLineRange(self, selected):
        first = selected[0]
        last = selected[-1]

        if first.kind == CellElement.OPENED_GROUP_BEGIN:
            firstLine = first.groupBeginCMLRef.ref.parts[0].beginLine
            pos = first.groupBeginCMLRef.ref.parts[0].beginPos
        else:
            firstLine = first.getLineRange()[0]
            pos = first.ref.beginPos

        if last.scopedItem():
            lastLine = last.ref.endLine
        elif last.kind == CellElement.OPENED_GROUP_BEGIN:
            lastLine = last.groupEndCMLRef.ref.parts[-1].endLine
        else:
            lastLine = last.getLineRange()[1]
        return firstLine, lastLine, pos

    def __getSelectedScopeRegions(self, selected):
        """Provides the regions of the selected scope items"""
        coveredRegions = []
        for item in selected:
            if item.scopedItem():
                if item.subKind in [ScopeCellElement.TOP_LEFT]:
                    if item.ref.leadingComment:
                        coveredRegions.append(
                            (item.ref.leadingComment.begin, item.ref.end))
                    else:
                        coveredRegions.append((item.ref.begin, item.ref.end))
            elif item.kind == CellElement.OPENED_GROUP_BEGIN:
                coveredRegions.append(item.getAbsPosRange())
        return coveredRegions

    def __areIfFullySelected(self, selected, regions):
        """Checks if selected IFs are fully selected"""
        for item in selected:
            if item.kind == CellElement.IF:
                ifBegin = item.ref.begin
                ifEnd = item.ref.end
                for item in self.items():
                    if item.isProxyItem():
                        continue
                    if item.scopedItem():
                        if item.subKind not in [
                                ScopeCellElement.TOP_LEFT,
                                ScopeCellElement.DOCSTRING,
                                ScopeCellElement.SIDE_COMMENT
                        ]:
                            continue
                    if item in selected:
                        continue
                    itemRange = item.getAbsPosRange()
                    if self.isInRegion(itemRange[0], itemRange[1], regions):
                        continue
                    if itemRange[0] > ifBegin and itemRange[0] < ifEnd:
                        return False
                    if itemRange[1] > ifBegin and itemRange[1] < ifEnd:
                        return False
                regions.append([ifBegin, ifEnd])
        return True

    @staticmethod
    def isInRegion(start, finish, regions):
        for region in regions:
            if start >= region[0] and finish <= region[1]:
                return True
        return False

    def __isSelectionContinuous(self, selected, regions, begin, end):
        """Checks if the selection is continuous"""
        for item in self.items():
            if item.isProxyItem():
                continue
            if item.scopedItem():
                if item.subKind not in [
                        ScopeCellElement.TOP_LEFT, ScopeCellElement.DOCSTRING,
                        ScopeCellElement.SIDE_COMMENT
                ]:
                    continue
            if item in selected:
                continue
            itemRange = item.getAbsPosRange()
            if self.isInRegion(itemRange[0], itemRange[1], regions):
                continue

            # It is important to keep < and > instead of <= and >=
            # This is because the scopes start with the first statement
            if itemRange[0] > begin and itemRange[0] < end:
                return False
            if itemRange[1] > begin and itemRange[1] < end:
                return False
        return True

    def __moreThanOneIfBranchSelected(self, selected, regions):
        """Checks that the continuous selected items belong to more than one
        not selected IF statements
        """
        ifRef = None
        for item in selected:
            if item.kind != CellElement.IF:
                itemRange = item.getAbsPosRange()
                if item.kind != CellElement.OPENED_GROUP_BEGIN:
                    if self.isInRegion(itemRange[0], itemRange[1], regions):
                        # Here: an item is in a selected scope item, in a selected
                        #       open group or in a fully selected if
                        continue
                # Test if an item belongs to an if statement branch
                if item.kind in [
                        CellElement.OPENED_GROUP_BEGIN,
                        CellElement.EMPTY_GROUP, CellElement.COLLAPSED_GROUP
                ]:
                    branchId = item.groupBeginCMLRef.ref.getParentIfID()
                else:
                    branchId = item.ref.getParentIfID()
                if branchId is not None:
                    if ifRef is None:
                        ifRef = branchId
                    else:
                        if branchId != ifRef:
                            # Selected items belong to more than one branch
                            return True
        return False
Пример #8
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)
Пример #9
0
class CFSceneContextMenuMixin:
    """Encapsulates the context menu handling"""
    def __init__(self):
        self.menu = None
        self.individualMenus = {}

        # Scene menu preparation
        self.sceneMenu = QMenu()
        self.sceneMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...',
                                 self.parent().onSaveAsSVG)
        self.sceneMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...',
                                 self.parent().onSaveAsPDF)
        self.sceneMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...',
                                 self.parent().onSaveAsPNG)
        self.sceneMenu.addSeparator()
        self.sceneMenu.addAction(getIcon('copymenu.png'), 'Copy to clipboard',
                                 self.parent().copyToClipboard)

        # Common menu for all the individually selected items
        self.commonMenu = QMenu()
        self.__ccAction = self.commonMenu.addAction(
            getIcon("customcolors.png"), "Custom colors...",
            self.onCustomColors)
        self.__rtAction = self.commonMenu.addAction(
            getIcon("replacetitle.png"), "Replace text...", self.onReplaceText)
        self.commonMenu.addSeparator()
        self.__removeCCAction = self.commonMenu.addAction(
            getIcon('trash.png'), 'Remove custom colors',
            self.onRemoveCustomColors)
        self.__removeRTAction = self.commonMenu.addAction(
            getIcon('trash.png'), 'Remove replacement text',
            self.onRemoveReplacementText)
        self.commonMenu.addSeparator()
        self.__cutAction = self.commonMenu.addAction(getIcon("cutmenu.png"),
                                                     "Cut", self.onCut)
        self.__copyAction = self.commonMenu.addAction(getIcon("copymenu.png"),
                                                      "Copy", self.onCopy)
        self.commonMenu.addSeparator()
        self.commonMenu.addAction(getIcon("trash.png"), "Delete",
                                  self.onDelete)

        # Individual items specific menu: begin
        ifContextMenu = QMenu()
        ifContextMenu.addAction(getIcon("switchbranches.png"),
                                "Switch branch layout", self.onSwitchIfBranch)

        self.individualMenus[IfCell] = ifContextMenu
        # Individual items specific menu: end

        # Menu for a group of selected items
        self.groupMenu = QMenu()
        self.__groupAction = self.groupMenu.addAction(getIcon("cfgroup.png"),
                                                      "Group...", self.onGroup)

    def onContextMenu(self, event):
        """Triggered when a context menu should be shown"""
        selectedItems = self.selectedItems()
        selectionCount = len(selectedItems)
        if selectionCount == 0:
            self.sceneMenu.popup(event.screenPos())
            return

        if selectionCount == 1:
            self.__buildIndividualMenu(selectedItems[0])
        else:
            self.__buildGroupMenu(selectedItems)
        self.menu.popup(event.screenPos())

    def __buildIndividualMenu(self, item):
        """Builds a context menu for the given item"""
        self.menu = QMenu()
        if type(item) in self.individualMenus:
            individualPart = self.individualMenus[type(item)]
            self.menu.addActions(individualPart.actions())
            self.menu.addSeparator()
        self.menu.addActions(self.commonMenu.actions())

        # Note: if certain items need to be disabled then it should be done
        #       here
        self.__disableMenuItems()

    def __buildGroupMenu(self, items):
        """Builds a context menu for the group of items"""
        self.menu = QMenu()
        if type(items[0]) in self.individualMenus:
            if self.areSelectedOfTypes([[items[0].kind, items[0].subKind]]):
                individualPart = self.individualMenus[type(items[0])]
                self.menu.addActions(individualPart.actions())
                self.menu.addSeparator()
        self.menu.addActions(self.commonMenu.actions())
        self.menu.addSeparator()
        self.menu.addActions(self.groupMenu.actions())

        # Note: if certain items need to be disabled then it should be done
        #       here
        self.__groupAction.setEnabled(False)
        self.__disableMenuItems()

    def __disableMenuItems(self):
        """Disables the common menu items as needed"""
        hasComment = self.isCommentInSelection()
        hasDocstring = self.isDocstringInSelection()
        hasMinimizedExcepts = self.isInSelected([(CellElement.EXCEPT_MINIMIZED,
                                                  None)])
        count = len(self.selectedItems())

        self.__ccAction.setEnabled(not hasComment and not hasMinimizedExcepts)
        self.__rtAction.setEnabled(not hasComment and not hasDocstring
                                   and not hasMinimizedExcepts)
        self.__removeCCAction.setEnabled(
            self.countItemsWithCML(CMLcc) == count)
        self.__removeRTAction.setEnabled(
            self.countItemsWithCML(CMLrt) == count)
        self.__cutAction.setEnabled(count == 1)
        self.__copyAction.setEnabled(count == 1)

    def __actionPrerequisites(self):
        """True if an editor related action can be done"""
        selectedItems = self.selectedItems()
        if not selectedItems:
            return False
        editor = selectedItems[0].getEditor()
        if editor is None:
            return False
        return True

    def onSwitchIfBranch(self):
        """If primitive should switch the branches"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        # The selected items need to be sorted in the reverse line no oreder
        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.kind == CellElement.IF:
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLsw)
                    if cmlComment is None:
                        # Did not exist, so needs to be generated
                        line = CMLsw.generate(item.ref.body.beginPos)
                        lineNo = item.getFirstLine()
                        editor.insertLines(line, lineNo)
                    else:
                        # Existed, so it just needs to be deleted
                        cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def onCustomColors(self):
        """Custom background and foreground colors"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        bgcolor, fgcolor, bordercolor = self.selectedItems()[0].getColors()
        hasDocstring = self.isDocstringInSelection()
        dlg = CustomColorsDialog(bgcolor, fgcolor,
                                 None if hasDocstring else bordercolor,
                                 self.parent())
        if dlg.exec_():
            bgcolor = dlg.backgroundColor()
            fgcolor = dlg.foregroundColor()
            bordercolor = dlg.borderColor()

            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    if item.isDocstring():
                        cmlComment = CMLVersion.find(
                            item.ref.docstring.leadingCMLComments, CMLcc)
                    else:
                        cmlComment = CMLVersion.find(
                            item.ref.leadingCMLComments, CMLcc)
                    if cmlComment is not None:
                        # Existed, so remove the old one first
                        lineNo = cmlComment.ref.beginLine
                        cmlComment.removeFromText(editor)
                    else:
                        lineNo = item.getFirstLine()

                    pos = item.ref.body.beginPos
                    if item.isDocstring():
                        pos = item.ref.docstring.beginPos
                    line = CMLcc.generate(bgcolor, fgcolor, bordercolor, pos)
                    editor.insertLines(line, lineNo)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onReplaceText(self):
        """Replace the code with a title"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        dlg = ReplaceTextDialog(self.parent())

        # If it was one item selection and there was a previous text then
        # set it for editing
        if len(self.selectedItems()) == 1:
            cmlComment = CMLVersion.find(
                self.selectedItems()[0].ref.leadingCMLComments, CMLrt)
            if cmlComment is not None:
                dlg.setText(cmlComment.getText())

        if dlg.exec_():
            replacementText = dlg.text()
            editor = self.selectedItems()[0].getEditor()
            with editor:
                for item in self.sortSelectedReverse():
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLrt)
                    if cmlComment is not None:
                        # Existed, so remove the old one first
                        lineNo = cmlComment.ref.beginLine
                        cmlComment.removeFromText(editor)
                    else:
                        lineNo = item.getFirstLine()

                    line = CMLrt.generate(replacementText,
                                          item.ref.body.beginPos)
                    editor.insertLines(line, lineNo)
            QApplication.processEvents()
            self.parent().redrawNow()
            self.restoreSelectionByID(selection)

    def onDelete(self):
        """Delete the item"""
        print("Delete")

    def onGroup(self):
        """Groups items into a single one"""
        print("Group")

    def onCopy(self):
        """Copying..."""
        print("Copy")

    def onCut(self):
        """Cutting..."""
        print("Cut")

    def onRemoveCustomColors(self):
        """Removing the previously set custom colors"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                if item.isDocstring():
                    cmlComment = CMLVersion.find(
                        item.ref.docstring.leadingCMLComments, CMLcc)
                else:
                    cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                                 CMLcc)
                if cmlComment is not None:
                    cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def onRemoveReplacementText(self):
        """Removing replacement text"""
        if not self.__actionPrerequisites():
            return

        # Memorize the current selection
        selection = self.serializeSelection()

        editor = self.selectedItems()[0].getEditor()
        with editor:
            for item in self.sortSelectedReverse():
                cmlComment = CMLVersion.find(item.ref.leadingCMLComments,
                                             CMLrt)
                if cmlComment is not None:
                    cmlComment.removeFromText(editor)
        QApplication.processEvents()
        self.parent().redrawNow()
        self.restoreSelectionByID(selection)

    def areSelectedOfTypes(self, matchList):
        """Checks if the selected items belong to the match"""
        # match is a list of pairs [kind, subKind]
        #   None would mean 'match any'
        selectedItems = self.selectedItems()
        if selectedItems:
            for selectedItem in selectedItems:
                for kind, subKind in matchList:
                    match = True
                    if kind is not None:
                        if kind != selectedItem.kind:
                            match = False
                    if subKind is not None:
                        if subKind != selectedItem.subKind:
                            match = False
                    if match:
                        break
                else:
                    return False
            return True
        return False

    def isInSelected(self, matchList):
        """Checks if any if the match list items is in the selection"""
        # match is a list of pairs [kind, subKind]
        #   None would mean 'match any'
        for selectedItem in self.selectedItems():
            for kind, subKind in matchList:
                match = True
                if kind is not None:
                    if kind != selectedItem.kind:
                        match = False
                if subKind is not None:
                    if subKind != selectedItem.subKind:
                        match = False
                if match:
                    return True
        return False

    def isDocstringInSelection(self):
        """True if a docstring item in the selection"""
        for item in self.selectedItems():
            if item.isDocstring():
                return True
        return False

    def isCommentInSelection(self):
        """True if a comment item in the selection"""
        for item in self.selectedItems():
            if item.isComment():
                return True
        return False

    def countItemsWithCML(self, cmlType):
        """Counts items with have a certain type of a CML comment"""
        count = 0
        for item in self.selectedItems():
            if item.isComment():
                continue
            if item.isDocstring():
                # Side comments for docstrings? Nonesense! So they are ignored
                # even if they are collected
                if CMLVersion.find(item.ref.docstring.leadingCMLComments,
                                   cmlType) is not None:
                    count += 1
                continue

            if hasattr(item.ref, 'leadingCMLComments'):
                if CMLVersion.find(item.ref.leadingCMLComments,
                                   cmlType) is not None:
                    count += 1
                    continue
            if hasattr(item.ref, 'sideCMLComments'):
                if CMLVersion.find(item.ref.sideCMLComments,
                                   cmlType) is not None:
                    count += 1
        return count

    def sortSelectedReverse(self):
        """Sorts the selected items in reverse order"""
        result = []
        for item in self.selectedItems():
            itemBegin = item.getAbsPosRange()[0]
            for index in range(len(result)):
                if itemBegin > result[index].getAbsPosRange()[0]:
                    result.insert(index, item)
                    break
            else:
                result.append(item)
        return result
Пример #10
0
class WatchPointView(QTreeView):
    """Watch expression viewer widget"""

    sigSelectionChanged = pyqtSignal(QModelIndex)

    def __init__(self, parent, wpointsModel):
        QTreeView.__init__(self, parent)

        self.__model = None
        self.setModel(wpointsModel)

        self.setItemsExpandable(False)
        self.setRootIsDecorated(False)
        self.setAlternatingRowColors(True)
        self.setUniformRowHeights(True)
        self.setSelectionMode(QAbstractItemView.SingleSelection)
        self.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.setItemDelegate(NoOutlineHeightDelegate(4))

        self.setContextMenuPolicy(Qt.CustomContextMenu)
        self.customContextMenuRequested.connect(self.__showContextMenu)
        self.doubleClicked.connect(self.__doubleClicked)

        self.__createPopupMenus()

    def setModel(self, model):
        """Sets the watch expression model"""
        self.__model = model

        self.sortingModel = QSortFilterProxyModel()
        self.sortingModel.setSourceModel(self.__model)
        QTreeView.setModel(self, self.sortingModel)

        header = self.header()
        header.setSortIndicator(0, Qt.AscendingOrder)
        header.setSortIndicatorShown(True)
        header.setSectionsClickable(True)

        self.setSortingEnabled(True)
        self.__layoutDisplay()

    def __layoutDisplay(self):
        """Performs the layout operation"""
        self.__resizeColumns()
        self.__resort()

    def __resizeColumns(self):
        """Resizes the view when items get added, edited or deleted"""
        self.header().resizeSections(QHeaderView.ResizeToContents)
        self.header().setStretchLastSection(True)

    def __resort(self):
        """Resorts the tree"""
        self.model().sort(self.header().sortIndicatorSection(),
                          self.header().sortIndicatorOrder())

    def __toSourceIndex(self, index):
        """Converts an index to a source index"""
        return self.sortingModel.mapToSource(index)

    def __fromSourceIndex(self, sindex):
        """Converts a source index to an index"""
        return self.sortingModel.mapFromSource(sindex)

    def __setRowSelected(self, index, selected=True):
        """Selects a row"""
        if not index.isValid():
            return

        if selected:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            flags = QItemSelectionModel.SelectionFlags(
                QItemSelectionModel.Deselect | QItemSelectionModel.Rows)
        self.selectionModel().select(index, flags)

    def __createPopupMenus(self):
        """Generates the popup menus"""
        self.menu = QMenu()
        self.menu.addAction("Add", self.__addWatchPoint)
        self.menu.addAction("Edit...", self.__editWatchPoint)
        self.menu.addSeparator()
        self.menu.addAction("Enable", self.__enableWatchPoint)
        self.menu.addAction("Enable all", self.__enableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction("Disable", self.__disableWatchPoint)
        self.menu.addAction("Disable all", self.__disableAllWatchPoints)
        self.menu.addSeparator()
        self.menu.addAction("Delete", self.__deleteWatchPoint)
        self.menu.addAction("Delete all", self.__deleteAllWatchPoints)

        self.backMenuActions = {}
        self.backMenu = QMenu()
        self.backMenu.addAction("Add", self.__addWatchPoint)
        self.backMenuActions["EnableAll"] = \
            self.backMenu.addAction("Enable all", self.__enableAllWatchPoints)
        self.backMenuActions["DisableAll"] = \
            self.backMenu.addAction("Disable all",
                                    self.__disableAllWatchPoints)
        self.backMenuActions["DeleteAll"] = \
            self.backMenu.addAction("Delete all",
                                    self.__deleteAllWatchPoints)
        self.backMenu.aboutToShow.connect(self.__showBackMenu)

        self.multiMenu = QMenu()
        self.multiMenu.addAction("Add", self.__addWatchPoint)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction("Enable selected",
                                 self.__enableSelectedWatchPoints)
        self.multiMenu.addAction("Enable all", self.__enableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction("Disable selected",
                                 self.__disableSelectedWatchPoints)
        self.multiMenu.addAction("Disable all", self.__disableAllWatchPoints)
        self.multiMenu.addSeparator()
        self.multiMenu.addAction("Delete selected",
                                 self.__deleteSelectedWatchPoints)
        self.multiMenu.addAction("Delete all", self.__deleteAllWatchPoints)

    def __showContextMenu(self, coord):
        """Show the context menu"""
        cnt = self.__getSelectedItemsCount()
        if cnt <= 1:
            index = self.indexAt(coord)
            if index.isValid():
                cnt = 1
                self.__setRowSelected(index)
        coord = self.mapToGlobal(coord)
        if cnt > 1:
            self.multiMenu.popup(coord)
        elif cnt == 1:
            self.menu.popup(coord)
        else:
            self.backMenu.popup(coord)

    def __findDuplicates(self,
                         cond,
                         special,
                         showMessage=False,
                         index=QModelIndex()):
        """Checks if an entry already exists"""
        idx = self.__model.getWatchPointIndex(cond, special)
        duplicate = idx.isValid(
        ) and idx.internalPointer() != index.internalPointer()
        #        if showMessage and duplicate:
        #            if not special:
        #                msg = """<p>A watch expression '<b>%1</b>'"""
        #                                  """ already exists.</p>""".arg(Utilities.html_encode(unicode(cond)))
        #            else:
        #                msg = self.trUtf8("""<p>A watch expression '<b>%1</b>'"""
        #                                  """ for the variable <b>%2</b> already exists.</p>""")\
        #                        .arg(special)\
        #                        .arg(Utilities.html_encode(unicode(cond)))
        #            KQMessageBox.warning(None,
        #                "Watch expression already exists", msg)
        return duplicate

    def __clearSelection(self):
        """Clears the selection"""
        for index in self.selectedIndexes():
            self.__setRowSelected(index, False)

    def __addWatchPoint(self):
        """Adds watch expression via a context menu entry"""
        #        dlg = EditWatchpointDialog( ( "", False, True, 0, "" ), self )
        #        if dlg.exec_() == QDialog.Accepted:
        #            cond, temp, enabled, ignorecount, special = dlg.getData()
        #            if not self.__findDuplicates(cond, special, True):
        #                self.__model.addWatchPoint(cond, special, (temp, enabled, ignorecount))
        #                self.__resizeColumns()
        #                self.__resort()
        return

    def __doubleClicked(self, index):
        """Handles the double clicked signal"""
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __editWatchPoint(self):
        """Handles the edit watch expression context menu entry"""
        index = self.currentIndex()
        if index.isValid():
            self.__doEditWatchPoint(index)

    def __doEditWatchPoint(self, index):
        """Edits a watch expression"""
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            wp = self.__model.getWatchPointByIndex(sindex)
            if not wp:
                return

            cond, special, temp, enabled, count = wp[:5]


#            dlg = EditWatchpointDialog(
#                (cond, temp, enabled, count, special), self)
#            if dlg.exec_() == QDialog.Accepted:
#                cond, temp, enabled, count, special = dlg.getData()
#                if not self.__findDuplicates(cond, special, True, sindex):
#                    self.__model.setWatchPointByIndex(sindex,
#                        unicode(cond), unicode(special), (temp, enabled, count))
#                    self.__resizeColumns()
#                    self.__resort()
        return

    def __setWpEnabled(self, index, enabled):
        """Sets the enabled status of a watch expression"""
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.setWatchPointEnabledByIndex(sindex, enabled)

    def __enableWatchPoint(self):
        """Handles the enable watch expression context menu entry"""
        index = self.currentIndex()
        self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __enableAllWatchPoints(self):
        """Handles the enable all watch expressions context menu entry"""
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, True)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __enableSelectedWatchPoints(self):
        """Handles the enable selected watch expressions context menu entry"""
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, True)
        self.__resizeColumns()
        self.__resort()

    def __disableWatchPoint(self):
        """Handles the disable watch expression context menu entry"""
        index = self.currentIndex()
        self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __disableAllWatchPoints(self):
        """Handles the disable all watch expressions context menu entry"""
        index = self.model().index(0, 0)
        while index.isValid():
            self.__setWpEnabled(index, False)
            index = self.indexBelow(index)
        self.__resizeColumns()
        self.__resort()

    def __disableSelectedWatchPoints(self):
        """Handles the disable selected watch expressions context menu entry"""
        for index in self.selectedIndexes():
            if index.column() == 0:
                self.__setWpEnabled(index, False)
        self.__resizeColumns()
        self.__resort()

    def __deleteWatchPoint(self):
        """Handles the delete watch expression context menu entry"""
        index = self.currentIndex()
        sindex = self.__toSourceIndex(index)
        if sindex.isValid():
            self.__model.deleteWatchPointByIndex(sindex)

    def __deleteAllWatchPoints(self):
        """Handles the delete all watch expressions context menu entry"""
        self.__model.deleteAll()

    def __deleteSelectedWatchPoints(self):
        """Handles the delete selected watch expressions context menu entry"""
        idxList = []
        for index in self.selectedIndexes():
            sindex = self.__toSourceIndex(index)
            if sindex.isValid() and index.column() == 0:
                lastrow = index.row()
                idxList.append(sindex)
        self.__model.deleteWatchPoints(idxList)

    def __showBackMenu(self):
        """Handles the aboutToShow signal of the background menu"""
        if self.model().rowCount() == 0:
            self.backMenuActions["EnableAll"].setEnabled(False)
            self.backMenuActions["DisableAll"].setEnabled(False)
            self.backMenuActions["DeleteAll"].setEnabled(False)
        else:
            self.backMenuActions["EnableAll"].setEnabled(True)
            self.backMenuActions["DisableAll"].setEnabled(True)
            self.backMenuActions["DeleteAll"].setEnabled(True)

    def __getSelectedItemsCount(self):
        """Provides the count of items selected"""
        count = len(self.selectedIndexes()) / (self.__model.columnCount() - 1)
        # column count is 1 greater than selectable
        return count

    def selectionChanged(self, selected, deselected):
        """The slot is called when the selection has changed"""
        if selected.indexes():
            self.sigSelectionChanged.emit(selected.indexes()[0])
        else:
            self.sigSelectionChanged.emit(QModelIndex())
        QTreeView.selectionChanged(self, selected, deselected)
class RedirectedIOConsole(QutepartWrapper):
    """Widget which implements the redirected IO console"""

    sigUserInput = pyqtSignal(str)

    MODE_OUTPUT = 0
    MODE_INPUT = 1

    def __init__(self, parent):
        QutepartWrapper.__init__(self, parent)

        self.setAttribute(Qt.WA_KeyCompression)

        self.mode = self.MODE_OUTPUT
        self.lastOutputPos = None
        self.inputEcho = True
        self.inputBuffer = ""
        self.__messages = IOConsoleMessages()

        self.__initGeneralSettings()
        self.__initMargins()
        self._initContextMenu()
        self.onTextZoomChanged()

        self.__hotKeys = {}
        self.__initHotKeys()
        self.installEventFilter(self)

        self.cursorPositionChanged.connect(self.setCursorStyle)

    def __initHotKeys(self):
        """Initializes a map for the hot keys event filter"""
        self.autoIndentLineAction.setShortcut('Ctrl+Shift+I')
        self.invokeCompletionAction.setEnabled(False)
        self.__hotKeys = {
            CTRL_SHIFT: {
                Qt.Key_C: self.onCtrlShiftC
            },
            SHIFT: {
                Qt.Key_End: self.onShiftEnd,
                Qt.Key_Home: self.onShiftHome,
                Qt.Key_Insert: self.onPasteText,
                Qt.Key_Delete: self.onShiftDel
            },
            CTRL: {
                Qt.Key_V: self.onPasteText,
                Qt.Key_X: self.onShiftDel,
                Qt.Key_C: self.onCtrlC,
                Qt.Key_Insert: self.onCtrlC,
                Qt.Key_Minus: Settings().onTextZoomOut,
                Qt.Key_Equal: Settings().onTextZoomIn,
                Qt.Key_0: Settings().onTextZoomReset,
                Qt.Key_Home: self.onFirstChar,
                Qt.Key_End: self.onLastChar
            },
            CTRL_KEYPAD: {
                Qt.Key_Minus: Settings().onTextZoomOut,
                Qt.Key_Plus: Settings().onTextZoomIn,
                Qt.Key_0: Settings().onTextZoomReset
            },
            NO_MODIFIER: {
                Qt.Key_Home: self.onHome,
                Qt.Key_End: self.moveToLineEnd
            }
        }

    def eventFilter(self, _, event):
        """Event filter to catch shortcuts on UBUNTU"""
        if event.type() == QEvent.KeyPress:
            key = event.key()
            modifiers = int(event.modifiers())
            try:
                if modifiers in self.__hotKeys:
                    if key in self.__hotKeys[modifiers]:
                        self.__hotKeys[modifiers][key]()
                        return True

                if modifiers == NO_MODIFIER:
                    if key in [Qt.Key_Delete, Qt.Key_Backspace]:
                        if not self.__isCutDelAvailable():
                            return True
            except Exception as exc:
                logging.warning(str(exc))
        return False

    def keyPressEvent(self, event):
        """Triggered when a key is pressed"""
        key = event.key()
        if key == Qt.Key_Escape:
            self.clearSearchIndicators()
            return

        if self.mode == self.MODE_OUTPUT:
            if key in [
                    Qt.Key_Left, Qt.Key_Up, Qt.Key_Right, Qt.Key_Down,
                    Qt.Key_PageUp, Qt.Key_PageDown
            ]:
                QutepartWrapper.keyPressEvent(self, event)
            return

        # It is an input mode
        txt = event.text()
        if len(txt) and txt >= ' ':
            # Printable character
            if self.absCursorPosition < self.lastOutputPos:
                # Out of the input zone
                return

            if self.inputEcho:
                QutepartWrapper.keyPressEvent(self, event)
            else:
                self.inputBuffer += txt
            return

        # Non-printable character or some other key
        if key == Qt.Key_Enter or key == Qt.Key_Return:
            userInput = self.__getUserInput()
            self.switchMode(self.MODE_OUTPUT)
            timestampLine, _ = self.getEndPosition()
            self.append('\n')
            self.clearUndoRedoHistory()
            line, pos = self.getEndPosition()
            self.cursorPosition = line, pos
            self.ensureLineOnScreen(line)
            msg = IOConsoleMsg(IOConsoleMsg.STDIN_MESSAGE, userInput + '\n')
            self.__messages.append(msg)

            # margin data
            timestamp = msg.getTimestamp()
            margin = self.getMargin('cdm_redirected_io_margin')
            margin.addData(timestampLine + 1, timestamp, timestamp,
                           IOConsoleMsg.STDIN_MESSAGE)

            self.sigUserInput.emit(userInput)
            return

        if key == Qt.Key_Backspace:
            if self.absCursorPosition == self.lastOutputPos:
                if not self.inputEcho:
                    self.inputBuffer = self.inputBuffer[:-1]
                return

        QutepartWrapper.keyPressEvent(self, event)

    def mouseDoubleClickEvent(self, event):
        """Disable search highlight on double click"""
        Qutepart.mouseDoubleClickEvent(self, event)

    def onPasteText(self):
        """Triggered when insert is requested"""
        if self.mode == self.MODE_OUTPUT:
            return
        if self.absCursorPosition < self.lastOutputPos:
            return

        # Check what is in the buffer
        text = QApplication.clipboard().text()
        if '\n' in text or '\r' in text:
            return

        if not self.inputEcho:
            self.inputBuffer += text
            return

        self.paste()

    def __getUserInput(self):
        """Provides the collected user input"""
        if self.mode != self.MODE_INPUT:
            return ''
        if self.inputEcho:
            _, endPos = self.getEndPosition()
            _, beginPos = self.mapToLineCol(self.lastOutputPos)
            return self.lines[-1][beginPos:endPos]
        value = self.inputBuffer
        self.inputBuffer = ""
        return value

    def __initGeneralSettings(self):
        """Sets some generic look and feel"""
        skin = GlobalData().skin

        self.updateSettings()

        self.setPaper(skin['ioconsolePaper'])
        self.setColor(skin['ioconsoleColor'])

        self.currentLineColor = None
        self.lineLengthEdge = None
        self.setCursorStyle()

    def updateSettings(self):
        """Updates the IO console settings"""
        if Settings()['ioconsolelinewrap']:
            self.setWordWrapMode(QTextOption.WrapAnywhere)
        else:
            self.setWordWrapMode(QTextOption.NoWrap)

        self.drawAnyWhitespace = Settings()['ioconsoleshowspaces']
        self.drawIncorrectIndentation = Settings()['ioconsoleshowspaces']

    def setCursorStyle(self):
        """Sets the cursor style depending on the mode and the cursor pos"""
        if self.mode == self.MODE_OUTPUT:
            if self.cursorWidth() != 1:
                self.setCursorWidth(1)
        else:
            if self.absCursorPosition >= self.lastOutputPos:
                if self.cursorWidth() == 1:
                    fontMetrics = QFontMetrics(self.font(), self)
                    self.setCursorWidth(fontMetrics.width('W'))
                    self.update()
            else:
                if self.cursorWidth() != 1:
                    self.setCursorWidth(1)
                    self.update()

    def switchMode(self, newMode):
        """Switches between input/output mode"""
        self.mode = newMode
        if self.mode == self.MODE_OUTPUT:
            self.lastOutputPos = None
            self.inputEcho = True
            self.inputBuffer = ""
        else:
            line, pos = self.getEndPosition()
            self.cursorPosition = line, pos
            self.lastOutputPos = self.absCursorPosition
            self.ensureLineOnScreen(line)
        self.setCursorStyle()

    def __initMargins(self):
        """Initializes the IO console margins"""
        # The supported margins: timestamp
        self.addMargin(CDMRedirectedIOMargin(self))

    def _initContextMenu(self):
        """Called to initialize a context menu"""
        self._menu = QMenu(self)
        self.__menuUndo = self._menu.addAction(getIcon('undo.png'), '&Undo',
                                               self.onUndo, "Ctrl+Z")
        self.__menuRedo = self._menu.addAction(getIcon('redo.png'), '&Redo',
                                               self.onRedo, "Ctrl+Y")
        self._menu.addSeparator()
        self.__menuCut = self._menu.addAction(getIcon('cutmenu.png'), 'Cu&t',
                                              self.onShiftDel, "Ctrl+X")
        self.__menuCopy = self._menu.addAction(getIcon('copymenu.png'),
                                               '&Copy', self.onCtrlC, "Ctrl+C")
        self.__menucopyTimestamp = self._menu.addAction(
            getIcon('copymenu.png'), '&Copy all with timestamps',
            self.onCtrlShiftC, "Ctrl+Shift+C")
        self.__menuPaste = self._menu.addAction(getIcon('pastemenu.png'),
                                                '&Paste', self.onPasteText,
                                                "Ctrl+V")
        self.__menuSelectAll = self._menu.addAction(
            getIcon('selectallmenu.png'), 'Select &all', self.selectAll,
            "Ctrl+A")
        self._menu.addSeparator()
        self.__menuOpenAsFile = self._menu.addAction(getIcon('filemenu.png'),
                                                     'O&pen as file',
                                                     self.openAsFile)
        self.__menuDownloadAndShow = self._menu.addAction(
            getIcon('filemenu.png'), 'Do&wnload and show',
            self.downloadAndShow)
        self.__menuOpenInBrowser = self._menu.addAction(
            getIcon('homepagemenu.png'), 'Open in browser', self.openInBrowser)
        self._menu.addSeparator()

        self._menu.aboutToShow.connect(self._contextMenuAboutToShow)
        self._menu.aboutToHide.connect(self._contextMenuAboutToHide)

    def contextMenuEvent(self, event):
        """Called just before showing a context menu"""
        event.accept()
        self._menu.popup(event.globalPos())

    def _contextMenuAboutToShow(self):
        """IO Console context menu is about to show"""
        self.__menuUndo.setEnabled(self.document().isUndoAvailable())
        self.__menuRedo.setEnabled(self.document().isRedoAvailable())

        pasteText = QApplication.clipboard().text()
        pasteEnable = pasteText != "" and \
                      '\n' not in pasteText and \
                      '\r' not in pasteText and \
                      self.mode != self.MODE_OUTPUT
        if pasteEnable:
            if self.absCursorPosition < self.lastOutputPos:
                pasteEnable = False

        # Need to make decision about menu items for modifying the input
        self.__menuCut.setEnabled(self.__isCutDelAvailable())
        self.__menuCopy.setEnabled(self.__messages.size > 0)
        self.__menucopyTimestamp.setEnabled(self.__messages.size > 0)
        self.__menuPaste.setEnabled(pasteEnable)
        self.__menuSelectAll.setEnabled(self.__messages.size > 0)

        self.__menuOpenAsFile.setEnabled(self.openAsFileAvailable())
        self.__menuDownloadAndShow.setEnabled(self.downloadAndShowAvailable())
        self.__menuOpenInBrowser.setEnabled(self.downloadAndShowAvailable())

    def _contextMenuAboutToHide(self):
        """IO console context menu is about to hide"""
        self.__menuUndo.setEnabled(True)
        self.__menuRedo.setEnabled(True)
        self.__menuCut.setEnabled(True)
        self.__menuCopy.setEnabled(True)
        self.__menucopyTimestamp.setEnabled(True)
        self.__menuPaste.setEnabled(True)
        self.__menuSelectAll.setEnabled(True)
        self.__menuOpenAsFile.setEnabled(True)
        self.__menuDownloadAndShow.setEnabled(True)
        self.__menuOpenInBrowser.setEnabled(True)

    def __isCutDelAvailable(self):
        """Returns True if cutting or deletion is possible"""
        if self.mode == self.MODE_OUTPUT:
            return False
        if self.selectedText:
            startPosition, cursorPosition = self.absSelectedPosition
            minPos = min(startPosition, cursorPosition)
            return minPos >= self.lastOutputPos
        return self.absCursorPosition > self.lastOutputPos

    def onShiftDel(self):
        """Deletes the selected text"""
        if self.selectedText:
            if self.__isCutDelAvailable():
                self.cut()
            return True
        return True

    def onUndo(self):
        """undo implementation"""
        if self.document().isUndoAvailable():
            self.undo()

    def onRedo(self):
        """redo implementation"""
        if self.document().isRedoAvailable():
            self.redo()

    def onCtrlShiftC(self):
        """Copy all with timestamps"""
        QApplication.clipboard().setText(
            self.__messages.renderWithTimestamps())

    def appendIDEMessage(self, text):
        """Appends an IDE message"""
        msg = IOConsoleMsg(IOConsoleMsg.IDE_MESSAGE, text)
        self.__appendMessage(msg)
        return msg

    def appendStdoutMessage(self, text):
        """Appends an stdout message"""
        msg = IOConsoleMsg(IOConsoleMsg.STDOUT_MESSAGE, text)
        self.__appendMessage(msg)
        return msg

    def appendStderrMessage(self, text):
        """Appends an stderr message"""
        msg = IOConsoleMsg(IOConsoleMsg.STDERR_MESSAGE, text)
        self.__appendMessage(msg)
        return msg

    def __appendMessage(self, message):
        """Appends a new message to the console"""
        if not self.__messages.append(message):
            # There was no trimming of the message list
            self.__renderMessage(message)
        else:
            # Some messages were stripped
            self.renderContent()

    def renderContent(self):
        """Regenerates the viewer content"""
        self.clear()
        self.getMargin('cdm_redirected_io_margin').clear()
        for msg in self.__messages.msgs:
            self.__renderMessage(msg)

    def __renderMessage(self, msg):
        """Adds a single message"""
        margin = self.getMargin('cdm_redirected_io_margin')
        timestamp = msg.getTimestamp()
        if msg.msgType == IOConsoleMsg.IDE_MESSAGE:
            line, pos = self.getEndPosition()

            txt = msg.msgText
            startMarkLine = line
            if pos != 0:
                txt = '\n' + txt
                startMarkLine += 1

            self.append(txt)

            line, _ = self.getEndPosition()
            for lineNo in range(startMarkLine, line + 1):
                margin.addData(lineNo + 1, timestamp, timestamp,
                               IOConsoleMsg.IDE_MESSAGE)
        else:
            line, pos = self.getEndPosition()
            txt = msg.msgText

            startTimestampLine = line
            if pos != 0:
                lastMsgType = margin.getLineMessageType(line + 1)
                if lastMsgType == IOConsoleMsg.IDE_MESSAGE:
                    txt = '\n' + txt
                    startTimestampLine = line + 1

            self.append(txt)
            endTimestampLine, pos = self.getEndPosition()
            if pos == 0:
                endTimestampLine -= 1

            for lineNo in range(startTimestampLine, endTimestampLine + 1):
                margin.addData(lineNo + 1, timestamp, timestamp, msg.msgType)

        self.clearUndoRedoHistory()
        if Settings()['ioconsoleautoscroll']:
            line, pos = self.getEndPosition()
            self.gotoLine(line + 1, pos + 1)

    def clearData(self):
        """Clears the collected data"""
        self.__messages.clear()
        self.getMargin('cdm_redirected_io_margin').clear()

    def clearAll(self):
        """Clears both data and visible content"""
        self.clearData()
        self.clear()
        self.clearUndoRedoHistory()