def __createLayout(self): """Creates the widget layout""" totalCalls = self.__stats.total_calls # The calls were not induced via recursion totalPrimitiveCalls = self.__stats.prim_calls totalTime = self.__stats.total_tt txt = "<b>Script:</b> " + self.__script + " " + \ self.__params['arguments'] + "<br/>" \ "<b>Run at:</b> " + self.__reportTime + "<br/>" + \ str(totalCalls) + " function calls (" + \ str(totalPrimitiveCalls) + " primitive calls) in " + \ FLOAT_FORMAT % totalTime + " CPU seconds" summary = QLabel(txt) summary.setToolTip(txt) summary.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) summary.setStyleSheet('QLabel {' + getLabelStyle(self) + '}') self.__scene = QGraphicsScene() self.__viewer = DiagramWidget() self.__viewer.sigEscapePressed.connect(self.__onESC) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) vLayout.addWidget(summary) vLayout.addWidget(self.__viewer) self.setLayout(vLayout)
class FlowUIWidget(QWidget): """The widget which goes along with the text editor""" def __init__(self, editor, parent): QWidget.__init__(self, parent) # It is always not visible at the beginning because there is no # editor content at the start self.setVisible(False) self.__editor = editor self.__parentWidget = parent self.__connected = False self.__needPathUpdate = False self.cflowSettings = getCflowSettings(self) self.__displayProps = (self.cflowSettings.hidedocstrings, self.cflowSettings.hidecomments, self.cflowSettings.hideexcepts, Settings()['smartZoom']) hLayout = QHBoxLayout() hLayout.setContentsMargins(0, 0, 0, 0) hLayout.setSpacing(0) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) # Make pylint happy self.__toolbar = None self.__navBar = None self.__cf = None self.__canvas = None self.__validGroups = [] self.__allGroupId = set() # Create the update timer self.__updateTimer = QTimer(self) self.__updateTimer.setSingleShot(True) self.__updateTimer.timeout.connect(self.process) vLayout.addWidget(self.__createNavigationBar()) vLayout.addWidget(self.__createStackedViews()) hLayout.addLayout(vLayout) hLayout.addWidget(self.__createToolbar()) self.setLayout(hLayout) self.updateSettings() # Connect to the change file type signal self.__mainWindow = GlobalData().mainWindow editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager editorsManager.sigFileTypeChanged.connect(self.__onFileTypeChanged) Settings().sigHideDocstringsChanged.connect( self.__onHideDocstringsChanged) Settings().sigHideCommentsChanged.connect(self.__onHideCommentsChanged) Settings().sigHideExceptsChanged.connect(self.__onHideExceptsChanged) Settings().sigSmartZoomChanged.connect(self.__onSmartZoomChanged) self.setSmartZoomLevel(Settings()['smartZoom']) def getParentWidget(self): return self.__parentWidget def view(self): """Provides a reference to the current view""" return self.smartViews.currentWidget() def scene(self): """Provides a reference to the current scene""" return self.view().scene def __createToolbar(self): """Creates the toolbar""" self.__toolbar = QToolBar(self) self.__toolbar.setOrientation(Qt.Vertical) self.__toolbar.setMovable(False) self.__toolbar.setAllowedAreas(Qt.RightToolBarArea) self.__toolbar.setIconSize(QSize(16, 16)) self.__toolbar.setFixedWidth(30) self.__toolbar.setContentsMargins(0, 0, 0, 0) # Buttons saveAsMenu = QMenu(self) saveAsSVGAct = saveAsMenu.addAction(getIcon('filesvg.png'), 'Save as SVG...') saveAsSVGAct.triggered.connect(self.onSaveAsSVG) saveAsPDFAct = saveAsMenu.addAction(getIcon('filepdf.png'), 'Save as PDF...') saveAsPDFAct.triggered.connect(self.onSaveAsPDF) saveAsPNGAct = saveAsMenu.addAction(getIcon('filepixmap.png'), 'Save as PNG...') saveAsPNGAct.triggered.connect(self.onSaveAsPNG) saveAsMenu.addSeparator() saveAsCopyToClipboardAct = saveAsMenu.addAction( getIcon('copymenu.png'), 'Copy to clipboard') saveAsCopyToClipboardAct.triggered.connect(self.copyToClipboard) self.__saveAsButton = QToolButton(self) self.__saveAsButton.setIcon(getIcon('saveasmenu.png')) self.__saveAsButton.setToolTip('Save as') self.__saveAsButton.setPopupMode(QToolButton.InstantPopup) self.__saveAsButton.setMenu(saveAsMenu) self.__saveAsButton.setFocusPolicy(Qt.NoFocus) self.__levelUpButton = QToolButton(self) self.__levelUpButton.setFocusPolicy(Qt.NoFocus) self.__levelUpButton.setIcon(getIcon('levelup.png')) self.__levelUpButton.setToolTip('Smart zoom level up (Shift+wheel)') self.__levelUpButton.clicked.connect(self.onSmartZoomLevelUp) self.__levelIndicator = QLabel('<b>0</b>', self) self.__levelIndicator.setAlignment(Qt.AlignCenter) self.__levelDownButton = QToolButton(self) self.__levelDownButton.setFocusPolicy(Qt.NoFocus) self.__levelDownButton.setIcon(getIcon('leveldown.png')) self.__levelDownButton.setToolTip('Smart zoom level down (Shift+wheel)') self.__levelDownButton.clicked.connect(self.onSmartZoomLevelDown) fixedSpacer = QWidget() fixedSpacer.setFixedHeight(10) self.__hideDocstrings = QToolButton(self) self.__hideDocstrings.setCheckable(True) self.__hideDocstrings.setIcon(getIcon('hidedocstrings.png')) self.__hideDocstrings.setToolTip('Show/hide docstrings') self.__hideDocstrings.setFocusPolicy(Qt.NoFocus) self.__hideDocstrings.setChecked(Settings()['hidedocstrings']) self.__hideDocstrings.clicked.connect(self.__onHideDocstrings) self.__hideComments = QToolButton(self) self.__hideComments.setCheckable(True) self.__hideComments.setIcon(getIcon('hidecomments.png')) self.__hideComments.setToolTip('Show/hide comments') self.__hideComments.setFocusPolicy(Qt.NoFocus) self.__hideComments.setChecked(Settings()['hidecomments']) self.__hideComments.clicked.connect(self.__onHideComments) self.__hideExcepts = QToolButton(self) self.__hideExcepts.setCheckable(True) self.__hideExcepts.setIcon(getIcon('hideexcepts.png')) self.__hideExcepts.setToolTip('Show/hide except blocks') self.__hideExcepts.setFocusPolicy(Qt.NoFocus) self.__hideExcepts.setChecked(Settings()['hideexcepts']) self.__hideExcepts.clicked.connect(self.__onHideExcepts) spacer = QWidget() spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.__toolbar.addWidget(self.__saveAsButton) self.__toolbar.addWidget(spacer) self.__toolbar.addWidget(self.__levelUpButton) self.__toolbar.addWidget(self.__levelIndicator) self.__toolbar.addWidget(self.__levelDownButton) self.__toolbar.addWidget(fixedSpacer) self.__toolbar.addWidget(self.__hideDocstrings) self.__toolbar.addWidget(self.__hideComments) self.__toolbar.addWidget(self.__hideExcepts) return self.__toolbar def __createNavigationBar(self): """Creates the navigation bar""" self.__navBar = ControlFlowNavigationBar(self) return self.__navBar def __createStackedViews(self): """Creates the graphics view""" self.smartViews = QStackedWidget(self) self.smartViews.setContentsMargins(0, 0, 0, 0) self.smartViews.addWidget(CFGraphicsView(self.__navBar, self)) self.smartViews.addWidget(CFGraphicsView(self.__navBar, self)) return self.smartViews def process(self): """Parses the content and displays the results""" if not self.__connected: self.__connectEditorSignals() start = timer() cf = getControlFlowFromMemory(self.__editor.text) end = timer() if cf.errors: self.__navBar.updateInfoIcon(self.__navBar.STATE_BROKEN_UTD) errors = [] for err in cf.errors: if err[0] == -1 and err[1] == -1: errors.append(err[2]) elif err[1] == -1: errors.append('[' + str(err[0]) + ':] ' + err[2]) elif err[0] == -1: errors.append('[:' + str(err[1]) + '] ' + err[2]) else: errors.append('[' + str(err[0]) + ':' + str(err[1]) + '] ' + err[2]) self.__navBar.setErrors(errors) return self.__cf = cf if self.isDebugMode(): logging.info('Parsed file: %s', formatFlow(str(self.__cf))) logging.info('Parse timing: %f', end - start) # Collect warnings (parser + CML warnings) and valid groups self.__validGroups = [] self.__allGroupId = set() allWarnings = self.__cf.warnings + \ CMLVersion.validateCMLComments(self.__cf, self.__validGroups, self.__allGroupId) # That will clear the error tooltip as well self.__navBar.updateInfoIcon(self.__navBar.STATE_OK_UTD) if allWarnings: warnings = [] for warn in allWarnings: if warn[0] == -1 and warn[1] == -1: warnings.append(warn[2]) elif warn[1] == -1: warnings.append('[' + str(warn[0]) + ':] ' + warn[2]) elif warn[0] == -1: warnings.append('[:' + str(warn[1]) + '] ' + warn[2]) else: warnings.append('[' + str(warn[0]) + ':' + str(warn[1]) + '] ' + warn[2]) self.__navBar.setWarnings(warnings) else: self.__navBar.clearWarnings() self.redrawScene() def __cleanupCanvas(self): """Cleans up the canvas""" if self.__canvas is not None: self.__canvas.cleanup() self.__canvas = None for item in self.scene().items(): item.cleanup() self.scene().clear() def redrawScene(self): """Redraws the scene""" smartZoomLevel = Settings()['smartZoom'] self.cflowSettings = getCflowSettings(self) if self.dirty(): self.__displayProps = (self.cflowSettings.hidedocstrings, self.cflowSettings.hidecomments, self.cflowSettings.hideexcepts, smartZoomLevel) self.cflowSettings.itemID = 0 self.cflowSettings = tweakSmartSettings(self.cflowSettings, smartZoomLevel) try: fileName = self.__parentWidget.getFileName() if not fileName: fileName = self.__parentWidget.getShortName() collapsedGroups = getCollapsedGroups(fileName) # Top level canvas has no adress and no parent canvas self.__cleanupCanvas() self.__canvas = VirtualCanvas(self.cflowSettings, None, None, self.__validGroups, collapsedGroups, None) lStart = timer() self.__canvas.layoutModule(self.__cf) lEnd = timer() self.__canvas.setEditor(self.__editor) width, height = self.__canvas.render() rEnd = timer() self.scene().setSceneRect(0, 0, width, height) self.__canvas.draw(self.scene(), 0, 0) dEnd = timer() if self.isDebugMode(): logging.info('Redrawing is done. Size: %d x %d', width, height) logging.info('Layout timing: %f', lEnd - lStart) logging.info('Render timing: %f', rEnd - lEnd) logging.info('Draw timing: %f', dEnd - rEnd) except Exception as exc: logging.error(str(exc)) raise def onFlowZoomChanged(self): """Triggered when a flow zoom is changed""" if self.__cf: selection = self.scene().serializeSelection() firstOnScreen = self.scene().getFirstLogicalItem() self.cflowSettings.onFlowZoomChanged() self.redrawScene() self.updateNavigationToolbar('') self.scene().restoreSelectionByID(selection) self.__restoreScroll(firstOnScreen) def __onFileTypeChanged(self, fileName, uuid, newFileType): """Triggered when a buffer content type has changed""" if self.__parentWidget.getUUID() != uuid: return if not isPythonMime(newFileType): self.__disconnectEditorSignals() self.__updateTimer.stop() self.__cleanupCanvas() self.__cf = None self.__validGroups = [] self.setVisible(False) self.__navBar.updateInfoIcon(self.__navBar.STATE_UNKNOWN) return # Update the bar and show it self.setVisible(True) self.process() # The buffer type change event comes when the content is loaded first # time. So this is a good point to restore the position _, _, _, cflowHPos, cflowVPos = getFilePosition(fileName) self.setScrollbarPositions(cflowHPos, cflowVPos) def terminate(self): """Called when a tab is closed""" if self.__updateTimer.isActive(): self.__updateTimer.stop() self.__updateTimer.deleteLater() self.__disconnectEditorSignals() self.__mainWindow = GlobalData().mainWindow editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager editorsManager.sigFileTypeChanged.disconnect(self.__onFileTypeChanged) Settings().sigHideDocstringsChanged.disconnect( self.__onHideDocstringsChanged) Settings().sigHideCommentsChanged.disconnect(self.__onHideCommentsChanged) Settings().sigHideExceptsChanged.disconnect(self.__onHideExceptsChanged) Settings().sigSmartZoomChanged.disconnect(self.__onSmartZoomChanged) # Helps GC to collect more self.__cleanupCanvas() for index in range(self.smartViews.count()): self.smartViews.widget(index).terminate() self.smartViews.widget(index).deleteLater() self.smartViews.deleteLater() self.__navBar.deleteLater() self.__cf = None self.__saveAsButton.menu().deleteLater() self.__saveAsButton.deleteLater() self.__levelUpButton.clicked.disconnect(self.onSmartZoomLevelUp) self.__levelUpButton.deleteLater() self.__levelDownButton.clicked.disconnect(self.onSmartZoomLevelDown) self.__levelDownButton.deleteLater() self.__hideDocstrings.clicked.disconnect(self.__onHideDocstrings) self.__hideDocstrings.deleteLater() self.__hideComments.clicked.disconnect(self.__onHideComments) self.__hideComments.deleteLater() self.__hideExcepts.clicked.disconnect(self.__onHideExcepts) self.__hideExcepts.deleteLater() self.__toolbar.deleteLater() self.__editor = None self.__parentWidget = None self.cflowSettings = None self.__displayProps = None def __connectEditorSignals(self): """When it is a python file - connect to the editor signals""" if not self.__connected: self.__editor.cursorPositionChanged.connect( self.__cursorPositionChanged) self.__editor.textChanged.connect(self.__onBufferChanged) self.__connected = True def __disconnectEditorSignals(self): """Disconnect the editor signals when the file is not a python one""" if self.__connected: self.__editor.cursorPositionChanged.disconnect( self.__cursorPositionChanged) self.__editor.textChanged.disconnect(self.__onBufferChanged) self.__connected = False def __cursorPositionChanged(self): """Cursor position changed""" # The timer should be reset only in case if the redrawing was delayed if self.__updateTimer.isActive(): self.__updateTimer.stop() self.__updateTimer.start(IDLE_TIMEOUT) def __onBufferChanged(self): """Triggered to update status icon and to restart the timer""" self.__updateTimer.stop() if self.__navBar.getCurrentState() in [self.__navBar.STATE_OK_UTD, self.__navBar.STATE_OK_CHN, self.__navBar.STATE_UNKNOWN]: self.__navBar.updateInfoIcon(self.__navBar.STATE_OK_CHN) else: self.__navBar.updateInfoIcon(self.__navBar.STATE_BROKEN_CHN) self.__updateTimer.start(IDLE_TIMEOUT) def redrawNow(self): """Redraw the diagram regardless of the timer""" if self.__updateTimer.isActive(): self.__updateTimer.stop() self.process() def generateNewGroupId(self): """Generates a new group ID (string)""" # It can also consider the current set of the groups: valid + invalid # and generate an integer id which is shorter for vacantGroupId in range(1000): groupId = str(vacantGroupId) if not groupId in self.__allGroupId: return groupId # Last resort return str(uuid.uuid1()) def updateNavigationToolbar(self, text): """Updates the toolbar text""" if self.__needPathUpdate: self.__navBar.setPath(text) def updateSettings(self): """Updates settings""" self.__needPathUpdate = Settings()['showCFNavigationBar'] self.__navBar.setPathVisible(self.__needPathUpdate) self.__navBar.setPath('') def highlightAtAbsPos(self, absPos, line, pos): """Scrolls the view to the item closest to absPos and selects it. line and pos are 1-based """ item, _ = self.scene().getNearestItem(absPos, line, pos) if item: GlobalData().mainWindow.setFocusToFloatingRenderer() self.scene().clearSelection() item.setSelected(True) self.view().scrollTo(item) self.setFocus() def setFocus(self): """Sets the focus""" self.view().setFocus() @staticmethod def __getDefaultSaveDir(): """Provides the default directory to save files to""" project = GlobalData().project if project.isLoaded(): return project.getProjectDir() return QDir.currentPath() def __selectFile(self, extension): """Picks a file of a certain extension""" dialog = QFileDialog(self, 'Save flowchart as') dialog.setFileMode(QFileDialog.AnyFile) dialog.setLabelText(QFileDialog.Accept, "Save") dialog.setNameFilter(extension.upper() + " files (*." + extension.lower() + ")") urls = [] for dname in QDir.drives(): urls.append(QUrl.fromLocalFile(dname.absoluteFilePath())) urls.append(QUrl.fromLocalFile(QDir.homePath())) project = GlobalData().project if project.isLoaded(): urls.append(QUrl.fromLocalFile(project.getProjectDir())) dialog.setSidebarUrls(urls) suggestedFName = self.__parentWidget.getFileName() if '.' in suggestedFName: dotIndex = suggestedFName.rindex('.') suggestedFName = suggestedFName[:dotIndex] dialog.setDirectory(self.__getDefaultSaveDir()) dialog.selectFile(suggestedFName + "." + extension.lower()) dialog.setOption(QFileDialog.DontConfirmOverwrite, False) dialog.setOption(QFileDialog.DontUseNativeDialog, True) if dialog.exec_() != QDialog.Accepted: return None fileNames = dialog.selectedFiles() fileName = os.path.abspath(str(fileNames[0])) if os.path.isdir(fileName): logging.error("A file must be selected") return None if "." not in fileName: fileName += "." + extension.lower() # Check permissions to write into the file or to a directory if os.path.exists(fileName): # Check write permissions for the file if not os.access(fileName, os.W_OK): logging.error("There is no write permissions for " + fileName) return None else: # Check write permissions to the directory dirName = os.path.dirname(fileName) if not os.access(dirName, os.W_OK): logging.error("There is no write permissions for the " "directory " + dirName) return None if os.path.exists(fileName): res = QMessageBox.warning( self, "Save flowchart as", "<p>The file <b>" + fileName + "</b> already exists.</p>", QMessageBox.StandardButtons(QMessageBox.Abort | QMessageBox.Save), QMessageBox.Abort) if res == QMessageBox.Abort or res == QMessageBox.Cancel: return None # All prerequisites are checked, return a file name return fileName def onSaveAsSVG(self): """Triggered on the 'Save as SVG' button""" fileName = self.__selectFile("svg") if fileName is None: return False try: self.__saveAsSVG(fileName) except Exception as excpt: logging.error(str(excpt)) return False return True def __saveAsSVG(self, fileName): """Saves the flowchart as an SVG file""" generator = QSvgGenerator() generator.setFileName(fileName) generator.setSize(QSize(self.scene().width(), self.scene().height())) painter = QPainter(generator) self.scene().render(painter) painter.end() def onSaveAsPDF(self): """Triggered on the 'Save as PDF' button""" fileName = self.__selectFile("pdf") if fileName is None: return False try: self.__saveAsPDF(fileName) except Exception as excpt: logging.error(str(excpt)) return False return True def __saveAsPDF(self, fileName): """Saves the flowchart as an PDF file""" printer = QPrinter() printer.setOutputFormat(QPrinter.PdfFormat) printer.setPaperSize(QSizeF(self.scene().width(), self.scene().height()), QPrinter.Point) printer.setFullPage(True) printer.setOutputFileName(fileName) painter = QPainter(printer) self.scene().render(painter) painter.end() def onSaveAsPNG(self): """Triggered on the 'Save as PNG' button""" fileName = self.__selectFile("png") if fileName is None: return False try: self.__saveAsPNG(fileName) except Exception as excpt: logging.error(str(excpt)) return False return True def __getPNG(self): """Renders the scene as PNG""" image = QImage(self.scene().width(), self.scene().height(), QImage.Format_ARGB32_Premultiplied) painter = QPainter(image) # It seems that the better results are without antialiasing # painter.setRenderHint( QPainter.Antialiasing ) self.scene().render(painter) painter.end() return image def __saveAsPNG(self, fileName): """Saves the flowchart as an PNG file""" image = self.__getPNG() image.save(fileName, "PNG") def copyToClipboard(self): """Copies the rendered scene to the clipboard as an image""" image = self.__getPNG() clip = QApplication.clipboard() clip.setImage(image) def getScrollbarPositions(self): """Provides the scrollbar positions""" hScrollBar = self.view().horizontalScrollBar() vScrollBar = self.view().verticalScrollBar() return hScrollBar.value(), vScrollBar.value() def setScrollbarPositions(self, hPos, vPos): """Sets the scrollbar positions for the view""" self.view().horizontalScrollBar().setValue(hPos) self.view().verticalScrollBar().setValue(vPos) def __onHideDocstrings(self): """Triggered when a hide docstring button is pressed""" Settings()['hidedocstrings'] = not Settings()['hidedocstrings'] def __onHideDocstringsChanged(self): """Signalled by settings""" selection = self.scene().serializeSelection() firstOnScreen = self.scene().getFirstLogicalItem() settings = Settings() self.__hideDocstrings.setChecked(settings['hidedocstrings']) if self.__checkNeedRedraw(): self.scene().restoreSelectionByID(selection) self.__restoreScroll(firstOnScreen) def __onHideComments(self): """Triggered when a hide comments button is pressed""" Settings()['hidecomments'] = not Settings()['hidecomments'] def __onHideCommentsChanged(self): """Signalled by settings""" selection = self.scene().serializeSelection() firstOnScreen = self.scene().getFirstLogicalItem() settings = Settings() self.__hideComments.setChecked(settings['hidecomments']) if self.__checkNeedRedraw(): self.scene().restoreSelectionByID(selection) self.__restoreScroll(firstOnScreen) def __onHideExcepts(self): """Triggered when a hide except blocks button is pressed""" Settings()['hideexcepts'] = not Settings()['hideexcepts'] def __onHideExceptsChanged(self): """Signalled by settings""" selection = self.scene().serializeSelection() firstOnScreen = self.scene().getFirstLogicalItem() settings = Settings() self.__hideExcepts.setChecked(settings['hideexcepts']) if self.__checkNeedRedraw(): self.scene().restoreSelectionByTooltip(selection) self.__restoreScroll(firstOnScreen) def __checkNeedRedraw(self): """Redraws the scene if necessary when a display setting is changed""" editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager if self.__parentWidget == editorsManager.currentWidget(): self.updateNavigationToolbar('') self.process() return True return False def dirty(self): """True if some other tab has switched display settings""" settings = Settings() return self.__displayProps[0] != settings['hidedocstrings'] or \ self.__displayProps[1] != settings['hidecomments'] or \ self.__displayProps[2] != settings['hideexcepts'] or \ self.__displayProps[3] != settings['smartZoom'] def onSmartZoomLevelUp(self): """Triggered when an upper smart zoom level was requested""" Settings().onSmartZoomIn() def onSmartZoomLevelDown(self): """Triggered when an lower smart zoom level was requested""" Settings().onSmartZoomOut() def setSmartZoomLevel(self, smartZoomLevel): """Sets the new smart zoom level""" maxSmartZoom = Settings().MAX_SMART_ZOOM if smartZoomLevel < 0 or smartZoomLevel > maxSmartZoom: return self.__levelIndicator.setText('<b>' + str(smartZoomLevel) + '</b>') self.__levelIndicator.setToolTip( getSmartZoomDescription(smartZoomLevel)) self.__levelUpButton.setEnabled(smartZoomLevel < maxSmartZoom) self.__levelDownButton.setEnabled(smartZoomLevel > 0) self.smartViews.setCurrentIndex(smartZoomLevel) def __onSmartZoomChanged(self): """Triggered when a smart zoom changed""" selection = self.scene().serializeSelection() firstOnScreen = self.scene().getFirstLogicalItem() self.setSmartZoomLevel(Settings()['smartZoom']) if self.__checkNeedRedraw(): self.scene().restoreSelectionByTooltip(selection) self.__restoreScroll(firstOnScreen) def __restoreScroll(self, toItem): """Restores the view scrolling to the best possible position""" if toItem: lineRange = toItem.getLineRange() absPosRange = toItem.getAbsPosRange() item, _ = self.scene().getNearestItem(absPosRange[0], lineRange[0], 0) if item: self.view().scrollTo(item, True) self.view().horizontalScrollBar().setValue(0) def validateCollapsedGroups(self, fileName): """Checks that there are no collapsed groups which are invalid""" if self.__navBar.getCurrentState() != self.__navBar.STATE_OK_UTD: return collapsedGroups = getCollapsedGroups(fileName) if collapsedGroups: toBeDeleted = [] for groupId in collapsedGroups: for validId, start, end in self.__validGroups: del start del end if validId == groupId: break else: toBeDeleted.append(groupId) if toBeDeleted: for groupId in toBeDeleted: collapsedGroups.remove(groupId) setCollapsedGroups(fileName, collapsedGroups) else: setCollapsedGroups(fileName, []) def getDocItemByAnchor(self, anchor): """Provides the graphics item for the given anchor if so""" return self.scene().getDocItemByAnchor(anchor) @staticmethod def isDebugMode(): """True if it is a debug mode""" return GlobalData().skin['debug']
class ControlFlowNavigationBar(QFrame): """Navigation bar at the top of the flow UI widget""" STATE_OK_UTD = 0 # Parsed OK, control flow up to date STATE_OK_CHN = 1 # Parsed OK, control flow changed STATE_BROKEN_UTD = 2 # Parsed with errors, control flow up to date STATE_BROKEN_CHN = 3 # Parsed with errors, control flow changed STATE_UNKNOWN = 4 def __init__(self, parent): QFrame.__init__(self, parent) self.__infoIcon = None self.__warningsIcon = None self.__layout = None self.__pathLabel = None self.__createLayout() self.__currentIconState = self.STATE_UNKNOWN def __createLayout(self): """Creates the layout""" self.__layout = QHBoxLayout(self) self.__layout.setContentsMargins(0, 0, 0, 0) # Create info icon self.__infoIcon = QLabel(self) self.__infoIcon.setPixmap(getPixmap('cfunknown.png')) self.__layout.addWidget(self.__infoIcon) self.__warningsIcon = QLabel(self) self.__warningsIcon.setPixmap(getPixmap('cfwarning.png')) self.__layout.addWidget(self.__warningsIcon) self.clearWarnings() # Create the path label self.__pathLabel = HeaderFitLabel(self) self.__pathLabel.setTextFormat(Qt.PlainText) self.__pathLabel.setAlignment(Qt.AlignLeft) self.__pathLabel.setWordWrap(False) self.__pathLabel.setTextInteractionFlags(Qt.NoTextInteraction) self.__pathLabel.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.__pathLabel.setMinimumWidth(40) self.__layout.addWidget(self.__pathLabel) self.__spacer = ToolBarExpandingSpacer(self) self.__spacer.setMinimumWidth(0) self.__layout.addWidget(self.__spacer) # Create the selection label self.__selectionLabel = HeaderLabel('', None, self) self.__selectionLabel.setTextFormat(Qt.PlainText) self.__selectionLabel.setAlignment(Qt.AlignCenter) self.__selectionLabel.setWordWrap(False) self.__selectionLabel.setTextInteractionFlags(Qt.NoTextInteraction) self.__selectionLabel.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.__selectionLabel.setMinimumWidth(40) self.__layout.addWidget(self.__selectionLabel) self.setSelectionLabel(0, None) def clearWarnings(self): """Clears the warnings""" self.__warningsIcon.setVisible(False) self.__warningsIcon.setToolTip("") def setWarnings(self, warnings): """Sets the warnings""" self.__warningsIcon.setToolTip('Control flow parser warnings:\n' + '\n'.join(warnings)) self.__warningsIcon.setVisible(True) def clearErrors(self): """Clears all the errors""" self.__infoIcon.setToolTip('') def setErrors(self, errors): """Sets the errors""" self.__infoIcon.setToolTip('Control flow parser errors:\n' + '\n'.join(errors)) def updateInfoIcon(self, state): """Updates the information icon""" if state == self.__currentIconState: return if state == self.STATE_OK_UTD: self.__infoIcon.setPixmap(getPixmap('cfokutd.png')) self.__infoIcon.setToolTip("Control flow is up to date") self.__currentIconState = self.STATE_OK_UTD elif state == self.STATE_OK_CHN: self.__infoIcon.setPixmap(getPixmap('cfokchn.png')) self.__infoIcon.setToolTip("Control flow is not up to date; " "will be updated on idle") self.__currentIconState = self.STATE_OK_CHN elif state == self.STATE_BROKEN_UTD: self.__infoIcon.setPixmap(getPixmap('cfbrokenutd.png')) self.__infoIcon.setToolTip("Control flow might be invalid " "due to invalid python code") self.__currentIconState = self.STATE_BROKEN_UTD elif state == self.STATE_BROKEN_CHN: self.__infoIcon.setPixmap(getPixmap('cfbrokenchn.png')) self.__infoIcon.setToolTip("Control flow might be invalid; " "will be updated on idle") self.__currentIconState = self.STATE_BROKEN_CHN else: # STATE_UNKNOWN self.__infoIcon.setPixmap(getPixmap('cfunknown.png')) self.__infoIcon.setToolTip("Control flow state is unknown") self.__currentIconState = self.STATE_UNKNOWN def getCurrentState(self): """Provides the current state""" return self.__currentIconState def setPath(self, txt): """Sets the path label content""" self.__pathLabel.setText(txt) def setPathVisible(self, switchOn): """Sets the path visible""" self.__pathLabel.setVisible(switchOn) self.__spacer.setVisible(not switchOn) def setSelectionLabel(self, text, tooltip): """Sets selection label""" self.__selectionLabel.setText(str(text)) if tooltip: self.__selectionLabel.setToolTip("Selected items:\n" + str(tooltip)) else: self.__selectionLabel.setToolTip("Number of selected items") def resizeEvent(self, event): """Editor has resized""" QFrame.resizeEvent(self, event)
class MDTopBar(QFrame): """MD widget top bar at the top""" STATE_OK_UTD = 0 # Parsed OK, MD up to date STATE_OK_CHN = 1 # Parsed OK, MD changed STATE_BROKEN_UTD = 2 # Parsed with errors, MD up to date STATE_BROKEN_CHN = 3 # Parsed with errors, MD changed STATE_UNKNOWN = 4 def __init__(self, parent): QFrame.__init__(self, parent) self.__infoIcon = None self.__warningsIcon = None self.__layout = None self.__createLayout() self.__currentIconState = self.STATE_UNKNOWN def __createLayout(self): """Creates the layout""" self.setFixedHeight(24) self.__layout = QHBoxLayout(self) self.__layout.setContentsMargins(0, 0, 0, 0) # Create info icon self.__infoIcon = QLabel() self.__infoIcon.setPixmap(getPixmap('cfunknown.png')) self.__layout.addWidget(self.__infoIcon) self.__warningsIcon = QLabel() self.__warningsIcon.setPixmap(getPixmap('cfwarning.png')) self.__layout.addWidget(self.__warningsIcon) self.clearWarnings() self.__spacer = QWidget() self.__spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.__spacer.setMinimumWidth(0) self.__layout.addWidget(self.__spacer) def clearWarnings(self): """Clears the warnings""" self.__warningsIcon.setVisible(False) self.__warningsIcon.setToolTip("") def setWarnings(self, warnings): """Sets the warnings""" self.__warningsIcon.setToolTip('Markdown parser warnings:\n' + '\n'.join(warnings)) self.__warningsIcon.setVisible(True) def clearErrors(self): """Clears all the errors""" self.__infoIcon.setToolTip('') def setErrors(self, errors): """Sets the errors""" self.__infoIcon.setToolTip('Markdown parser errors:\n' + '\n'.join(errors)) def updateInfoIcon(self, state): """Updates the information icon""" if state == self.__currentIconState: return if state == self.STATE_OK_UTD: self.__infoIcon.setPixmap(getPixmap('cfokutd.png')) self.__infoIcon.setToolTip("Markdown render is up to date") self.__currentIconState = self.STATE_OK_UTD elif state == self.STATE_OK_CHN: self.__infoIcon.setPixmap(getPixmap('cfokchn.png')) self.__infoIcon.setToolTip("Markdown render is not up to date; " "will be updated on idle") self.__currentIconState = self.STATE_OK_CHN elif state == self.STATE_BROKEN_UTD: self.__infoIcon.setPixmap(getPixmap('cfbrokenutd.png')) self.__infoIcon.setToolTip("Markdown render might be invalid " "due to invalid python code") self.__currentIconState = self.STATE_BROKEN_UTD elif state == self.STATE_BROKEN_CHN: self.__infoIcon.setPixmap(getPixmap('cfbrokenchn.png')) self.__infoIcon.setToolTip("Markdown render might be invalid; " "will be updated on idle") self.__currentIconState = self.STATE_BROKEN_CHN else: # STATE_UNKNOWN self.__infoIcon.setPixmap(getPixmap('cfunknown.png')) self.__infoIcon.setToolTip("Markdown render state is unknown") self.__currentIconState = self.STATE_UNKNOWN def getCurrentState(self): """Provides the current state""" return self.__currentIconState def resizeEvent(self, event): """Editor has resized""" QFrame.resizeEvent(self, event)
class NavigationBar(QFrame): """Navigation bar at the top of the editor (python only)""" STATE_OK_UTD = 0 # Parsed OK, context up to date STATE_OK_CHN = 1 # Parsed OK, context changed STATE_BROKEN_UTD = 2 # Parsed with errors, context up to date STATE_BROKEN_CHN = 3 # Parsed with errors, context changed STATE_UNKNOWN = 4 def __init__(self, editor, parent): QFrame.__init__(self, parent) self.__editor = editor self.__parentWidget = parent # It is always not visible at the beginning because there is no # editor content at the start self.setVisible(False) # There is no parser info used to display values self.__currentInfo = None self.__currentIconState = self.STATE_UNKNOWN self.__connected = False # List of PathElement starting after the global scope self.__path = [] self.__createLayout() # Create the update timer self.__updateTimer = QTimer(self) self.__updateTimer.setSingleShot(True) self.__updateTimer.timeout.connect(self.updateBar) # Connect to the change file type signal mainWindow = GlobalData().mainWindow editorsManager = mainWindow.editorsManagerWidget.editorsManager editorsManager.sigFileTypeChanged.connect(self.__onFileTypeChanged) def getEditor(self): """Provides the editor""" return self.__editor def __connectEditorSignals(self): """When it is a python file - connect to the editor signals""" if not self.__connected: self.__editor.cursorPositionChanged.connect( self.__cursorPositionChanged) self.__editor.textChanged.connect(self.__onBufferChanged) self.__connected = True def __disconnectEditorSignals(self): """Disconnect the editor signals when the file is not a python one""" if self.__connected: self.__editor.cursorPositionChanged.disconnect( self.__cursorPositionChanged) self.__editor.textChanged.disconnect(self.__onBufferChanged) self.__connected = False def __createLayout(self): """Creates the layout""" self.setFixedHeight(24) self.__layout = QHBoxLayout(self) self.__layout.setContentsMargins(0, 0, 0, 0) # Set the background color # Create info icon self.__infoIcon = QLabel() self.__layout.addWidget(self.__infoIcon) self.__globalScopeCombo = NavBarComboBox(self) self.__globalScopeCombo.jumpToLine.connect(self.__onJumpToLine) self.__layout.addWidget(self.__globalScopeCombo) self.__spacer = QWidget() self.__spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.__layout.addWidget(self.__spacer) def __updateInfoIcon(self, state): """Updates the information icon""" if state != self.__currentIconState: if state == self.STATE_OK_UTD: self.__infoIcon.setPixmap(getPixmap('nbokutd.png')) self.__infoIcon.setToolTip("Context is up to date") self.__currentIconState = self.STATE_OK_UTD elif state == self.STATE_OK_CHN: self.__infoIcon.setPixmap(getPixmap('nbokchn.png')) self.__infoIcon.setToolTip("Context is not up to date; " "will be updated on idle") self.__currentIconState = self.STATE_OK_CHN elif state == self.STATE_BROKEN_UTD: self.__infoIcon.setPixmap(getPixmap('nbbrokenutd.png')) self.__infoIcon.setToolTip("Context might be invalid " "due to invalid python code") self.__currentIconState = self.STATE_BROKEN_UTD else: # STATE_BROKEN_CHN self.__infoIcon.setPixmap(getPixmap('nbbrokenchn.png')) self.__infoIcon.setToolTip("Context might be invalid; " "will be updated on idle") self.__currentIconState = self.STATE_BROKEN_CHN def resizeEvent(self, event): """Editor has resized""" QFrame.resizeEvent(self, event) # Arguments: fileName, uuid, newFileType def __onFileTypeChanged(self, _, uuid, newFileType): """Triggered when a buffer content type has changed""" if self.__parentWidget.getUUID() != uuid: return if isPythonMime(newFileType) and Settings()['showNavigationBar']: # Update the bar and show it self.setVisible(True) self.updateBar() else: self.__disconnectEditorSignals() self.__updateTimer.stop() self.__currentInfo = None self.setVisible(False) self.__currentIconState = self.STATE_UNKNOWN def updateSettings(self): """Called when navigation bar settings have been updated""" textMime = self.__parentWidget.getMime() if Settings()['showNavigationBar'] and isPythonMime(textMime): self.setVisible(True) self.updateBar() else: self.__disconnectEditorSignals() self.__updateTimer.stop() self.__currentInfo = None self.setVisible(False) def updateBar(self): """Triggered when the timer is fired""" self.__updateTimer.stop() # just in case if not isPythonMime(self.__parentWidget.getMime()): return if not self.__connected: self.__connectEditorSignals() # Parse the buffer content self.__currentInfo = getBriefModuleInfoFromMemory(self.__editor.text) # Decide what icon to use if self.__currentInfo.isOK: self.__updateInfoIcon(self.STATE_OK_UTD) else: self.__updateInfoIcon(self.STATE_BROKEN_UTD) # Calc the cursor context context = getContext(self.__editor, self.__currentInfo, True, False) # Display the context self.__populateGlobalScope() if context.length == 0: self.__globalScopeCombo.setCurrentIndex(-1) else: index = self.__globalScopeCombo.findData( context.levels[0][0].line) self.__globalScopeCombo.setCurrentIndex(index) usedFromStore = 0 index = 1 while index < context.length: if len(self.__path) < index: newPathItem = PathElement(self) self.__path.append(newPathItem) self.__layout.addWidget(newPathItem.icon) self.__layout.addWidget(newPathItem.combo) combo = newPathItem.combo combo.pathIndex = len(self.__path) - 1 combo.jumpToLine.connect(self.__onJumpToLine) else: self.__path[index - 1].icon.setVisible(True) self.__path[index - 1].combo.setVisible(True) combo = self.__path[index - 1].combo combo.clear() # Populate the combo box self.__populateClassesAndFunctions(context.levels[index - 1][0], combo) combo.setCurrentIndex(combo.findData(context.levels[index][0].line)) index += 1 usedFromStore += 1 # it might need to have one more level with nothing selected if context.length > 0: if len(context.levels[context.length - 1][0].functions) > 0 or \ len(context.levels[context.length - 1][0].classes) > 0: # Need to add a combo if len(self.__path) <= usedFromStore: newPathItem = PathElement(self) self.__path.append(newPathItem) self.__layout.addWidget(newPathItem.icon) self.__layout.addWidget(newPathItem.combo) combo = newPathItem.combo combo.pathIndex = len(self.__path) - 1 combo.jumpToLine.connect(self.__onJumpToLine) else: self.__path[index - 1].icon.setVisible(True) self.__path[index - 1].combo.setVisible(True) combo = self.__path[index - 1].combo combo.clear() self.__populateClassesAndFunctions( context.levels[context.length - 1][0], combo) combo.setCurrentIndex(-1) usedFromStore += 1 # Hide extra components if so index = usedFromStore while index < len(self.__path): self.__path[index].icon.setVisible(False) self.__path[index].combo.setVisible(False) index += 1 # Make sure the spacer is the last item self.__layout.removeWidget(self.__spacer) self.__layout.addWidget(self.__spacer) def __populateGlobalScope(self): """Repopulates the global scope combo box""" self.__globalScopeCombo.clear() self.__populateClassesAndFunctions(self.__currentInfo, self.__globalScopeCombo) if not Settings()['navbarglobalsimports']: return if len(self.__currentInfo.globals) == 0 and \ len(self.__currentInfo.imports) == 0: return if self.__globalScopeCombo.count() != 0: self.__globalScopeCombo.insertSeparator( self.__globalScopeCombo.count()) for glob in self.__currentInfo.globals: self.__globalScopeCombo.addItem(getIcon('globalvar.png'), glob.name, glob.line) for imp in self.__currentInfo.imports: self.__globalScopeCombo.addItem(getIcon('imports.png'), imp.name, imp.line) @staticmethod def __populateClassesAndFunctions(infoObj, combo): """Populates the combo with classes and functions from the infoObj""" for klass in infoObj.classes: combo.addItem(getIcon('class.png'), klass.name, klass.line) for func in infoObj.functions: if func.isPrivate(): icon = getIcon('method_private.png') elif func.isProtected(): icon = getIcon('method_protected.png') else: icon = getIcon('method.png') combo.addItem(icon, func.name, func.line) def __cursorPositionChanged(self): """Cursor position changed""" self.__onNeedUpdate() def __onBufferChanged(self): """Buffer changed""" self.__onNeedUpdate() def __onNeedUpdate(self): """Triggered to update status icon and to restart the timer""" self.__updateTimer.stop() if self.__currentInfo.isOK: self.__updateInfoIcon(self.STATE_OK_CHN) else: self.__updateInfoIcon(self.STATE_BROKEN_CHN) self.__updateTimer.start(IDLE_TIMEOUT) def __onJumpToLine(self, line): """Triggered when it needs to jump to a line""" self.__editor.gotoLine(line, 0) self.__editor.setFocus() def setFocusToLastCombo(self): """Activates the last combo""" if self.__currentInfo is None: return for index in range(len(self.__path) - 1, -1, -1): if self.__path[index].combo.isVisible(): self.__path[index].combo.setFocus() self.__path[index].combo.showPopup() return self.__globalScopeCombo.setFocus() self.__globalScopeCombo.showPopup() def activateCombo(self, currentCombo, newIndex): """Triggered when a neighbour combo should be activated""" if newIndex == -1: if len(self.__path) > 0: if self.__path[0].combo.isVisible(): currentCombo.hidePopup() self.__globalScopeCombo.setFocus() self.__globalScopeCombo.showPopup() return if newIndex >= len(self.__path): # This is the most right one return if self.__path[newIndex].combo.isVisible(): currentCombo.hidePopup() self.__path[newIndex].combo.setFocus() self.__path[newIndex].combo.showPopup()
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)
class SVNPluginLogDialog(QDialog): """SVN plugin log dialog""" NODIFF = '<html><body bgcolor="#ffffe6"></body></html>' def __init__(self, plugin, client, path, logInfo, parent=None): QDialog.__init__(self, parent) self.__plugin = plugin self.__client = client self.__path = path self.__logInfo = logInfo self.__lhsSelected = None self.__rhsSelected = None self.__createLayout() self.setWindowTitle("SVN Log") lastIndex = len(self.__logInfo) - 1 index = 0 for log in self.__logInfo: newItem = LogItem(log) self.__logView.addTopLevelItem(newItem) if index != lastIndex: rev = log.revision.number nextRev = self.__logInfo[index + 1].revision.number diffButton = self.__createDiffButton( log.revision, self.__logInfo[index + 1].revision) if rev is not None and nextRev is not None: diffButton.setToolTip( "Click to see diff to the older revision (r." + str(rev) + " to r." + str(nextRev) + ")") else: diffButton.setEnabled(False) diffButton.setToolTip( "Could not determine current or previous revision") else: diffButton = self.__createDiffButton(None, None) diffButton.setEnabled(False) diffButton.setToolTip( "Diff to previous revision is not avalable for the first revision" ) self.__logView.setItemWidget(newItem, DIFFTONEXT_COL, diffButton) index += 1 self.__resizeLogView() self.__sortLogView() self.__logView.setFocus() def __createDiffButton(self, rev, prevRev): """Creates a diff button for a path""" button = DiffButton() button.rev = rev button.prevRev = prevRev self.connect(button, SIGNAL('CustomClick'), self.onDiffBetween) return button def __resizeLogView(self): """Resizes the plugins table""" self.__logView.header().setStretchLastSection(True) self.__logView.header().resizeSections(QHeaderView.ResizeToContents) self.__logView.header().resizeSection(SELECT_COL, 28) self.__logView.header().setResizeMode(SELECT_COL, QHeaderView.Fixed) self.__logView.header().resizeSection(DIFFTONEXT_COL, 24) self.__logView.header().setResizeMode(DIFFTONEXT_COL, QHeaderView.Fixed) def __sortLogView(self): """Sorts the log table""" self.__logView.sortItems(self.__logView.sortColumn(), self.__logView.header().sortIndicatorOrder()) def __createLayout(self): """Creates the dialog layout""" self.resize(640, 480) self.setSizeGripEnabled(True) vboxLayout = QVBoxLayout(self) # Revisions to compare compareGroupbox = QGroupBox(self) compareGroupbox.setTitle("Revisions to compare") sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( compareGroupbox.sizePolicy().hasHeightForWidth()) compareGroupbox.setSizePolicy(sizePolicy) revisionLayout = QHBoxLayout(compareGroupbox) self.__lhsRevisionLabel = QLabel() self.__lhsRevisionLabel.setFrameStyle(QFrame.StyledPanel) self.__lhsResetButton = QToolButton() self.__lhsResetButton.setIcon( getIcon(pluginHomeDir + 'svnclearrev.png')) self.__lhsResetButton.setFocusPolicy(Qt.NoFocus) self.__lhsResetButton.setEnabled(False) self.__lhsResetButton.setToolTip("Reset revision to compare") self.__lhsResetButton.clicked.connect(self.__onLHSReset) self.__rhsRevisionLabel = QLabel() self.__rhsRevisionLabel.setFrameStyle(QFrame.StyledPanel) self.__rhsResetButton = QToolButton() self.__rhsResetButton.setIcon( getIcon(pluginHomeDir + 'svnclearrev.png')) self.__rhsResetButton.setFocusPolicy(Qt.NoFocus) self.__rhsResetButton.setEnabled(False) self.__rhsResetButton.setToolTip("Reset revision to compare") self.__rhsResetButton.clicked.connect(self.__onRHSReset) lhsLayout = QHBoxLayout() lhsLayout.addWidget(self.__lhsRevisionLabel) lhsLayout.addWidget(self.__lhsResetButton) rhsLayout = QHBoxLayout() rhsLayout.addWidget(self.__rhsRevisionLabel) rhsLayout.addWidget(self.__rhsResetButton) bothLayout = QVBoxLayout() bothLayout.addLayout(lhsLayout) bothLayout.addLayout(rhsLayout) revisionLayout.addLayout(bothLayout) self.__diffButton = QToolButton() self.__diffButton.setText("Diff") self.__diffButton.setFocusPolicy(Qt.NoFocus) self.__diffButton.setEnabled(False) self.__diffButton.clicked.connect(self.__onDiff) revisionLayout.addWidget(self.__diffButton) vboxLayout.addWidget(compareGroupbox) # Log table logHeaderFrame = QFrame() logHeaderFrame.setFrameStyle(QFrame.StyledPanel) logHeaderFrame.setAutoFillBackground(True) self.__setLightPalette(logHeaderFrame) logHeaderFrame.setFixedHeight(24) logHeaderLayout = QHBoxLayout() logHeaderLayout.setContentsMargins(3, 0, 0, 0) logHeaderLayout.addWidget(QLabel("Subversion log of " + self.__path)) logHeaderFrame.setLayout(logHeaderLayout) vboxLayout.addWidget(logHeaderFrame) self.__logView = QTreeWidget() self.__logView.setAlternatingRowColors(True) self.__logView.setRootIsDecorated(False) self.__logView.setItemsExpandable(False) self.__logView.setSortingEnabled(True) self.__logView.setItemDelegate(NoOutlineHeightDelegate(4)) self.__logViewHeader = QTreeWidgetItem( ["", "", "Revision", "Date", "Author", "Message"]) self.__logView.setHeaderItem(self.__logViewHeader) self.__logView.header().setSortIndicator(REVISION_COL, Qt.AscendingOrder) self.__logView.itemChanged.connect(self.__onLogViewChanged) vboxLayout.addWidget(self.__logView) # Diff part diffHeaderFrame = QFrame() diffHeaderFrame.setFrameStyle(QFrame.StyledPanel) diffHeaderFrame.setAutoFillBackground(True) self.__setLightPalette(diffHeaderFrame) diffHeaderFrame.setFixedHeight(24) diffLabel = QLabel("Diff") diffExpandingSpacer = QSpacerItem(10, 10, QSizePolicy.Expanding) self.__showHideDiffButton = QToolButton() self.__showHideDiffButton.setAutoRaise(True) self.__showHideDiffButton.setIcon(getIcon('less.png')) self.__showHideDiffButton.setFixedSize(20, 20) self.__showHideDiffButton.setToolTip("Show diff") self.__showHideDiffButton.setFocusPolicy(Qt.NoFocus) self.__showHideDiffButton.clicked.connect(self.__onShowHideDiff) diffLayout = QHBoxLayout() diffLayout.setContentsMargins(3, 0, 0, 0) diffLayout.addWidget(diffLabel) diffLayout.addSpacerItem(diffExpandingSpacer) diffLayout.addWidget(self.__showHideDiffButton) diffHeaderFrame.setLayout(diffLayout) self.__diffViewer = DiffTabWidget() self.__diffViewer.setHTML(self.NODIFF) self.__diffViewer.setVisible(False) vboxLayout.addWidget(diffHeaderFrame) vboxLayout.addWidget(self.__diffViewer) # Buttons at the bottom buttonBox = QDialogButtonBox(self) buttonBox.setOrientation(Qt.Horizontal) buttonBox.setStandardButtons(QDialogButtonBox.Ok) buttonBox.button(QDialogButtonBox.Ok).setDefault(True) buttonBox.accepted.connect(self.close) vboxLayout.addWidget(buttonBox) @staticmethod def __setLightPalette(frame): """Creates a lighter palette for the widget background""" palette = frame.palette() background = palette.color(QPalette.Background) background.setRgb(min(background.red() + 30, 255), min(background.green() + 30, 255), min(background.blue() + 30, 255)) palette.setColor(QPalette.Background, background) frame.setPalette(palette) def __onShowHideDiff(self): """On/off the diff section""" if self.__diffViewer.isVisible(): self.__diffViewer.setVisible(False) self.__showHideDiffButton.setIcon(getIcon('less.png')) self.__showHideDiffButton.setToolTip("Show diff") else: self.__diffViewer.setVisible(True) self.__showHideDiffButton.setIcon(getIcon('more.png')) self.__showHideDiffButton.setToolTip("Hide diff") def onDiffBetween(self, rev, prevRev): """Called when diff is requested between revisions""" if not rev or not prevRev: return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: lhsContent = self.__client.cat(self.__path, prevRev) rhsContent = self.__client.cat(self.__path, rev) except Exception as exc: QApplication.restoreOverrideCursor() logging.error(str(exc)) return except: QApplication.restoreOverrideCursor() logging.error("Unknown error while retrieving " + self.__path + " content from the repository.") return QApplication.restoreOverrideCursor() diff = difflib.unified_diff(lhsContent.splitlines(), rhsContent.splitlines()) nodiffMessage = self.__path + " has no difference from revision " + \ str(prevRev.number) + " to revision " + str(rev.number) if diff is None: logging.info(nodiffMessage) return # There are changes, so replace the text and tell about the changes diffAsText = '\n'.join(list(diff)) if diffAsText.strip() == '': logging.info(nodiffMessage) return lhs = "--- revision " + str(prevRev.number) diffAsText = diffAsText.replace("--- ", lhs, 1) rhs = "+++ revision " + str(rev.number) diffAsText = diffAsText.replace("+++ ", rhs, 1) self.__diffViewer.setHTML(parse_from_memory(diffAsText, False, True)) if not self.__diffViewer.isVisible(): self.__onShowHideDiff() def __onDiff(self): """Show diff between revisions""" self.onDiffBetween(self.__rhsSelected.revision, self.__lhsSelected.revision) def __onLogViewChanged(self, item, column): """Revision selected for diff""" if item.checkState(SELECT_COL) == Qt.Checked: # An item has been selected if self.__lhsSelected is None: self.__lhsSelected = item.logInfo self.__normalizeSelected() return if self.__rhsSelected is None: self.__rhsSelected = item.logInfo self.__normalizeSelected() return # Both of the places have been occupied. Pick the one to update. if item.logInfo.date > self.__rhsSelected.date: self.__rhsSelected = item.logInfo else: self.__lhsSelected = item.logInfo self.__normalizeSelected() else: # An item has been de-selected if self.__lhsSelected is not None: if self.__lhsSelected.revision.number == item.logInfo.revision.number: self.__lhsSelected = None elif self.__rhsSelected is not None: if self.__rhsSelected.revision.number == item.logInfo.revision.number: self.__rhsSelected = None self.__normalizeSelected() return def __onLHSReset(self): """Revision removed from diff""" if self.__lhsSelected is not None: self.__deselectRevision(self.__lhsSelected.revision.number) self.__lhsSelected = None self.__lhsRevisionLabel.setText("") self.__diffButton.setEnabled(False) self.__lhsResetButton.setEnabled(False) def __onRHSReset(self): """Revision removed from diff""" if self.__rhsSelected is not None: self.__deselectRevision(self.__rhsSelected.revision.number) self.__rhsSelected = None self.__rhsRevisionLabel.setText("") self.__diffButton.setEnabled(False) self.__rhsResetButton.setEnabled(False) def __deselectRevision(self, revNumber): """Deselects a revision in the list""" index = 0 while index < self.__logView.topLevelItemCount(): item = self.__logView.topLevelItem(index) if item.logInfo.revision.number == revNumber: item.setCheckState(SELECT_COL, Qt.Unchecked) break index += 1 def __normalizeSelected(self): """Puts the earliest revision first""" if self.__lhsSelected is not None and self.__rhsSelected is not None: # It might be necessary to exchange the versions if self.__rhsSelected.date < self.__lhsSelected.date: temp = self.__rhsSelected self.__rhsSelected = self.__lhsSelected self.__lhsSelected = temp self.__diffButton.setEnabled(True) else: self.__diffButton.setEnabled(False) if self.__lhsSelected is None: self.__lhsRevisionLabel.setText("") self.__lhsRevisionLabel.setToolTip("") self.__lhsResetButton.setEnabled(False) else: self.__lhsRevisionLabel.setText( str(self.__lhsSelected.revision.number) + " (" + timestampToString(self.__lhsSelected.date) + ")") self.__lhsRevisionLabel.setToolTip(str(self.__lhsSelected.message)) self.__lhsResetButton.setEnabled(True) if self.__rhsSelected is None: self.__rhsRevisionLabel.setText("") self.__rhsRevisionLabel.setToolTip("") self.__rhsResetButton.setEnabled(False) else: self.__rhsRevisionLabel.setText( str(self.__rhsSelected.revision.number) + " (" + timestampToString(self.__rhsSelected.date) + ")") self.__rhsRevisionLabel.setToolTip(str(self.__rhsSelected.message)) self.__rhsResetButton.setEnabled(True)