UI/CodeDocumentationViewer.py

changeset 5912
b6643d36dddd
parent 5911
0c7bcba51391
child 5913
7ab2293917f8
diff -r 0c7bcba51391 -r b6643d36dddd UI/CodeDocumentationViewer.py
--- a/UI/CodeDocumentationViewer.py	Mon Oct 16 20:18:04 2017 +0200
+++ b/UI/CodeDocumentationViewer.py	Tue Oct 17 19:40:32 2017 +0200
@@ -10,15 +10,75 @@
 
 from __future__ import unicode_literals
 
-from PyQt5.QtCore import pyqtSlot, pyqtSignal
-from PyQt5.QtWidgets import QWidget
+from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QThread
+from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \
+    QComboBox, QSizePolicy, QLineEdit, QTextEdit
 
-from .Ui_CodeDocumentationViewer import Ui_CodeDocumentationViewer
+from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget
 
 import Preferences
 
 
-class CodeDocumentationViewer(QWidget, Ui_CodeDocumentationViewer):
+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.__contents = QTextEdit(self)
+        self.__contents.setTabChangesFocus(True)
+        self.__contents.setReadOnly(True)
+        self.__contents.setObjectName("contents")
+        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)
+        
+        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 setHtml(self, html):
+        self.__contents.setHtml(html)
+    
+    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 CodeDocumentationViewer(QWidget):
     """
     Class implementing a widget to show some source code information provided
     by plug-ins.
@@ -33,9 +93,7 @@
         @type QWidget
         """
         super(CodeDocumentationViewer, self).__init__(parent)
-        self.setupUi(self)
-        
-        self.searchWidget.attachTextEdit(self.contents)
+        self.__setupUi()
         
         self.__ui = parent
         
@@ -55,11 +113,54 @@
             "No source code documentation provider has been registered or"
             " this function has been disabled.")
         
-        self.providerComboBox.addItem(self.tr("<disabled>"), "disabled")
+        self.__processingThread = DocumentProcessingThread()
+        self.__processingThread.htmlReady.connect(self.__setHtml)
+    
+    def __setupUi(self):
+        """
+        Private method to generate the UI layout.
+        """
+        self.setObjectName("CodeDocumentationViewer")
+        
+        self.verticalLayout = QVBoxLayout(self)
+        self.verticalLayout.setObjectName("verticalLayout")
+        
+        # top row of widgets
+        self.horizontalLayout = QHBoxLayout()
+        self.horizontalLayout.setObjectName("horizontalLayout")
+        
+        self.label = QLabel(self)
+        self.label.setObjectName("label")
+        self.label.setText(self.tr("Code Info Provider:"))
+        self.horizontalLayout.addWidget(self.label)
         
-        font = Preferences.getEditorOtherFonts("MonospacedFont")
-        self.contents.setFontFamily(font.family())
-        self.contents.setFontPointSize(font.pointSize())
+        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.horizontalLayout.addWidget(self.providerComboBox)
+        
+        self.objectLineEdit = QLineEdit(self)
+        self.objectLineEdit.setReadOnly(True)
+        self.objectLineEdit.setObjectName("objectLineEdit")
+        self.horizontalLayout.addWidget(self.objectLineEdit)
+        
+        self.verticalLayout.addLayout(self.horizontalLayout)
+        
+        self.contents = PlainTextDocumentationViewer(self)
+        self.contents.setObjectName("contents")
+        self.verticalLayout.addWidget(self.contents)
+        
+        self.providerComboBox.currentIndexChanged[int].connect(
+            self.on_providerComboBox_currentIndexChanged)
     
     def finalizeSetup(self):
         """
@@ -133,6 +234,21 @@
         
         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.
@@ -199,7 +315,7 @@
 
             if documentationInfo["note"]:
                 if self.__showMarkdown:
-                    note = self.tr("**Info**: _{0}_\n\n----\n\n",
+                    note = self.tr("**Info**: {0}\n\n----\n\n",
                                    "string with markdown syntax").format(
                         documentationInfo["note"])
                 else:
@@ -212,7 +328,19 @@
             fullText = "".join([title, definition, note,
                                 documentationInfo['docstring']])
         
-        self.contents.setPlainText(fullText)
+        if self.__showMarkdown:
+            self.__processingThread.process("markdown", fullText)
+        else:
+            self.contents.setText(fullText)
+    
+    def __setHtml(self, html):
+        """
+        Private slot to set the prepared HTML text.
+        
+        @param html prepared HTML text
+        @type str
+        """
+        self.contents.setHtml(html)
     
     @pyqtSlot(int)
     def on_providerComboBox_currentIndexChanged(self, index):
@@ -223,14 +351,14 @@
         @type int
         """
         if not self.__shuttingDown and not self.__startingUp:
+            self.contents.clear()
+            self.objectLineEdit.clear()
+            
             provider = self.providerComboBox.itemData(index)
             if provider == self.__disabledProvider:
                 self.documentationReady(self.__disabledString)
-            elif provider in self.__providers:
-                Preferences.setDocuViewer("Provider", provider)
+            Preferences.setDocuViewer("Provider", provider)
             self.__selectedProvider = provider
-            self.contents.clear()
-            self.objectLineEdit.clear()
     
     def shutdown(self):
         """
@@ -254,7 +382,121 @@
             if index < 0:
                 index = 0
             self.providerComboBox.setCurrentIndex(index)
+
+
+class DocumentProcessingThread(QThread):
+    """
+    Class implementing a thread to process some text into HTML usable by the
+    viewer.
+    
+    @signal htmlReady(str) emitted with the processed HTML to signal the
+        availability of the processed HTML
+    """
+    htmlReady = pyqtSignal(str)
+    
+    def __init__(self, parent=None):
+        """
+        Constructor
         
-        font = Preferences.getEditorOtherFonts("MonospacedFont")
-        self.contents.setFontFamily(font.family())
-        self.contents.setFontPointSize(font.pointSize())
+        @param parent reference to the parent object (QObject)
+        """
+        super(DocumentProcessingThread, self).__init__()
+    
+    def process(self, language, text):
+        """
+        Public method to convert the given text to HTML.
+        
+        @param language language of the text
+        @type str
+        @param text text to be processed
+        @type str
+        """
+        if self.wait():
+            self.__language = language
+            self.__text = text
+            self.start()
+    
+    def run(self):
+        """
+        Public thread method to convert the stored data.
+        """
+        language = self.__language
+        text = self.__text
+        
+        if language == "markdown":
+            html = self.__convertMarkdown(text, True, "html5")
+        else:
+            html = "<html><body><p>"
+            html += self.tr("Format '{0}' is not supported.").format(language)
+            html += "</p></body></html>"
+        
+        self.htmlReady.emit(html)
+    
+    def __convertMarkdown(self, text, convertNewLineToBreak, htmlFormat):
+        """
+        Private method to convert Markdown text into HTML.
+        
+        @param text text to be processed
+        @type str
+        @param convertNewLineToBreak flag indicating to convert new lines
+            to HTML break
+        @type bool
+        @param htmlFormat HTML format to be generated by markdown
+        @type str
+        @return processed HTML
+        @rtype str
+        """
+        try:
+            import markdown     # __IGNORE_EXCEPTION__
+        except ImportError:
+            return self.tr(
+                """<p>Markdown view requires the <b>Markdown</b> """
+                """package.<br/>Install it with your package manager,"""
+                """ 'pip install Markdown' or see """
+                """<a href="http://pythonhosted.org/Markdown/install.html">"""
+                """installation instructions.</a></p>""")
+        
+        try:
+            import mdx_mathjax  # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
+        except ImportError:
+            # mathjax doesn't require import statement if installed
+            # as extension
+            pass
+        
+        if convertNewLineToBreak:
+            extensions = ['fenced_code', 'nl2br', 'extra']
+        else:
+            extensions = ['fenced_code', 'extra']
+        
+        # version 2.0 supports only extension names, not instances
+        if markdown.version_info[0] > 2 or \
+                (markdown.version_info[0] == 2 and
+                 markdown.version_info[1] > 0):
+            class _StrikeThroughExtension(markdown.Extension):
+                """
+                Class is placed here, because it depends on imported markdown,
+                and markdown import is lazy.
+                
+                (see https://pythonhosted.org/Markdown/extensions/api.html
+                this page for details)
+                """
+                DEL_RE = r'(~~)(.*?)~~'
+
+                def extendMarkdown(self, md, md_globals):
+                    # Create the del pattern
+                    del_tag = markdown.inlinepatterns.SimpleTagPattern(
+                        self.DEL_RE, 'del')
+                    # Insert del pattern into markdown parser
+                    md.inlinePatterns.add('del', del_tag, '>not_strong')
+            
+            extensions.append(_StrikeThroughExtension())
+
+        try:
+            return markdown.markdown(text, extensions=extensions + ['mathjax'],
+                                     output_format=htmlFormat.lower())
+        except (ImportError, ValueError):
+            # markdown raises ValueError or ImportError, depends on version
+            # It is not clear, how to distinguish missing mathjax from other
+            # errors. So keep going without mathjax.
+            return markdown.markdown(text, extensions=extensions,
+                                     output_format=htmlFormat.lower())

eric ide

mercurial