Thu, 23 Aug 2018 17:54:43 +0200
CodeDocumentationViewer: fixed issues related to the non-availability of either QtWebEngine or QtWebKit (e.g. using 32-bit PyQt5 wheel on Windows).
# -*- coding: utf-8 -*- # Copyright (c) 2017 - 2018 Detlev Offenbach <detlev@die-offenbachs.de> # """ Module implementing a widget to show some source code information provided by plug-ins. """ from __future__ import unicode_literals try: basestring # __IGNORE_WARNING__ except NameError: basestring = str from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QTimer from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ QComboBox, QSizePolicy, QLineEdit, QTextEdit, QToolTip, QToolButton, \ QActionGroup, QMenu from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget from E5Gui.E5ToolButton import E5ToolButton import Preferences import UI.PixmapCache from .CodeDocumentationViewerTemplate import \ prepareDocumentationViewerHtmlDocument, \ prepareDocumentationViewerHtmlDocWarningDocument, \ prepareDocumentationViewerHtmlWarningDocument from .data import codeDocumentationViewer_rc # __IGNORE_WARNING__ class PlainTextDocumentationViewer(QWidget): """ Class implementing the plain text documentation viewer. """ def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget @type QWidget """ super(PlainTextDocumentationViewer, self).__init__(parent) self.setObjectName("PlainTextDocumentationViewer") self.__verticalLayout = QVBoxLayout(self) self.__verticalLayout.setObjectName("verticalLayout") self.__verticalLayout.setContentsMargins(0, 0, 0, 0) self.__contents = QTextEdit(self) self.__contents.setTabChangesFocus(True) self.__contents.setReadOnly(True) self.__contents.setLineWrapMode(QTextEdit.NoWrap) self.__contents.setObjectName("contents") self.__verticalLayout.addWidget(self.__contents) self.__searchWidget = E5TextEditSearchWidget(self, False) self.__searchWidget.setFocusPolicy(Qt.WheelFocus) self.__searchWidget.setObjectName("searchWidget") self.__verticalLayout.addWidget(self.__searchWidget) self.__searchWidget.attachTextEdit(self.__contents, "QTextEdit") self.preferencesChanged() def clear(self): """ Public method to clear the contents. """ self.__contents.clear() def setText(self, text): """ Public method to set the text to be shown. @param text text to be shown @type str """ self.__contents.setPlainText(text) def preferencesChanged(self): """ Public slot to handle a change of preferences. """ font = Preferences.getEditorOtherFonts("MonospacedFont") self.__contents.setFontFamily(font.family()) self.__contents.setFontPointSize(font.pointSize()) class WebViewDocumentationViewer(QWidget): """ Class implementing the rich text documentation viewer. """ def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget @type QWidget """ super(WebViewDocumentationViewer, self).__init__(parent) self.setObjectName("WebViewDocumentationViewer") self.__verticalLayout = QVBoxLayout(self) self.__verticalLayout.setObjectName("verticalLayout") self.__verticalLayout.setContentsMargins(0, 0, 0, 0) try: from PyQt5.QtWebEngineWidgets import QWebEngineView, \ QWebEngineSettings self.__contents = QWebEngineView(self) self.__contents.page().linkHovered.connect(self.__showLink) try: self.__contents.settings().setAttribute( QWebEngineSettings.FocusOnNavigationEnabled, False) except AttributeError: # pre Qt 5.8 pass self.__usesWebKit = False except ImportError: from PyQt5.QtWebKitWidgets import QWebPage, QWebView self.__contents = QWebView(self) self.__contents.page().setLinkDelegationPolicy( QWebPage.DelegateAllLinks) self.__usesWebKit = True sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.__contents.sizePolicy().hasHeightForWidth()) self.__contents.setSizePolicy(sizePolicy) self.__contents.setContextMenuPolicy(Qt.NoContextMenu) self.__contents.setUrl(QUrl("about:blank")) self.__verticalLayout.addWidget(self.__contents) self.__searchWidget = E5TextEditSearchWidget(self, False) self.__searchWidget.setFocusPolicy(Qt.WheelFocus) self.__searchWidget.setObjectName("searchWidget") self.__verticalLayout.addWidget(self.__searchWidget) self.__searchWidget.attachTextEdit( self.__contents, "QWebView" if self.__usesWebKit else "QWebEngineView", ) @pyqtSlot(str) def __showLink(self, urlStr): """ Private slot to show the hovered link in a tooltip. @param urlStr hovered URL @type str """ QToolTip.showText(QCursor.pos(), urlStr, self.__contents) def setHtml(self, html): """ Public method to set the HTML text of the widget. @param html HTML text to be shown @type str """ self.__contents.setEnabled(False) self.__contents.setHtml(html) self.__contents.setEnabled(True) def clear(self): """ Public method to clear the shown contents. """ self.__contents.setHtml("") class CodeDocumentationViewer(QWidget): """ Class implementing a widget to show some source code information provided by plug-ins. @signal providerAdded() emitted to indicate the availability of a new provider @signal providerRemoved() emitted to indicate the removal of a provider """ providerAdded = pyqtSignal() providerRemoved = pyqtSignal() def __init__(self, parent=None): """ Constructor @param parent reference to the parent widget @type QWidget """ super(CodeDocumentationViewer, self).__init__(parent) self.__setupUi() self.__ui = parent self.__providers = {} self.__selectedProvider = "" self.__disabledProvider = "disabled" self.__shuttingDown = False self.__startingUp = True self.__lastDocumentation = None self.__requestingEditor = None self.__unregisterTimer = QTimer(self) self.__unregisterTimer.setInterval(30000) # 30 seconds self.__unregisterTimer.setSingleShot(True) self.__unregisterTimer.timeout.connect(self.__unregisterTimerTimeout) self.__mostRecentlyUnregisteredProvider = None def __setupUi(self): """ Private method to generate the UI layout. """ self.setObjectName("CodeDocumentationViewer") self.verticalLayout = QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setContentsMargins(3, 3, 3, 3) # top row 1 of widgets self.horizontalLayout1 = QHBoxLayout() self.horizontalLayout1.setObjectName("horizontalLayout1") self.label = QLabel(self) self.label.setObjectName("label") self.label.setText(self.tr("Code Info Provider:")) self.label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.horizontalLayout1.addWidget(self.label) self.providerComboBox = QComboBox(self) sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.providerComboBox.sizePolicy().hasHeightForWidth()) self.providerComboBox.setSizePolicy(sizePolicy) self.providerComboBox.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.providerComboBox.setObjectName("providerComboBox") self.providerComboBox.setToolTip( self.tr("Select the code info provider")) self.providerComboBox.addItem(self.tr("<disabled>"), "disabled") self.horizontalLayout1.addWidget(self.providerComboBox) # top row 2 of widgets self.horizontalLayout2 = QHBoxLayout() self.horizontalLayout2.setObjectName("horizontalLayout2") self.objectLineEdit = QLineEdit(self) self.objectLineEdit.setReadOnly(True) self.objectLineEdit.setObjectName("objectLineEdit") self.horizontalLayout2.addWidget(self.objectLineEdit) self.verticalLayout.addLayout(self.horizontalLayout1) self.verticalLayout.addLayout(self.horizontalLayout2) # Plain Text Viewer self.__plainTextViewer = PlainTextDocumentationViewer(self) self.__plainTextViewer.setObjectName("__plainTextViewer") self.verticalLayout.addWidget(self.__plainTextViewer) # Rich Text (Web) Viewer try: self.__richTextViewer = WebViewDocumentationViewer(self) self.__richTextViewer.setObjectName("__richTextViewer") self.verticalLayout.addWidget(self.__richTextViewer) except ImportError: # neither QtWebEngineWidgets nor QtWebKitWidgets is available self.__richTextViewer = None self.__toolButton = E5ToolButton(self) self.__toolButton.setObjectName( "navigation_supermenu_button") self.__toolButton.setIcon(UI.PixmapCache.getIcon("superMenu.png")) self.__toolButton.setToolTip(self.tr("Main Menu")) self.__toolButton.setPopupMode(QToolButton.InstantPopup) self.__toolButton.setToolButtonStyle(Qt.ToolButtonIconOnly) self.__toolButton.setFocusPolicy(Qt.NoFocus) self.__toolButton.setAutoRaise(True) self.__toolButton.setShowMenuInside(True) self.__optionsActionGroup = QActionGroup(self) self.__optionsActionGroup.setExclusive(True) self.__optionsMenu = QMenu(self) if self.__richTextViewer: self.__richTextAct = self.__optionsMenu.addAction( self.tr("Rich Text"), lambda: self.__showTextViewer(True)) self.__richTextAct.setCheckable(True) self.__optionsActionGroup.addAction(self.__richTextAct) else: # neither QtWebEngineWidgets nor QtWebKitWidgets is available self.__richTextAct = None self.__plainTextAct = self.__optionsMenu.addAction( self.tr("Plain Text"), lambda: self.__showTextViewer(False)) self.__plainTextAct.setCheckable(True) self.__optionsActionGroup.addAction(self.__plainTextAct) self.__toolButton.setMenu(self.__optionsMenu) self.horizontalLayout2.addWidget(self.__toolButton) self.providerComboBox.currentIndexChanged[int].connect( self.on_providerComboBox_currentIndexChanged) def finalizeSetup(self): """ Public method to finalize the setup of the documentation viewer. """ if self.__richTextViewer is None: showRichText = False else: showRichText = Preferences.getDocuViewer("ShowInfoAsRichText") self.__showTextViewer(showRichText) self.__startingUp = False provider = Preferences.getDocuViewer("Provider") if provider in self.__providers: index = self.providerComboBox.findData(provider) else: index = 0 provider = self.__disabledProvider self.providerComboBox.setCurrentIndex(index) self.__selectedProvider = provider if index == 0: self.__showDisabledMessage() def registerProvider(self, providerName, providerDisplay, provider, supported): """ Public method register a source docu provider. @param providerName name of the provider (must be unique) @type str @param providerDisplay visible name of the provider @type str @param provider function to be called to determine source docu @type function(editor) @param supported function to be called to determine, if a language is supported @type function(language) @exception KeyError raised if a provider with the given name was already registered """ if providerName in self.__providers: raise KeyError( "Provider '{0}' already registered.".format(providerName)) self.__providers[providerName] = (provider, supported) self.providerComboBox.addItem(providerDisplay, providerName) self.providerAdded.emit() if self.__unregisterTimer.isActive(): if providerName == self.__mostRecentlyUnregisteredProvider: # this is assumed to be a plug-in reload self.__unregisterTimer.stop() self.__mostRecentlyUnregisteredProvider = None self.__selectProvider(providerName) def unregisterProvider(self, providerName): """ Public method register a source docu provider. @param providerName name of the provider (must be unique) @type str """ if providerName in self.__providers: if providerName == self.__selectedProvider: self.providerComboBox.setCurrentIndex(0) # in case this is just a temporary unregistration (< 30s) # e.g. when the plug-in is re-installed or updated self.__mostRecentlyUnregisteredProvider = providerName self.__unregisterTimer.start() del self.__providers[providerName] index = self.providerComboBox.findData(providerName) self.providerComboBox.removeItem(index) self.providerRemoved.emit() @pyqtSlot() def __unregisterTimerTimeout(self): """ Private slot handling the timeout signal of the unregister timer. """ self.__mostRecentlyUnregisteredProvider = None def isSupportedLanguage(self, language): """ Public method to check, if the given language is supported by the selected provider. @param language editor programming language to check @type str @return flag indicating the support status @rtype bool """ supported = False if self.__selectedProvider != self.__disabledProvider: supported = self.__providers[self.__selectedProvider][1](language) return supported def getProviders(self): """ Public method to get a list of providers and their visible strings. @return list containing the providers and their visible strings @rtype list of tuple of (str,str) """ providers = [] for index in range(1, self.providerComboBox.count()): provider = self.providerComboBox.itemData(index) text = self.providerComboBox.itemText(index) providers.append((provider, text)) return providers def showInfo(self, editor): """ Public method to request code documentation data from a provider. @param editor reference to the editor to request code docu for @type Editor """ line, index = editor.getCursorPosition() word = editor.getWord(line, index) if not word: # try again one index before word = editor.getWord(line, index - 1) self.objectLineEdit.setText(word) if self.__selectedProvider != self.__disabledProvider: self.__plainTextViewer.clear() if self.__richTextViewer: self.__richTextViewer.clear() self.__providers[self.__selectedProvider][0](editor) def documentationReady(self, documentationInfo, isWarning=False, isDocWarning=False): """ Public method to provide the documentation info to the viewer. If documentationInfo is a dictionary, it should contain these (optional) keys and data: name: the name of the inspected object argspec: its arguments specification note: A phrase describing the type of object (function or method) and the module it belongs to. docstring: its documentation string typ: its type information @param documentationInfo dictionary containing the source docu data @type dict or str @param isWarning flag indicating a warning page @type bool @param isDocWarning flag indicating a documentation warning page @type bool """ self.__ui.activateCodeDocumentationViewer(switchFocus=False) if not isWarning and not isDocWarning: self.__lastDocumentation = documentationInfo if not documentationInfo: if self.__selectedProvider == self.__disabledProvider: self.__showDisabledMessage() else: self.documentationReady(self.tr("No documentation available"), isDocWarning=True) else: if self.__showMarkdown: if isWarning: html = prepareDocumentationViewerHtmlWarningDocument( documentationInfo) elif isDocWarning: html = prepareDocumentationViewerHtmlDocWarningDocument( documentationInfo) elif isinstance(documentationInfo, dict): html = prepareDocumentationViewerHtmlDocument( documentationInfo) else: html = documentationInfo self.__setHtml(html) else: if isinstance(documentationInfo, basestring): fullText = documentationInfo elif isinstance(documentationInfo, dict): name = documentationInfo["name"] if name: title = "".join([name, "\n", "=" * len(name), "\n\n"]) if "argspec" in documentationInfo and \ documentationInfo["argspec"]: definition = self.tr("Definition: {0}{1}\n")\ .format(name, documentationInfo["argspec"]) elif name: definition = self.tr("Definition: {0}\n")\ .format(name) else: definition = "" if "typ" in documentationInfo and \ documentationInfo["typ"]: typeInfo = self.tr("Type: {0}\n").format( documentationInfo["typ"]) else: typeInfo = "" if "note" in documentationInfo and \ documentationInfo["note"]: note = self.tr("Note: {0}\n").format( documentationInfo["note"]) else: note = "" header = "".join([title, definition, typeInfo, note]) else: header = "" if "docstring" not in documentationInfo or \ not documentationInfo["docstring"]: docString = self.tr( "No further documentation available") else: if header: docString = "\n----\n\n{0}".format( documentationInfo["docstring"]) else: docString = documentationInfo["docstring"] fullText = "".join([header, docString]) self.__plainTextViewer.setText(fullText) def __showDisabledMessage(self): """ Private method to show a message giving the reason for being disabled. """ if len(self.__providers) == 0: self.documentationReady( self.tr("No source code documentation provider has been" " registered. This function has been disabled."), isWarning=True) else: self.documentationReady( self.tr("This function has been disabled."), isWarning=True) def __setHtml(self, html): """ Private slot to set the prepared HTML text. @param html prepared HTML text @type str """ if self.__richTextViewer: self.__richTextViewer.setHtml(html) def __setHtmlWarning(self, warningText): """ Private slot to set a display message. @param warningText text to be shown as a warning @type str """ if self.__richTextViewer: html = prepareDocumentationViewerHtmlWarningDocument(warningText) self.__richTextViewer.setHtml(html) @pyqtSlot(int) def on_providerComboBox_currentIndexChanged(self, index): """ Private slot to handle the selection of a provider. @param index index of the selected provider @type int """ if not self.__shuttingDown and not self.__startingUp: self.__plainTextViewer.clear() if self.__richTextViewer: self.__richTextViewer.clear() self.objectLineEdit.clear() provider = self.providerComboBox.itemData(index) if provider == self.__disabledProvider: self.__showDisabledMessage() else: self.__lastDocumentation = None Preferences.setDocuViewer("Provider", provider) self.__selectedProvider = provider def shutdown(self): """ Public method to perform shutdown actions. """ self.__shuttingDown = True Preferences.setDocuViewer("Provider", self.__selectedProvider) def preferencesChanged(self): """ Public slot to handle a change of preferences. """ if self.__richTextViewer is None: showRichText = False else: showRichText = Preferences.getDocuViewer("ShowInfoAsRichText") if showRichText != self.__showMarkdown: self.__showTextViewer(showRichText) provider = Preferences.getDocuViewer("Provider") self.__selectProvider(provider) def __selectProvider(self, provider): """ Private method to select a provider programmatically. @param provider name of the provider to be selected @type str """ if provider != self.__selectedProvider: index = self.providerComboBox.findData(provider) if index < 0: index = 0 self.providerComboBox.setCurrentIndex(index) def __showTextViewer(self, richText): """ Private slot to show the selected viewer. @param richText flag indicating the rich text viewer @type bool """ self.__showMarkdown = richText self.__plainTextViewer.clear() self.__plainTextViewer.setVisible(not richText) if self.__richTextViewer: self.__richTextViewer.clear() self.__richTextViewer.setVisible(richText) self.__plainTextAct.setChecked(not richText) if self.__richTextAct: self.__richTextAct.setChecked(richText) self.documentationReady(self.__lastDocumentation) Preferences.setDocuViewer("ShowInfoAsRichText", richText)