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 TextEditor(QutepartWrapper, EditorContextMenuMixin): """Text editor implementation""" sigEscapePressed = pyqtSignal() sigCFlowSyncRequested = pyqtSignal(int, int, int) def __init__(self, parent, debugger): self._parent = parent QutepartWrapper.__init__(self, parent) EditorContextMenuMixin.__init__(self) self.setAttribute(Qt.WA_KeyCompression) self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) skin = GlobalData().skin self.setPaper(skin['nolexerPaper']) self.setColor(skin['nolexerColor']) self.currentLineColor = skin['currentLinePaper'] self.onTextZoomChanged() self.__initMargins(debugger) self.cursorPositionChanged.connect(self._onCursorPositionChanged) self.__skipChangeCursor = False self.__openedLine = None self.setFocusPolicy(Qt.StrongFocus) self.indentWidth = 4 self.updateSettings() # Completion support self.__completionPrefix = '' self.__completionLine = -1 self.__completionPos = -1 self.__completer = CodeCompleter(self) self.__inCompletion = False self.__completer.activated.connect(self.insertCompletion) self.__lastTabPosition = None # Calltip support self.__calltip = None self.__callPosition = None self.__calltipTimer = QTimer(self) self.__calltipTimer.setSingleShot(True) self.__calltipTimer.timeout.connect(self.__onCalltipTimer) self.__initHotKeys() self.installEventFilter(self) def dedentLine(self): """Dedent the current line or selection""" self.decreaseIndentAction.activate(QAction.Trigger) def __initHotKeys(self): """Initializes a map for the hot keys event filter""" self.autoIndentLineAction.setShortcut('Ctrl+Shift+I') self.invokeCompletionAction.setEnabled(False) self.__hotKeys = { CTRL_SHIFT: {Qt.Key_T: self.onJumpToTop, Qt.Key_M: self.onJumpToMiddle, Qt.Key_B: self.onJumpToBottom}, SHIFT: {Qt.Key_Delete: self.onShiftDel, Qt.Key_Backtab: self.dedentLine, Qt.Key_End: self.onShiftEnd, Qt.Key_Home: self.onShiftHome}, CTRL: {Qt.Key_X: self.onShiftDel, Qt.Key_C: self.onCtrlC, Qt.Key_Insert: self.onCtrlC, Qt.Key_Apostrophe: self.onHighlight, Qt.Key_Period: self.onNextHighlight, Qt.Key_Comma: self.onPrevHighlight, Qt.Key_M: self.onCommentUncomment, Qt.Key_Space: self.onAutoComplete, Qt.Key_F1: self.onTagHelp, Qt.Key_Backslash: self.onGotoDefinition, Qt.Key_BracketRight: self.onOccurences, Qt.Key_Slash: self.onShowCalltip, Qt.Key_Minus: Settings().onTextZoomOut, Qt.Key_Equal: Settings().onTextZoomIn, Qt.Key_0: Settings().onTextZoomReset, Qt.Key_Home: self.onFirstChar, Qt.Key_End: self.onLastChar, Qt.Key_B: self.highlightInOutline, Qt.Key_QuoteLeft: self.highlightInCFlow}, ALT: {Qt.Key_U: self.onScopeBegin}, CTRL_KEYPAD: {Qt.Key_Minus: Settings().onTextZoomOut, Qt.Key_Plus: Settings().onTextZoomIn, Qt.Key_0: Settings().onTextZoomReset}, NO_MODIFIER: {Qt.Key_Home: self.onHome, Qt.Key_End: self.moveToLineEnd, Qt.Key_F12: self.makeLineFirst}} # Not all the derived classes need certain tool functionality if hasattr(self._parent, "getType"): widgetType = self._parent.getType() if widgetType in [MainWindowTabWidgetBase.PlainTextEditor]: if hasattr(self._parent, "onOpenImport"): self.__hotKeys[CTRL][Qt.Key_I] = self._parent.onOpenImport if hasattr(self._parent, "onNavigationBar"): self.__hotKeys[NO_MODIFIER][Qt.Key_F2] = \ self._parent.onNavigationBar # Arguments: obj, event def eventFilter(self, _, event): """Event filter to catch shortcuts on UBUNTU""" if event.type() == QEvent.KeyPress: key = event.key() if self.isReadOnly(): if key in [Qt.Key_Delete, Qt.Key_Backspace, Qt.Key_Backtab, Qt.Key_X, Qt.Key_Tab, Qt.Key_Space, Qt.Key_Slash, Qt.Key_Z, Qt.Key_Y]: return True modifiers = int(event.modifiers()) try: self.__hotKeys[modifiers][key]() return True except KeyError: return False except Exception as exc: logging.warning(str(exc)) return False def wheelEvent(self, event): """Mouse wheel event""" if QApplication.keyboardModifiers() == Qt.ControlModifier: angleDelta = event.angleDelta() if not angleDelta.isNull(): if angleDelta.y() > 0: Settings().onTextZoomIn() else: Settings().onTextZoomOut() event.accept() else: QutepartWrapper.wheelEvent(self, event) def focusInEvent(self, event): """Enable Shift+Tab when the focus is received""" if self._parent.shouldAcceptFocus(): QutepartWrapper.focusInEvent(self, event) else: self._parent.setFocus() def focusOutEvent(self, event): """Disable Shift+Tab when the focus is lost""" self.__completer.hide() if not self.__inCompletion: self.__resetCalltip() QutepartWrapper.focusOutEvent(self, event) def updateSettings(self): """Updates the editor settings""" settings = Settings() if settings['verticalEdge']: self.lineLengthEdge = settings['editorEdge'] self.lineLengthEdgeColor = GlobalData().skin['edgeColor'] self.drawSolidEdge = True else: self.lineLengthEdge = None self.drawAnyWhitespace = settings['showSpaces'] self.drawIncorrectIndentation = settings['showSpaces'] if settings['lineWrap']: self.setWordWrapMode(QTextOption.WrapAnywhere) else: self.setWordWrapMode(QTextOption.NoWrap) if hasattr(self._parent, "getNavigationBar"): navBar = self._parent.getNavigationBar() if navBar: navBar.updateSettings() def __initMargins(self, debugger): """Initializes the editor margins""" self.addMargin(CDMLineNumberMargin(self)) self.addMargin(CDMFlakesMargin(self)) self.getMargin('cdm_flakes_margin').setVisible(False) if debugger: self.addMargin(CDMBreakpointMargin(self, debugger)) self.getMargin('cdm_bpoint_margin').setVisible(False) def highlightCurrentDebuggerLine(self, line, asException): """Highlights the current debugger line""" margin = self.getMargin('cdm_flakes_margin') if margin: if asException: margin.setExceptionLine(line) else: margin.setCurrentDebugLine(line) def clearCurrentDebuggerLine(self): """Removes the current debugger line marker""" margin = self.getMargin('cdm_flakes_margin') if margin: margin.clearDebugMarks() def readFile(self, fileName): """Reads the text from a file""" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: content, self.encoding = readEncodedFile(fileName) self.eol = detectEolString(content) # Copied from enki (enki/core/document.py: _readFile()): # Strip last EOL. Qutepart adds it when saving file if content.endswith('\r\n'): content = content[:-2] elif content.endswith('\n') or content.endswith('\r'): content = content[:-1] self.text = content self.mime, _, xmlSyntaxFile = getFileProperties(fileName) if xmlSyntaxFile: self.detectSyntax(xmlSyntaxFile) self.document().setModified(False) except: QApplication.restoreOverrideCursor() raise QApplication.restoreOverrideCursor() def writeFile(self, fileName): """Writes the text to a file""" QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) if Settings()['removeTrailingOnSave']: self.removeTrailingWhitespaces() try: encoding = detectWriteEncoding(self, fileName) if encoding is None: QApplication.restoreOverrideCursor() logging.error('Could not detect write encoding for ' + fileName) return False writeEncodedFile(fileName, self.textForSaving(), encoding) except Exception as exc: logging.error(str(exc)) QApplication.restoreOverrideCursor() return False self.encoding = encoding if self.explicitUserEncoding: userEncoding = getFileEncoding(fileName) if userEncoding != self.explicitUserEncoding: setFileEncoding(fileName, self.explicitUserEncoding) self.explicitUserEncoding = None self._parent.updateModificationTime(fileName) self._parent.setReloadDialogShown(False) QApplication.restoreOverrideCursor() return True def setReadOnly(self, mode): """Overridden version""" QPlainTextEdit.setReadOnly(self, mode) if mode: # Otherwise the cursor is suppressed in the RO mode self.setTextInteractionFlags(self.textInteractionFlags() | Qt.TextSelectableByKeyboard) self.increaseIndentAction.setEnabled(not mode) self.decreaseIndentAction.setEnabled(not mode) self.autoIndentLineAction.setEnabled(not mode) self.indentWithSpaceAction.setEnabled(not mode) self.unIndentWithSpaceAction.setEnabled(not mode) self.undoAction.setEnabled(not mode) self.redoAction.setEnabled(not mode) self.moveLineUpAction.setEnabled(not mode) self.moveLineDownAction.setEnabled(not mode) self.deleteLineAction.setEnabled(not mode) self.pasteLineAction.setEnabled(not mode) self.cutLineAction.setEnabled(not mode) self.duplicateLineAction.setEnabled(not mode) def keyPressEvent(self, event): """Handles the key press events""" key = event.key() if self.isReadOnly(): # Space scrolls # Ctrl+X/Shift+Del/Alt+D/Alt+X deletes something if key in [Qt.Key_Delete, Qt.Key_Backspace, Qt.Key_Space, Qt.Key_X, Qt.Key_Tab, Qt.Key_Z, Qt.Key_Y]: return # Qutepart has its own handler and lets to insert new lines when # ENTER is clicked, so use the QPlainTextEdit return QPlainTextEdit.keyPressEvent(self, event) self.__skipChangeCursor = True if self.__completer.isVisible(): self.__skipChangeCursor = False if key == Qt.Key_Escape: self.__completer.hide() self.setFocus() return # There could be backspace or printed characters only QutepartWrapper.keyPressEvent(self, event) QApplication.processEvents() if key == Qt.Key_Backspace: if self.__completionPrefix == '': self.__completer.hide() self.setFocus() else: self.__completionPrefix = self.__completionPrefix[:-1] self.__completer.setPrefix(self.__completionPrefix) else: self.__completionPrefix += event.text() self.__completer.setPrefix(self.__completionPrefix) if self.__completer.completionCount() == 0: self.__completer.hide() self.setFocus() elif key in [Qt.Key_Enter, Qt.Key_Return]: QApplication.processEvents() line, _ = self.cursorPosition QutepartWrapper.keyPressEvent(self, event) QApplication.processEvents() if line == self.__openedLine: self.lines[line] = '' # If the new line has one or more spaces then it is a candidate for # automatic trimming line, pos = self.cursorPosition text = self.lines[line] self.__openedLine = None if pos > 0 and len(text.strip()) == 0: self.__openedLine = line elif key in [Qt.Key_Up, Qt.Key_PageUp, Qt.Key_Down, Qt.Key_PageDown]: line, _ = self.cursorPosition lineToTrim = line if line == self.__openedLine else None QutepartWrapper.keyPressEvent(self, event) QApplication.processEvents() if lineToTrim is not None: line, _ = self.cursorPosition if line != lineToTrim: # The cursor was really moved to another line self.lines[lineToTrim] = '' self.__openedLine = None elif key == Qt.Key_Escape: self.__resetCalltip() self.sigEscapePressed.emit() event.accept() elif key == Qt.Key_Tab: if self.selectedText: QutepartWrapper.keyPressEvent(self, event) self.__lastTabPosition = None else: line, pos = self.cursorPosition currentPosition = self.absCursorPosition if pos != 0: char = self.lines[line][pos - 1] if char not in [' ', ':', '{', '}', '[', ']', ',', '<', '>', '+', '!', ')'] and \ currentPosition != self.__lastTabPosition: self.__lastTabPosition = currentPosition self.onAutoComplete() event.accept() else: QutepartWrapper.keyPressEvent(self, event) self.__lastTabPosition = currentPosition else: QutepartWrapper.keyPressEvent(self, event) self.__lastTabPosition = currentPosition elif key == Qt.Key_Z and \ int(event.modifiers()) == (Qt.ControlModifier + Qt.ShiftModifier): event.accept() elif key == Qt.Key_ParenLeft: if Settings()['editorCalltips']: QutepartWrapper.keyPressEvent(self, event) self.onShowCalltip(False) else: QutepartWrapper.keyPressEvent(self, event) else: # Special keyboard keys are delivered as 0 values if key != 0: self.__openedLine = None QutepartWrapper.keyPressEvent(self, event) self.__skipChangeCursor = False def _onCursorPositionChanged(self): """Triggered when the cursor changed the position""" self.__lastTabPosition = None line, _ = self.cursorPosition if self.__calltip: if self.__calltipTimer.isActive(): self.__calltipTimer.stop() if self.absCursorPosition < self.__callPosition: self.__resetCalltip() else: self.__calltipTimer.start(500) if not self.__skipChangeCursor: if line == self.__openedLine: self.__openedLine = None return if self.__openedLine is not None: self.__skipChangeCursor = True self.lines[self.__openedLine] = '' self.__skipChangeCursor = False self.__openedLine = None def getCurrentPosFont(self): """Provides the font of the current character""" if self.lexer_ is not None: font = self.lexer_.font(self.styleAt(self.currentPosition())) else: font = self.font() font.setPointSize(font.pointSize() + self.getZoom()) return font def onCommentUncomment(self): """Triggered when Ctrl+M is received""" if self.isReadOnly() or not self.isPythonBuffer(): return with self: # Detect what we need - comment or uncomment line, _ = self.cursorPosition txt = self.lines[line] nonSpaceIndex = self.firstNonSpaceIndex(txt) if self.isCommentLine(line): # need to uncomment if nonSpaceIndex == len(txt) - 1: # Strip the only '#' character stripCount = 1 else: # Strip up to two characters if the next char is a ' ' if txt[nonSpaceIndex + 1] == ' ': stripCount = 2 else: stripCount = 1 newTxt = txt[:nonSpaceIndex] + txt[nonSpaceIndex + stripCount:] if not newTxt.strip(): newTxt = '' self.lines[line] = newTxt else: # need to comment if nonSpaceIndex is None: self.lines[line] = '# ' else: newTxt = '# '.join((txt[:nonSpaceIndex], txt[nonSpaceIndex:])) self.lines[line] = newTxt # Jump to the beginning of the next line if line + 1 < len(self.lines): line += 1 self.cursorPosition = line, 0 self.ensureLineOnScreen(line) def onAutoComplete(self): """Triggered when ctrl+space or TAB is clicked""" if self.isReadOnly(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) self.__inCompletion = True self.__completionPrefix = self.getWordBeforeCursor() words = getCompletionList(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if len(words) == 0: self.setFocus() self.__inCompletion = False return self.__completer.setWordsList(words, self.font()) self.__completer.setPrefix(self.__completionPrefix) count = self.__completer.completionCount() if count == 0: self.setFocus() self.__inCompletion = False return # Make sure the line is visible line, _ = self.cursorPosition self.ensureLineOnScreen(line + 1) # Remove the selection as it could be interpreted not as expected if self.selectedText: self.clearSelection() if count == 1: self.insertCompletion(self.__completer.currentCompletion()) else: cRectangle = self.cursorRect() cRectangle.setLeft(cRectangle.left() + self.viewport().x()) cRectangle.setTop(cRectangle.top() + self.viewport().y() + 2) self.__completer.complete(cRectangle) # If something was selected then the next tab does not need to # bring the completion again. This is covered in the # insertCompletion() method. Here we reset the last tab position # preliminary because it is unknown if something will be inserted. self.__lastTabPosition = None self.__inCompletion = False def onTagHelp(self): """Provides help for an item if available""" if not self.isPythonBuffer(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) definitions = getDefinitions(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() parts = [] for definition in definitions: header = 'Type: ' + definition[3] if definition[5]: header += '\nModule: ' + definition[5] parts.append(header + '\n\n' + definition[4]) if parts: QToolTip.showText(self.mapToGlobal(self.cursorRect().bottomLeft()), '<pre>' + '\n\n'.join(parts) + '</pre>') else: GlobalData().mainWindow.showStatusBarMessage( "Definition is not found") def makeLineFirst(self): """Make the cursor line the first on the screen""" currentLine, _ = self.cursorPosition self.setFirstVisibleLine(currentLine) def onJumpToTop(self): """Jumps to the first position of the first visible line""" self.cursorPosition = self.firstVisibleLine(), 0 def onJumpToMiddle(self): """Jumps to the first line pos in a middle of the editing area""" # Count the number of the visible line count = 0 firstVisible = self.firstVisibleLine() lastVisible = self.lastVisibleLine() candidate = firstVisible while candidate <= lastVisible: if self.isLineVisible(candidate): count += 1 candidate += 1 shift = int(count / 2) jumpTo = firstVisible while shift > 0: if self.isLineVisible(jumpTo): shift -= 1 jumpTo += 1 self.cursorPosition = jumpTo, 0 def onJumpToBottom(self): """Jumps to the first position of the last line""" currentFirstVisible = self.firstVisibleLine() self.cursorPosition = self.lastVisibleLine(), 0 safeLastVisible = self.lastVisibleLine() while self.firstVisibleLine() != currentFirstVisible: # Here: a partially visible last line caused scrolling. So the # cursor needs to be set to the previous visible line self.cursorPosition = currentFirstVisible, 0 safeLastVisible -= 1 while not self.isLineVisible(safeLastVisible): safeLastVisible -= 1 self.cursorPosition = safeLastVisible, 0 def onGotoDefinition(self): """The user requested a jump to definition""" if not self.isPythonBuffer(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) definitions = getDefinitions(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if definitions: if len(definitions) == 1: GlobalData().mainWindow.openFile( definitions[0][0], definitions[0][1], definitions[0][2] + 1) else: if hasattr(self._parent, "importsBar"): self._parent.importsBar.showDefinitions(definitions) else: GlobalData().mainWindow.showStatusBarMessage( "Definition is not found") def onScopeBegin(self): """The user requested jumping to the current scope begin""" if self.isPythonBuffer(): info = getBriefModuleInfoFromMemory(self.text) context = getContext(self, info, True) if context.getScope() != context.GlobalScope: GlobalData().mainWindow.jumpToLine(context.getLastScopeLine()) return def onShowCalltip(self, showMessage=True): """The user requested show calltip""" if self.__calltip is not None: self.__resetCalltip() return if not self.isPythonBuffer(): return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) signatures = getCallSignatures(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if not signatures: if showMessage: GlobalData().mainWindow.showStatusBarMessage( "No calltip found") return # For the time being let's take only the first signature... calltipParams = [] for param in signatures[0].params: calltipParams.append(param.description[len(param.type) + 1:]) calltip = signatures[0].name + '(' + ', '.join(calltipParams) + ')' self.__calltip = Calltip(self) self.__calltip.showCalltip(calltip, signatures[0].index) line = signatures[0].bracket_start[0] column = signatures[0].bracket_start[1] self.__callPosition = self.mapToAbsPosition(line - 1, column) def __resetCalltip(self): """Hides the calltip and resets how it was shown""" self.__calltipTimer.stop() if self.__calltip is not None: self.__calltip.hide() self.__calltip = None self.__callPosition = None def resizeCalltip(self): """Resizes the calltip if so""" if self.__calltip: self.__calltip.resize() def __onCalltipTimer(self): """Handles the calltip update timer""" if self.__calltip: if self.absCursorPosition < self.__callPosition: self.__resetCalltip() return QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) signatures = getCallSignatures(self, self._parent.getFileName()) QApplication.restoreOverrideCursor() if not signatures: self.__resetCalltip() return line = signatures[0].bracket_start[0] column = signatures[0].bracket_start[1] callPosition = self.mapToAbsPosition(line - 1, column) if callPosition != self.__callPosition: self.__resetCalltip() else: # It is still the same call, check the commas self.__calltip.highlightParameter(signatures[0].index) def onOccurences(self): """The user requested a list of occurences""" if not self.isPythonBuffer(): return if self._parent.getType() == MainWindowTabWidgetBase.VCSAnnotateViewer: return if not os.path.isabs(self._parent.getFileName()): GlobalData().mainWindow.showStatusBarMessage( "Please save the buffer and try again") return fileName = self._parent.getFileName() QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) definitions = getOccurrences(self, fileName) QApplication.restoreOverrideCursor() if len(definitions) == 0: GlobalData().mainWindow.showStatusBarMessage('No occurences found') return # There are found items GlobalData().mainWindow.showStatusBarMessage('') result = [] for definition in definitions: fName = definition.module_path if not fName: fName = fileName lineno = definition.line index = getSearchItemIndex(result, fName) if index < 0: widget = GlobalData().mainWindow.getWidgetForFileName(fName) if widget is None: uuid = "" else: uuid = widget.getUUID() newItem = ItemToSearchIn(fName, uuid) result.append(newItem) index = len(result) - 1 result[index].addMatch(definition.name, lineno) GlobalData().mainWindow.displayFindInFiles('', result) def insertCompletion(self, text): """Triggered when a completion is selected""" if text: currentWord = self.getCurrentWord() line, pos = self.cursorPosition prefixLength = len(self.__completionPrefix) if text != currentWord and text != self.__completionPrefix: with self: lineContent = self.lines[line] leftPart = lineContent[0:pos - prefixLength] rightPart = lineContent[pos:] self.lines[line] = leftPart + text + rightPart newPos = pos + len(text) - prefixLength self.cursorPosition = line, newPos self.__completionPrefix = '' self.__completer.hide() # The next time there is nothing to insert for sure self.__lastTabPosition = self.absCursorPosition def insertLines(self, text, line): """Inserts the given text into new lines starting from 1-based line""" toInsert = text.splitlines() with self: if line > 0: line -= 1 for item in toInsert: self.lines.insert(line, item) line += 1 def hideCompleter(self): """Hides the completer if visible""" self.__completer.hide() def clearPyflakesMessages(self): """Clears all the pyflakes markers""" self.getMargin('cdm_flakes_margin').clearPyflakesMessages() def setPyflakesMessages(self, messages): """Shows up a pyflakes messages""" self.getMargin('cdm_flakes_margin').setPyflakesMessages(messages) def highlightInCFlow(self): """Triggered when highlight in the control flow is requested""" if self.isPythonBuffer(): line, pos = self.cursorPosition absPos = self.absCursorPosition self.sigCFlowSyncRequested.emit(absPos, line + 1, pos + 1) def setDebugMode(self, debugOn, disableEditing): """Called to switch between debug/development""" skin = GlobalData().skin if debugOn: if disableEditing: self.setLinenoMarginBackgroundColor(skin['marginPaperDebug']) self.setLinenoMarginForegroundColor(skin['marginColorDebug']) self.setReadOnly(True) else: self.setLinenoMarginBackgroundColor(skin['marginPaper']) self.setLinenoMarginForegroundColor(skin['marginColor']) self.setReadOnly(False) bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: bpointMargin.setDebugMode(debugOn, disableEditing) def restoreBreakpoints(self): """Restores the breakpoints""" bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: bpointMargin.restoreBreakpoints() def isLineBreakable(self): """True if a line is breakable""" bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: return bpointMargin.isLineBreakable() return False def validateBreakpoints(self): """Checks breakpoints and deletes those which are invalid""" bpointMargin = self.getMargin('cdm_bpoint_margin') if bpointMargin: bpointMargin.validateBreakpoints() def isPythonBuffer(self): """True if it is a python buffer""" return isPythonMime(self.mime) def setLinenoMarginBackgroundColor(self, color): """Sets the margins background""" linenoMargin = self.getMargin('cdm_line_number_margin') if linenoMargin: linenoMargin.setBackgroundColor(color) def setLinenoMarginForegroundColor(self, color): """Sets the lineno margin foreground color""" linenoMargin = self.getMargin('cdm_line_number_margin') if linenoMargin: linenoMargin.setForegroundColor(color) def terminate(self): """Overloaded version to pass the request to margins too""" for margin in self.getMargins(): if hasattr(margin, 'onClose'): margin.onClose() QutepartWrapper.terminate(self) def resizeEvent(self, event): """Resize the parent panels if required""" QutepartWrapper.resizeEvent(self, event) self.hideCompleter() if hasattr(self._parent, 'resizeBars'): self._parent.resizeBars()
class MDWidget(QWidget): """The MD rendered content widget which goes along with the text editor""" def __init__(self, editor, parent): QWidget.__init__(self, parent) self.setVisible(False) self.__editor = editor self.__parentWidget = parent self.__connected = False hLayout = QHBoxLayout() hLayout.setContentsMargins(0, 0, 0, 0) hLayout.setSpacing(0) vLayout = QVBoxLayout() vLayout.setContentsMargins(0, 0, 0, 0) vLayout.setSpacing(0) # Make pylint happy self.__toolbar = None self.__topBar = None # Create the update timer self.__updateTimer = QTimer(self) self.__updateTimer.setSingleShot(True) self.__updateTimer.timeout.connect(self.process) vLayout.addWidget(self.__createTopBar()) vLayout.addWidget(self.__createMDView()) hLayout.addLayout(vLayout) hLayout.addWidget(self.__createToolbar()) self.setLayout(hLayout) # Connect to the change file type signal self.__mainWindow = GlobalData().mainWindow editorsManager = self.__mainWindow.editorsManagerWidget.editorsManager editorsManager.sigFileTypeChanged.connect(self.__onFileTypeChanged) def __createToolbar(self): """Creates the toolbar""" self.__toolbar = QToolBar(self) self.__toolbar.setOrientation(Qt.Vertical) self.__toolbar.setMovable(False) self.__toolbar.setAllowedAreas(Qt.RightToolBarArea) self.__toolbar.setIconSize(QSize(16, 16)) self.__toolbar.setFixedWidth(30) self.__toolbar.setContentsMargins(0, 0, 0, 0) # Some control buttons could be added later return self.__toolbar def __createTopBar(self): """Creates the top bar""" self.__topBar = MDTopBar(self) return self.__topBar def __createMDView(self): """Creates the graphics view""" self.mdView = MDViewer(self) return self.mdView def process(self): """Parses the content and displays the results""" if not self.__connected: self.__connectEditorSignals() renderedText, errors, warnings = renderMarkdown(self.__editor.text) if errors: self.__topBar.updateInfoIcon(self.__topBar.STATE_BROKEN_UTD) self.__topBar.setErrors(errors) return if renderedText is None: self.__topBar.updateInfoIcon(self.__topBar.STATE_BROKEN_UTD) self.__topBar.setErrors(['Unknown morkdown rendering error']) return # That will clear the error tooltip as well self.__topBar.updateInfoIcon(self.__topBar.STATE_OK_UTD) if warnings: self.__topBar.setWarnings(warnings) else: self.__topBar.clearWarnings() hsbValue, vsbValue = self.getScrollbarPositions() self.mdView.setHtml(renderedText) self.setScrollbarPositions(hsbValue, vsbValue) def __onFileTypeChanged(self, fileName, uuid, newFileType): """Triggered when a buffer content type has changed""" if self.__parentWidget.getUUID() != uuid: return if not isMarkdownMime(newFileType): self.__disconnectEditorSignals() self.__updateTimer.stop() self.setVisible(False) self.__topBar.updateInfoIcon(self.__topBar.STATE_UNKNOWN) return # Update the bar and show it self.setVisible(True) self.process() # The buffer type change event comes when the content is loaded first # time. So this is a good point to restore the position _, _, _, hPos, vPos = getFilePosition(fileName) self.setScrollbarPositions(hPos, vPos) def __connectEditorSignals(self): """When it is a python file - connect to the editor signals""" if not self.__connected: self.__editor.cursorPositionChanged.connect( self.__cursorPositionChanged) self.__editor.textChanged.connect(self.__onBufferChanged) self.__connected = True def __disconnectEditorSignals(self): """Disconnect the editor signals when the file is not a python one""" if self.__connected: self.__editor.cursorPositionChanged.disconnect( self.__cursorPositionChanged) self.__editor.textChanged.disconnect(self.__onBufferChanged) self.__connected = False def __cursorPositionChanged(self): """Cursor position changed""" # The timer should be reset only in case if the redrawing was delayed if self.__updateTimer.isActive(): self.__updateTimer.stop() self.__updateTimer.start(IDLE_TIMEOUT) def __onBufferChanged(self): """Triggered to update status icon and to restart the timer""" self.__updateTimer.stop() if self.__topBar.getCurrentState() in [ self.__topBar.STATE_OK_UTD, self.__topBar.STATE_OK_CHN, self.__topBar.STATE_UNKNOWN ]: self.__topBar.updateInfoIcon(self.__topBar.STATE_OK_CHN) else: self.__topBar.updateInfoIcon(self.__topBar.STATE_BROKEN_CHN) self.__updateTimer.start(IDLE_TIMEOUT) def redrawNow(self): """Redraw the diagram regardless of the timer""" if self.__updateTimer.isActive(): self.__updateTimer.stop() self.process() def getScrollbarPositions(self): """Provides the scrollbar positions""" hScrollBar = self.mdView.horizontalScrollBar() hsbValue = hScrollBar.value() if hScrollBar else 0 vScrollBar = self.mdView.verticalScrollBar() vsbValue = vScrollBar.value() if vScrollBar else 0 return hsbValue, vsbValue def setScrollbarPositions(self, hPos, vPos): """Sets the scrollbar positions for the view""" hsb = self.mdView.horizontalScrollBar() if hsb: hsb.setValue(hPos) vsb = self.mdView.verticalScrollBar() if vsb: vsb.setValue(vPos) def getFileName(self): return self.__parentWidget.getFileName()
class RunManager(QObject): """Manages the external running processes""" # script path, output file, start time, finish time, redirected sigProfilingResults = pyqtSignal(str, str, str, str, bool) sigDebugSessionPrologueStarted = pyqtSignal(object, str, object, object) sigIncomingMessage = pyqtSignal(str, str, object) sigProcessFinished = pyqtSignal(str, int) def __init__(self, mainWindow): QObject.__init__(self) self.__mainWindow = mainWindow self.__processes = [] self.__prologueProcesses = [] self.__tcpServer = QTcpServer() self.__tcpServer.newConnection.connect(self.__newConnection) self.__tcpServer.listen(QHostAddress.LocalHost) self.__waitTimer = QTimer(self) self.__waitTimer.setSingleShot(True) self.__waitTimer.timeout.connect(self.__onWaitTimer) self.__prologueTimer = QTimer(self) self.__prologueTimer.setSingleShot(True) self.__prologueTimer.timeout.connect(self.__onPrologueTimer) def __newConnection(self): """Handles new incoming connections""" clientSocket = self.__tcpServer.nextPendingConnection() clientSocket.setSocketOption(QAbstractSocket.KeepAliveOption, 1) clientSocket.setSocketOption(QAbstractSocket.LowDelayOption, 1) QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) try: self.__waitForHandshake(clientSocket) except: QApplication.restoreOverrideCursor() raise QApplication.restoreOverrideCursor() def __waitForHandshake(self, clientSocket): """Waits for the message with the proc ID""" if clientSocket.waitForReadyRead(1000): try: method, procuuid, params, jsonStr = getParsedJSONMessage( clientSocket) if IDE_DEBUG: print("Run manager (wait for handshake) received: " + str(jsonStr)) if method != METHOD_PROC_ID_INFO: logging.error('Unexpected message at the handshake stage. ' 'Expected: ' + METHOD_PROC_ID_INFO + '. Received: ' + str(method)) self.__safeSocketClose(clientSocket) return None except (TypeError, ValueError) as exc: self.__mainWindow.showStatusBarMessage( 'Unsolicited connection to the RunManager. Ignoring...') self.__safeSocketClose(clientSocket) return None procIndex = self.__getProcessIndex(procuuid) if procIndex is not None: self.__onProcessStarted(procuuid) self.__processes[procIndex].procWrapper.setSocket(clientSocket) return params @staticmethod def __safeSocketClose(clientSocket): """No exception socket close""" try: clientSocket.close() except Exception as exc: logging.error('Run manager safe socket close: ' + str(exc)) def __pickWidget(self, procuuid, kind): """Picks the widget for a process""" consoleReuse = Settings()['ioconsolereuse'] if consoleReuse == NO_REUSE: widget = IOConsoleWidget(procuuid, kind) self.__mainWindow.addIOConsole(widget, kind) return widget widget = None consoles = self.__mainWindow.getIOConsoles() for console in consoles: if console.kind == kind: procIndex = self.__getProcessIndex(console.procuuid) if procIndex is None: widget = console widget.onReuse(procuuid) self.__mainWindow.onReuseConsole(widget, kind) if consoleReuse == CLEAR_AND_REUSE: widget.clear() break if widget is None: widget = IOConsoleWidget(procuuid, kind) self.__mainWindow.addIOConsole(widget, kind) return widget def __updateParameters(self, path, kind): """Displays the dialog and updates the parameters if needed""" params = getRunParameters(path) profilerParams = None debuggerParams = None if kind == PROFILE: profilerParams = Settings().getProfilerSettings() elif kind == DEBUG: debuggerParams = Settings().getDebuggerSettings() dlg = RunDialog(path, params, profilerParams, debuggerParams, kind, self.__mainWindow) if dlg.exec_() == QDialog.Accepted: addRunParams(path, dlg.runParams) if kind == PROFILE: if dlg.profilerParams != profilerParams: Settings().setProfilerSettings(dlg.profilerParams) elif kind == DEBUG: if dlg.debuggerParams != debuggerParams: Settings().setDebuggerSettings(dlg.debuggerParams) return True return False def __prepareRemoteProcess(self, path, kind): """Prepares the data structures to start a remote proces""" redirected = getRunParameters(path)['redirected'] remoteProc = RemoteProcess() remoteProc.kind = kind remoteProc.procWrapper = RemoteProcessWrapper( path, self.__tcpServer.serverPort(), redirected, kind) remoteProc.procWrapper.state = STATE_PROLOGUE if redirected or kind == DEBUG: self.__prologueProcesses.append( (remoteProc.procWrapper.procuuid, time.time())) if not self.__prologueTimer.isActive(): self.__prologueTimer.start(1000) if redirected: remoteProc.widget = self.__pickWidget( remoteProc.procWrapper.procuuid, kind) remoteProc.widget.appendIDEMessage('Starting script ' + path + '...') remoteProc.procWrapper.sigClientStdout.connect( remoteProc.widget.appendStdoutMessage) remoteProc.procWrapper.sigClientStderr.connect( remoteProc.widget.appendStderrMessage) remoteProc.procWrapper.sigClientInput.connect( remoteProc.widget.input) remoteProc.widget.sigUserInput.connect(self.__onUserInput) remoteProc.procWrapper.sigFinished.connect(self.__onProcessFinished) remoteProc.procWrapper.sigIncomingMessage.connect( self.__onIncomingMessage) self.__processes.append(remoteProc) return remoteProc def run(self, path, needDialog): """Runs the given script regardless if it is redirected""" if needDialog: if not self.__updateParameters(path, RUN): return remoteProc = self.__prepareRemoteProcess(path, RUN) try: remoteProc.procWrapper.start() if not remoteProc.procWrapper.redirected: remoteProc.procWrapper.startTime = datetime.now() if not self.__waitTimer.isActive(): self.__waitTimer.start(1000) except Exception as exc: self.__onProcessFinished(remoteProc.procWrapper.procuuid, FAILED_TO_START) logging.error(str(exc)) def profile(self, path, needDialog): """Profiles the given script regardless if it is redirected""" if needDialog: if not self.__updateParameters(path, PROFILE): return remoteProc = self.__prepareRemoteProcess(path, PROFILE) try: remoteProc.procWrapper.start() if not remoteProc.procWrapper.redirected: remoteProc.procWrapper.startTime = datetime.now() if not self.__waitTimer.isActive(): self.__waitTimer.start(1000) except Exception as exc: self.__onProcessFinished(remoteProc.procWrapper.procuuid, FAILED_TO_START) logging.error(str(exc)) def debug(self, path, needDialog): """Debugs the given script regardless if it is redirected""" if needDialog: if not self.__updateParameters(path, DEBUG): return remoteProc = self.__prepareRemoteProcess(path, DEBUG) # The run parameters could be changed by another run after the # debugging has started so they need to be saved per session self.sigDebugSessionPrologueStarted.emit( remoteProc.procWrapper, path, getRunParameters(path), Settings().getDebuggerSettings()) try: remoteProc.procWrapper.start() if not remoteProc.procWrapper.redirected: remoteProc.procWrapper.startTime = datetime.now() if not self.__waitTimer.isActive(): self.__waitTimer.start(1000) except Exception as exc: self.__onProcessFinished(remoteProc.procWrapper.procuuid, FAILED_TO_START) logging.error(str(exc)) def killAll(self): """Kills all the processes if needed""" index = len(self.__processes) - 1 while index >= 0: item = self.__processes[index] if item.procWrapper.redirected: item.procWrapper.stop() index -= 1 # Wait till all the processes stopped count = self.__getDetachedCount() while count > 0: time.sleep(0.01) QApplication.processEvents() count = self.__getDetachedCount() def __getDetachedCount(self): """Return the number of detached processes still running""" count = 0 index = len(self.__processes) - 1 while index >= 0: if self.__processes[index].procWrapper.redirected: count += 1 index -= 1 return count def kill(self, procuuid): """Kills a single process""" index = self.__getProcessIndex(procuuid) if index is None: return item = self.__processes[index] if not item.procWrapper.redirected: return item.procWrapper.stop() def __getProcessIndex(self, procuuid): """Returns a process index in the list""" for index, item in enumerate(self.__processes): if item.procWrapper.procuuid == procuuid: return index return None def __onProcessFinished(self, procuuid, retCode): """Triggered when a redirected process has finished""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] item.procWrapper.finishTime = datetime.now() needProfileSignal = False if retCode == KILLED: msg = "Script killed" tooltip = "killed" elif retCode == DISCONNECTED: msg = "Connection lost to the script process" tooltip = "connection lost" elif retCode == FAILED_TO_START: msg = "Script failed to start" tooltip = "failed to start" elif retCode == STOPPED_BY_REQUEST: # Debugging only: user clicked 'stop' msg = "Script finished by the user request" tooltip = "stopped by user" item.procWrapper.wait() elif retCode == UNHANDLED_EXCEPTION: # Debugging only: unhandled exception msg = "Script finished due to an unhandled exception" tooltip = "unhandled exception" item.procWrapper.wait() elif retCode == SYNTAX_ERROR_AT_START: msg = "Failure to run due to a syntax error" tooltip = "syntax error" item.procWrapper.wait() else: msg = "Script finished with exit code " + str(retCode) tooltip = "finished, exit code " + str(retCode) item.procWrapper.wait() if item.kind == PROFILE: needProfileSignal = True if item.widget: item.widget.scriptFinished() item.widget.appendIDEMessage(msg) self.__mainWindow.updateIOConsoleTooltip(procuuid, tooltip) self.__mainWindow.onConsoleFinished(item.widget) item.widget.sigUserInput.disconnect(self.__onUserInput) if needProfileSignal: self.__sendProfileResultsSignal(item.procWrapper) del self.__processes[index] self.sigProcessFinished.emit(procuuid, retCode) def __onProcessStarted(self, procuuid): """Triggered when a process has started""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] if item.widget: msg = item.widget.appendIDEMessage('Script started') item.procWrapper.startTime = msg.timestamp def __onUserInput(self, procuuid, userInput): """Triggered when the user input is collected""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] if item.procWrapper.redirected: item.procWrapper.userInput(userInput) def __onWaitTimer(self): """Triggered when the timer fired""" needNewTimer = False index = len(self.__processes) - 1 while index >= 0: item = self.__processes[index] if not item.procWrapper.redirected: if item.procWrapper.waitDetached(): item.procWrapper.finishTime = datetime.now() if item.procWrapper.kind == PROFILE: self.__sendProfileResultsSignal(item.procWrapper) del self.__processes[index] else: needNewTimer = True index -= 1 if needNewTimer: self.__waitTimer.start(1000) def __sendProfileResultsSignal(self, procWrapper): """Sends a signal to tell that the results are available""" self.sigProfilingResults.emit( procWrapper.path, GlobalData().getProfileOutputPath(procWrapper.procuuid), printableTimestamp(procWrapper.startTime), printableTimestamp(procWrapper.finishTime), procWrapper.redirected) def __onIncomingMessage(self, procuuid, method, params): """Handles a debugger incoming message""" if IDE_DEBUG: print('Debugger message from ' + procuuid + ' Method: ' + method + ' Prameters: ' + repr(params)) self.sigIncomingMessage.emit(procuuid, method, params) def __onPrologueTimer(self): """Triggered when a prologue phase controlling timer fired""" needNewTimer = False index = len(self.__prologueProcesses) - 1 while index >= 0: procuuid, startTime = self.__prologueProcesses[index] procIndex = self.__getProcessIndex(procuuid) if procIndex is None: # No such process anymore del self.__prologueProcesses[index] else: item = self.__processes[procIndex] if item.procWrapper.state != STATE_PROLOGUE: # The state has been changed del self.__prologueProcesses[index] else: if time.time() - startTime > HANDSHAKE_TIMEOUT: # Waited too long item.widget.appendIDEMessage( 'Timeout: the process did not start; ' 'killing the process.') item.procWrapper.stop() else: needNewTimer = True index -= 1 if needNewTimer: self.__prologueTimer.start(1000) def appendIDEMessage(self, procuuid, message): """Appends a message to the appropriate IO window""" index = self.__getProcessIndex(procuuid) if index is not None: item = self.__processes[index] if item.widget: item.widget.appendIDEMessage(message) return logging.error(message)