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())