diff -r b6643d36dddd -r 7ab2293917f8 UI/CodeDocumentationViewer.py --- a/UI/CodeDocumentationViewer.py Tue Oct 17 19:40:32 2017 +0200 +++ b/UI/CodeDocumentationViewer.py Wed Oct 18 19:16:28 2017 +0200 @@ -10,13 +10,17 @@ from __future__ import unicode_literals -from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QThread +from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QThread, QUrl +from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \ - QComboBox, QSizePolicy, QLineEdit, QTextEdit + QComboBox, QSizePolicy, QLineEdit, QTextEdit, QToolTip, QToolButton, \ + QActionGroup, QMenu from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget +from E5Gui.E5ToolButton import E5ToolButton import Preferences +import UI.PixmapCache class PlainTextDocumentationViewer(QWidget): @@ -35,6 +39,7 @@ self.__verticalLayout = QVBoxLayout(self) self.__verticalLayout.setObjectName("verticalLayout") + self.__verticalLayout.setContentsMargins(0, 0, 0, 0) self.__contents = QTextEdit(self) self.__contents.setTabChangesFocus(True) @@ -47,7 +52,7 @@ self.__searchWidget.setObjectName("searchWidget") self.__verticalLayout.addWidget(self.__searchWidget) - self.__searchWidget.attachTextEdit(self.__contents) + self.__searchWidget.attachTextEdit(self.__contents, "QTextEdit") self.preferencesChanged() @@ -66,9 +71,6 @@ """ self.__contents.setPlainText(text) - def setHtml(self, html): - self.__contents.setHtml(html) - def preferencesChanged(self): """ Public slot to handle a change of preferences. @@ -77,13 +79,94 @@ 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 + self.__contents = QWebEngineView(self) + self.__contents.page().linkHovered.connect(self.__showLink) + 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) + 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.setHtml(html) + + 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): """ @@ -106,8 +189,6 @@ self.__lastDocumentation = None - self.__showMarkdown = Preferences.getDocuViewer("ShowInfoAsMarkdown") - self.__noDocumentationString = self.tr("No documentation available") self.__disabledString = self.tr( "No source code documentation provider has been registered or" @@ -153,11 +234,45 @@ self.objectLineEdit.setObjectName("objectLineEdit") self.horizontalLayout.addWidget(self.objectLineEdit) + 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.__optionsMenu = QMenu(self) + self.__richTextAct = self.__optionsMenu.addAction( + self.tr("Rich Text"), + lambda: self.__showTextViewer(True)) + self.__richTextAct.setCheckable(True) + self.__plainTextAct = self.__optionsMenu.addAction( + self.tr("Plain Text"), + lambda: self.__showTextViewer(False)) + self.__plainTextAct.setCheckable(True) + self.__optionsActionGroup = QActionGroup(self) + self.__optionsActionGroup.setExclusive(True) + self.__optionsActionGroup.addAction(self.__richTextAct) + self.__optionsActionGroup.addAction(self.__plainTextAct) + + self.__toolButton.setMenu(self.__optionsMenu) + self.horizontalLayout.addWidget(self.__toolButton) + self.verticalLayout.addLayout(self.horizontalLayout) - self.contents = PlainTextDocumentationViewer(self) - self.contents.setObjectName("contents") - self.verticalLayout.addWidget(self.contents) + # Plain Text Viewer + self.__plainTextViewer = PlainTextDocumentationViewer(self) + self.__plainTextViewer.setObjectName("__plainTextViewer") + self.verticalLayout.addWidget(self.__plainTextViewer) + + # Rich Text (Web) Viewer + self.__richTextViewer = WebViewDocumentationViewer(self) + self.__richTextViewer.setObjectName("__richTextViewer") + self.verticalLayout.addWidget(self.__richTextViewer) self.providerComboBox.currentIndexChanged[int].connect( self.on_providerComboBox_currentIndexChanged) @@ -166,6 +281,8 @@ """ Public method to finalize the setup of the documentation viewer. """ + self.__showTextViewer(Preferences.getDocuViewer("ShowInfoAsMarkdown")) + self.__startingUp = False provider = Preferences.getDocuViewer("Provider") if provider in self.__providers: @@ -200,6 +317,8 @@ self.__providers[providerName] = (provider, supported) self.providerComboBox.addItem(providerDisplay, providerName) + + self.providerAdded.emit() # TODO: document this hook in the plug-in document def unregisterProvider(self, providerName): @@ -216,6 +335,8 @@ del self.__providers[providerName] index = self.providerComboBox.findData(providerName) self.providerComboBox.removeItem(index) + + self.providerRemoved.emit() def isSupportedLanguage(self, language): """ @@ -260,11 +381,12 @@ word = editor.getWord(line, index) if not word: # try again one index before - word = editor.getWord(line, index - 1) + word = editor.getWord(line, index - 1) self.objectLineEdit.setText(word) if self.__selectedProvider != self.__disabledProvider: - self.contents.clear() + self.__plainTextViewer.clear() + self.__richTextViewer.clear() self.__providers[self.__selectedProvider][0](editor) # TODO: document this hook in the plug-in document @@ -288,50 +410,62 @@ self.__lastDocumentation = documentationInfo - if not documentationInfo: - fullText = self.__noDocumentationString - elif isinstance(documentationInfo, str): - fullText = documentationInfo - elif isinstance(documentationInfo, dict): - # format the text with markdown syntax - name = documentationInfo["name"] - if name: - title = "".join([name, "\n", - "=" * len(name), "\n\n"]) - else: - title = "" - - if documentationInfo["argspec"]: - if self.__showMarkdown: - definition = self.tr("**Definition**: {0}{1}\n", - "string with markdown syntax").format( - name, documentationInfo["argspec"]) + if documentationInfo is not None: + if not documentationInfo: + if self.__selectedProvider == self.__disabledProvider: + fullText = self.__disabledString + else: + fullText = self.__noDocumentationString + elif isinstance(documentationInfo, str): + fullText = documentationInfo + elif isinstance(documentationInfo, dict): + # format the text with markdown syntax + name = documentationInfo["name"] + if name: + title = "".join([name, "\n", + "=" * len(name), "\n\n"]) else: - definition = self.tr("Definition: {0}{1}\n", - "string as plain text").format( - name, documentationInfo["argspec"]) - else: - definition = '' + title = "" - if documentationInfo["note"]: - if self.__showMarkdown: - note = self.tr("**Info**: {0}\n\n----\n\n", - "string with markdown syntax").format( - documentationInfo["note"]) + if documentationInfo["argspec"]: + if self.__showMarkdown: + definition = self.tr( + "**Definition**: {0}{1}\n", + "string with markdown syntax").format( + name, documentationInfo["argspec"]) + else: + definition = self.tr( + "Definition: {0}{1}\n", + "string as plain text").format( + name, documentationInfo["argspec"]) else: - note = self.tr("Info: {0}\n\n----\n\n", - "string as plain text").format( - documentationInfo["note"]) - else: - note = "" + definition = '' - fullText = "".join([title, definition, note, - documentationInfo['docstring']]) - - if self.__showMarkdown: - self.__processingThread.process("markdown", fullText) - else: - self.contents.setText(fullText) + if documentationInfo["note"]: + if self.__showMarkdown: + note = self.tr( + "**Info**: {0}\n\n----\n\n", + "string with markdown syntax").format( + documentationInfo["note"]) + else: + note = self.tr( + "Info: {0}\n\n----\n\n", + "string as plain text").format( + documentationInfo["note"]) + else: + note = "" + + if documentationInfo["docstring"] is None: + docString = "" + else: + docString = documentationInfo["docstring"] + + fullText = "".join([title, definition, note, docString]) + + if self.__showMarkdown: + self.__processingThread.process("markdown", fullText) + else: + self.__plainTextViewer.setText(fullText) def __setHtml(self, html): """ @@ -340,7 +474,7 @@ @param html prepared HTML text @type str """ - self.contents.setHtml(html) + self.__richTextViewer.setHtml(html) @pyqtSlot(int) def on_providerComboBox_currentIndexChanged(self, index): @@ -351,12 +485,16 @@ @type int """ if not self.__shuttingDown and not self.__startingUp: - self.contents.clear() + self.__plainTextViewer.clear() + self.__richTextViewer.clear() self.objectLineEdit.clear() provider = self.providerComboBox.itemData(index) if provider == self.__disabledProvider: self.documentationReady(self.__disabledString) + else: + self.__lastDocumentation = None + Preferences.setDocuViewer("Provider", provider) self.__selectedProvider = provider @@ -373,8 +511,7 @@ """ showMarkdown = Preferences.getDocuViewer("ShowInfoAsMarkdown") if showMarkdown != self.__showMarkdown: - self.__showMarkdown = showMarkdown - self.documentationReady(self.__lastDocumentation) + self.__showTextViewer(showMarkdown) provider = Preferences.getDocuViewer("Provider") if provider != self.__selectedProvider: @@ -382,6 +519,28 @@ 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.__richTextViewer.clear() + + self.__plainTextViewer.setVisible(not richText) + self.__richTextViewer.setVisible(richText) + + self.__plainTextAct.setChecked(not richText) + self.__richTextAct.setChecked(richText) + + self.documentationReady(self.__lastDocumentation) + + Preferences.setDocuViewer("ShowInfoAsMarkdown", richText) class DocumentProcessingThread(QThread):