Ejemplo n.º 1
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
Ejemplo n.º 2
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)
Ejemplo n.º 3
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)