Wed, 18 Oct 2017 19:16:28 +0200
Changed the rich text display of the document viewer to use a QWebEngineView or QWebView based widget.
--- a/E5Gui/E5TextEditSearchWidget.py Tue Oct 17 19:40:32 2017 +0200 +++ b/E5Gui/E5TextEditSearchWidget.py Wed Oct 18 19:16:28 2017 +0200 @@ -10,11 +10,9 @@ from __future__ import unicode_literals from PyQt5.QtCore import pyqtSlot, Qt -from PyQt5.QtGui import QTextDocument +from PyQt5.QtGui import QPalette, QBrush, QColor, QTextDocument, QTextCursor from PyQt5.QtWidgets import QWidget -from E5Gui import E5MessageBox - from .Ui_E5TextEditSearchWidget import Ui_E5TextEditSearchWidget import UI.PixmapCache @@ -34,11 +32,17 @@ self.setupUi(self) self.__textedit = None + self.__texteditType = "" self.__findBackwards = True self.findPrevButton.setIcon(UI.PixmapCache.getIcon("1leftarrow.png")) self.findNextButton.setIcon(UI.PixmapCache.getIcon("1rightarrow.png")) + self.__defaultBaseColor = \ + self.findtextCombo.lineEdit().palette().color(QPalette.Base) + self.__defaultTextColor = \ + self.findtextCombo.lineEdit().palette().color(QPalette.Text) + self.findHistory = [] self.findtextCombo.setCompleter(None) @@ -46,16 +50,25 @@ self.__findByReturnPressed) self.__setSearchButtons(False) + self.infoLabel.hide() self.setFocusProxy(self.findtextCombo) - def attachTextEdit(self, textedit): + def attachTextEdit(self, textedit, editType="QTextEdit"): """ Public method to attach a QTextEdit widget. - @param textedit reference to the QTextEdit to be attached (QTextEdit) + @param textedit reference to the edit widget to be attached + @type QTextEdit, QWebEngineView or QWebView + @param editType type of the attached edit widget + @type str (one of "QTextEdit", "QWebEngineView" or "QWebView") """ + assert editType in ["QTextEdit", "QWebEngineView", "QWebView"] + self.__textedit = textedit + self.__texteditType = editType + + self.wordCheckBox.setVisible(editType == "QTextEdit") def keyPressEvent(self, event): """ @@ -75,6 +88,9 @@ @param txt text of the combobox (string) """ self.__setSearchButtons(txt != "") + + self.infoLabel.hide() + self.__setFindtextComboBackground(False) def __setSearchButtons(self, enabled): """ @@ -115,7 +131,13 @@ if not self.__textedit: return + self.infoLabel.clear() + self.infoLabel.hide() + self.__setFindtextComboBackground(False) + txt = self.findtextCombo.currentText() + if not txt: + return self.__findBackwards = backwards # This moves any previous occurrence of this statement to the head @@ -126,6 +148,25 @@ self.findtextCombo.clear() self.findtextCombo.addItems(self.findHistory) + if self.__texteditType == "QTextEdit": + ok = self.__findPrevNextQTextEdit(backwards) + self.__findNextPrevCallback(ok) + elif self.__texteditType == "QWebEngineView": + self.__findPrevNextQWebEngineView(backwards) + elif self.__texteditType == "QWebView": + ok = self.__findPrevNextQWebView(backwards) + self.__findNextPrevCallback(ok) + + def __findPrevNextQTextEdit(self, backwards): + """ + Private method to to search the associated edit widget of + type QTextEdit. + + @param backwards flag indicating a backwards search + @type bool + @return flag indicating the search result + @rtype bool + """ if backwards: flags = QTextDocument.FindFlags(QTextDocument.FindBackward) else: @@ -134,10 +175,87 @@ flags |= QTextDocument.FindCaseSensitively if self.wordCheckBox.isChecked(): flags |= QTextDocument.FindWholeWords - ok = self.__textedit.find(txt, flags) + ok = self.__textedit.find(self.findtextCombo.currentText(), flags) if not ok: - E5MessageBox.information( - self, - self.tr("Find"), - self.tr("""'{0}' was not found.""").format(txt)) + # wrap around once + cursor = self.__textedit.textCursor() + if backwards: + moveOp = QTextCursor.End # move to end of document + else: + moveOp = QTextCursor.Start # move to start of document + cursor.movePosition(moveOp) + self.__textedit.setTextCursor(cursor) + ok = self.__textedit.find(self.findtextCombo.currentText(), flags) + + return ok + + def __findPrevNextQWebView(self, backwards): + """ + Private method to to search the associated edit widget of + type QWebView. + + @param backwards flag indicating a backwards search + @type bool + @return flag indicating the search result + @rtype bool + """ + from PyQt5.QtWebKitWidgets import QWebPage + + findFlags = QWebPage.FindFlags(QWebPage.HighlightAllOccurrences) + if self.caseCheckBox.isChecked(): + findFlags |= QWebPage.FindCaseSensitively + if backwards: + findFlags |= QWebPage.FindBackward + + return self.findText(self.findtextCombo.currentText(), findFlags) + + def __findPrevNextQWebEngineView(self, backwards): + """ + Private method to to search the associated edit widget of + type QWebEngineView. + + @param backwards flag indicating a backwards search + @type bool + """ + from PyQt5.QtWebEngineWidgets import QWebEnginePage + + findFlags = QWebEnginePage.FindFlags() + if self.caseCheckBox.isChecked(): + findFlags |= QWebEnginePage.FindCaseSensitively + if backwards: + findFlags |= QWebEnginePage.FindBackward + self.__textedit.findText(self.findtextCombo.currentText(), + findFlags, self.__findNextPrevCallback) + + def __findNextPrevCallback(self, found): + """ + Private method to process the result of the last search. + + @param found flag indicating if the last search succeeded + @type bool + """ + if not found: + txt = self.findtextCombo.currentText() + self.infoLabel.setText( + self.tr("'{0}' was not found.").format(txt)) + self.infoLabel.show() + self.__setFindtextComboBackground(True) + + def __setFindtextComboBackground(self, error): + """ + Private slot to change the findtext combo background to indicate + errors. + + @param error flag indicating an error condition (boolean) + """ + le = self.findtextCombo.lineEdit() + p = le.palette() + if error: + p.setBrush(QPalette.Base, QBrush(QColor("#FF6666"))) + p.setBrush(QPalette.Text, QBrush(QColor("#000000"))) + else: + p.setBrush(QPalette.Base, self.__defaultBaseColor) + p.setBrush(QPalette.Text, self.__defaultTextColor) + le.setPalette(p) + le.update()
--- a/E5Gui/E5TextEditSearchWidget.ui Tue Oct 17 19:40:32 2017 +0200 +++ b/E5Gui/E5TextEditSearchWidget.ui Wed Oct 18 19:16:28 2017 +0200 @@ -7,10 +7,10 @@ <x>0</x> <y>0</y> <width>475</width> - <height>22</height> + <height>43</height> </rect> </property> - <layout class="QHBoxLayout" name="horizontalLayout"> + <layout class="QGridLayout" name="gridLayout"> <property name="leftMargin"> <number>0</number> </property> @@ -23,15 +23,15 @@ <property name="bottomMargin"> <number>0</number> </property> - <item> + <item row="0" column="0"> <widget class="QLabel" name="label"> <property name="text"> <string>Find:</string> </property> </widget> </item> - <item> - <widget class="QComboBox" name="findtextCombo"> + <item row="0" column="1"> + <widget class="E5ClearableComboBox" name="findtextCombo"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Fixed"> <horstretch>0</horstretch> @@ -55,36 +55,57 @@ </property> </widget> </item> - <item> + <item row="0" column="2"> <widget class="QCheckBox" name="caseCheckBox"> <property name="text"> <string>Match case</string> </property> </widget> </item> - <item> + <item row="0" column="3"> <widget class="QCheckBox" name="wordCheckBox"> <property name="text"> <string>Whole word</string> </property> </widget> </item> - <item> - <widget class="QToolButton" name="findPrevButton"> - <property name="toolTip"> - <string>Press to find the previous occurrence</string> + <item row="0" column="4"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="spacing"> + <number>0</number> </property> - </widget> + <item> + <widget class="QToolButton" name="findPrevButton"> + <property name="toolTip"> + <string>Press to find the previous occurrence</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="findNextButton"> + <property name="toolTip"> + <string>Press to find the next occurrence</string> + </property> + </widget> + </item> + </layout> </item> - <item> - <widget class="QToolButton" name="findNextButton"> - <property name="toolTip"> - <string>Press to find the next occurrence</string> + <item row="1" column="0" colspan="5"> + <widget class="QLabel" name="infoLabel"> + <property name="text"> + <string/> </property> </widget> </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>E5ClearableComboBox</class> + <extends>QComboBox</extends> + <header>E5Gui/E5ComboBox.h</header> + </customwidget> + </customwidgets> <tabstops> <tabstop>findtextCombo</tabstop> <tabstop>caseCheckBox</tabstop>
--- a/QScintilla/Editor.py Tue Oct 17 19:40:32 2017 +0200 +++ b/QScintilla/Editor.py Wed Oct 18 19:16:28 2017 +0200 @@ -4854,6 +4854,7 @@ """ return (self.acAPI or bool(self.__ctHookFunctions)) + def callTip(self): """ Public method to show calltips. @@ -4983,7 +4984,6 @@ ct = ct - ctshift return ct - ################################################################# ## Methods needed by the code documentation viewer #################################################################
--- 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):
--- a/UI/UserInterface.py Tue Oct 17 19:40:32 2017 +0200 +++ b/UI/UserInterface.py Wed Oct 18 19:16:28 2017 +0200 @@ -761,7 +761,7 @@ from .CodeDocumentationViewer import CodeDocumentationViewer self.codeDocumentationViewer = CodeDocumentationViewer(self) self.rToolbox.addItem(self.codeDocumentationViewer, - UI.PixmapCache.getIcon("codeDocuViewer.png"), + UI.PixmapCache.getIcon("codeDocuViewer.png"), self.tr("Code Documentation Viewer")) # Create the debug viewer maybe without the embedded shell @@ -908,7 +908,7 @@ self.codeDocumentationViewer = CodeDocumentationViewer(self) self.rightSidebar.addTab( self.codeDocumentationViewer, - UI.PixmapCache.getIcon("codeDocuViewer.png"), + UI.PixmapCache.getIcon("codeDocuViewer.png"), self.tr("Code Documentation Viewer")) # Create the debug viewer maybe without the embedded shell
--- a/ViewManager/ViewManager.py Tue Oct 17 19:40:32 2017 +0200 +++ b/ViewManager/ViewManager.py Wed Oct 18 19:16:28 2017 +0200 @@ -6831,7 +6831,7 @@ def __isEditorInfoSupportedEd(self, editor): """ - Public method to check, if a language is supported by the + Private method to check, if an editor is supported by the documentation viewer. @param editor reference to the editor to check for