UI/CodeDocumentationViewer.py

Tue, 17 Oct 2017 19:40:32 +0200

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Tue, 17 Oct 2017 19:40:32 +0200
changeset 5912
b6643d36dddd
parent 5911
0c7bcba51391
child 5913
7ab2293917f8
permissions
-rw-r--r--

Added the rich text view to the documentation viewer.

# -*- coding: utf-8 -*-

# Copyright (c) 2017 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

from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QThread
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, \
    QComboBox, QSizePolicy, QLineEdit, QTextEdit

from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget

import Preferences


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.
    """
    providerAdded = 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.__showMarkdown = Preferences.getDocuViewer("ShowInfoAsMarkdown")
        
        self.__noDocumentationString = self.tr("No documentation available")
        self.__disabledString = self.tr(
            "No source code documentation provider has been registered or"
            " this function has been 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)
        
        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):
        """
        Public method to finalize the setup of the documentation viewer.
        """
        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
    
    # TODO: document this hook in the plug-in document
    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)
    
    # TODO: document this hook in the plug-in document
    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)
            
            del self.__providers[providerName]
            index = self.providerComboBox.findData(providerName)
            self.providerComboBox.removeItem(index)
    
    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.contents.clear()
            self.__providers[self.__selectedProvider][0](editor)
    
    # TODO: document this hook in the plug-in document
    def documentationReady(self, documentationInfo):
        """
        Public method to provide the documentation info to the viewer.
        
        If documentationInfo is a dictionary, it should contains these keys
        and data:
        
        name: the name of the inspected object
        argspec: its argspec
        note: A phrase describing the type of object (function or method) and
            the module it belongs to.
        docstring: its documentation string
        
        @param documentationInfo dictionary containing the source docu data
        @type dict or str
        """
        self.__ui.activateCodeDocumentationViewer(switchFocus=False)
        
        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"])
                else:
                    definition = self.tr("Definition: {0}{1}\n",
                                         "string as plain text").format(
                        name, documentationInfo["argspec"])
            else:
                definition = ''

            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 = ""

            fullText = "".join([title, definition, note,
                                documentationInfo['docstring']])
        
        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):
        """
        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.contents.clear()
            self.objectLineEdit.clear()
            
            provider = self.providerComboBox.itemData(index)
            if provider == self.__disabledProvider:
                self.documentationReady(self.__disabledString)
            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.
        """
        showMarkdown = Preferences.getDocuViewer("ShowInfoAsMarkdown")
        if showMarkdown != self.__showMarkdown:
            self.__showMarkdown = showMarkdown
            self.documentationReady(self.__lastDocumentation)
        
        provider = Preferences.getDocuViewer("Provider")
        if provider != self.__selectedProvider:
            index = self.providerComboBox.findData(provider)
            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
        
        @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