Tue, 17 Oct 2017 19:40:32 +0200
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())