UI/CodeDocumentationViewer.py

changeset 5913
7ab2293917f8
parent 5912
b6643d36dddd
child 5914
e44c04a89dbc
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):

eric ide

mercurial