Mon, 11 Oct 2021 19:59:45 +0200
Continued implementing the embedded help viewer widget.
--- a/eric7/HelpViewer/HelpViewerImpl.py Thu Oct 07 20:22:02 2021 +0200 +++ b/eric7/HelpViewer/HelpViewerImpl.py Mon Oct 11 19:59:45 2021 +0200 @@ -7,18 +7,106 @@ Module implementing the help viewer base class. """ -from PyQt6.QtCore import QObject +from PyQt6.QtCore import pyqtSignal, QCoreApplication + +AboutBlank = QCoreApplication.translate( + "HelpViewer", + "<html>" + "<head><title>about:blank</title></head>" + "<body></body>" + "</html>") + +PageNotFound = QCoreApplication.translate( + "HelpViewer", + """<html>""" + """<head><title>Error 404...</title></head>""" + """<body><div align="center"><br><br>""" + """<h1>The page could not be found</h1><br>""" + """<h3>'{0}'</h3></div></body>""" + """</html>""") -class HelpViewerImpl(QObject): +class HelpViewerImpl: """ Class implementing the help viewer base class. + + This is the base class of help viewer implementations and defines the + interface. Als subclasses must implement the these methods. """ - def __init__(self, parent=None): + titleChanged = pyqtSignal() + loadFinished = pyqtSignal(bool) + + def __init__(self, engine): """ Constructor - @param parent reference to the parent widget - @type QWidget + @param engine reference to the help engine + @type QHelpEngine + """ + self._engine = engine + + def setUrl(self, url): + """ + Public method to set the URL of the document to be shown. + + @param url source of the document + @type QUrl + @exception RuntimeError raised when not implemented + """ + raise RuntimeError("Not implemented") + + def getData(self, url): + """ + Public method to get the data to be shown. + + @param url URL to be loaded + @type QUrl + @return data to be shown + @rtype str """ - super().__init__(parent) + scheme = url.scheme() + if scheme == "about": + if url.toString() == "about:blank": + return AboutBlank + else: + return PageNotFound.format(url.toString()) + elif scheme in ("file", ""): + filePath = url.toLocalFile() + try: + with open(filePath, "r", encoding="utf-8") as f: + htmlText = f.read() + return htmlText + except OSError: + return PageNotFound.format(url.toString()) + elif scheme == "qthelp": + if self._engine.findFile(url).isValid(): + data = bytes(self._engine.fileData(url)).decode("utf-8") + if not data: + data = PageNotFound.format(url.toString()) + return data + else: + return PageNotFound.format(url.toString()) + else: + # None is an indicator that we cannot handle the request + return None + + def title(self): + """ + Public method get the page title. + + @return page title + @rtype str + @exception RuntimeError raised when not implemented + """ + raise RuntimeError("Not implemented") + return "" + + def gotoHistory(self, index): + """ + Public method to step through the history. + + @param index history index (<0 backward, >0 forward) + @type int + @exception RuntimeError raised when not implemented + """ + raise RuntimeError("Not implemented")
--- a/eric7/HelpViewer/HelpViewerImpl_qtb.py Thu Oct 07 20:22:02 2021 +0200 +++ b/eric7/HelpViewer/HelpViewerImpl_qtb.py Mon Oct 11 19:59:45 2021 +0200 @@ -7,21 +7,119 @@ Module implementing the QTextBrowser based help viewer class. """ +from PyQt6.QtCore import Qt, QByteArray, QUrl +from PyQt6.QtGui import QDesktopServices, QImage from PyQt6.QtWidgets import QTextBrowser from .HelpViewerImpl import HelpViewerImpl -class HelpViewerImpl_qtb(QTextBrowser, HelpViewerImpl): +class HelpViewerImpl_qtb(HelpViewerImpl, QTextBrowser): """ Class implementing the QTextBrowser based help viewer class. """ - def __init__(self, parent): + def __init__(self, engine, parent=None): """ Constructor + @param engine reference to the help engine + @type QHelpEngine @param parent reference to the parent widget @type QWidget """ - super().__init__(parent) -# HelpViewerImpl.__init__(self) + QTextBrowser.__init__(self, parent=parent) + HelpViewerImpl.__init__(self, engine) + + self.sourceChanged.connect(self.titleChanged) + + def setUrl(self, url): + """ + Public method to set the URL of the document to be shown. + + @param url source of the document + @type QUrl + """ + self.setSource(url) + + def doSetSource(self, url, type): + """ + Public method to load the data and show it. + + @param url DESCRIPTION + @type TYPE + @param type DESCRIPTION + @type TYPE + """ + data = self.getData(url) + if data is None: + QDesktopServices.openUrl(url) + return + + if url != QUrl("about:blank"): + super().doSetSource(url, type) + + self.setHtml(data) + self.sourceChanged.emit(url) + self.loadFinished.emit(True) + + def title(self): + """ + Public method get the page title. + + @return page title + @rtype str + """ + return self.documentTitle() + + def loadResource(self, type_, name): + """ + Public method to load data of the specified type from the resource with + the given name. + + @param type_ resource type + @type int + @param name resource name + @type QUrl + """ + ba = QByteArray() + + if type_ < 4: # QTextDocument.ResourceType.MarkdownResource + url = self._engine.findFile(name) + ba = self._engine.fileData(url) + if url.toString().lower().endswith(".svg"): + image = QImage() + image.loadFromData(ba, "svg") + if not image.isNull(): + return image + + return ba + + def mousePressEvent(self, evt): + """ + Protected method called by a mouse press event. + + @param evt reference to the mouse event + @type QMouseEvent + """ + if evt.button() == Qt.MouseButton.XButton1: + self.backward() + elif evt.button() == Qt.MouseButton.XButton2: + self.forward() + else: + super().mousePressEvent(evt) + + def gotoHistory(self, index): + """ + Public method to step through the history. + + @param index history index (<0 backward, >0 forward) + @type int + """ + if index < 0: + # backward + for ind in range(-index): + self.backward() + else: + # forward + for ind in range(index): + self.forward()
--- a/eric7/HelpViewer/HelpViewerWidget.py Thu Oct 07 20:22:02 2021 +0200 +++ b/eric7/HelpViewer/HelpViewerWidget.py Mon Oct 11 19:59:45 2021 +0200 @@ -7,14 +7,21 @@ Module implementing an embedded viewer for QtHelp and local HTML files. """ -from PyQt6.QtCore import pyqtSlot, QUrl -from PyQt6.QtGui import QTextDocument +import os + +from PyQt6.QtCore import pyqtSlot, Qt, QUrl +from PyQt6.QtGui import QAction +from PyQt6.QtHelp import QHelpEngine from PyQt6.QtWidgets import ( QWidget, QHBoxLayout, QVBoxLayout, QComboBox, QSizePolicy, QStackedWidget, - QToolButton, QButtonGroup, QAbstractButton + QToolButton, QButtonGroup, QAbstractButton, QMenu ) +from EricWidgets import EricFileDialog, EricMessageBox + import UI.PixmapCache +import Utilities +import Preferences from .OpenPagesWidget import OpenPagesWidget @@ -33,6 +40,8 @@ super().__init__(parent) self.setObjectName("HelpViewerWidget") + self.__ui = parent + self.__layout = QVBoxLayout() self.__layout.setObjectName("MainLayout") self.__layout.setContentsMargins(0, 3, 0, 0) @@ -46,6 +55,8 @@ QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred) self.__selectorLayout.addWidget(self.__helpSelector) self.__populateHelpSelector() + self.__helpSelector.currentIndexChanged.connect( + self.__helpTopicSelected) self.__openButton = QToolButton(self) self.__openButton.setIcon(UI.PixmapCache.getIcon("open")) @@ -53,10 +64,76 @@ self.__openButton.clicked.connect(self.__openFile) self.__selectorLayout.addWidget(self.__openButton) + self.__actionsButton = QToolButton(self) + self.__actionsButton.setIcon( + UI.PixmapCache.getIcon("actionsToolButton")) + self.__actionsButton.setToolTip( + self.tr("Select action from menu")) + self.__actionsButton.setPopupMode( + QToolButton.ToolButtonPopupMode.InstantPopup) + self.__selectorLayout.addWidget(self.__actionsButton) + self.__layout.addLayout(self.__selectorLayout) ################################################################### + self.__navButtonsLayout = QHBoxLayout() + + self.__navButtonsLayout.addStretch() + + # TODO: add backward button + self.__backwardButton = QToolButton(self) + self.__backwardButton.setIcon(UI.PixmapCache.getIcon("back")) + self.__backwardButton.setToolTip(self.tr("Move one page backward")) + self.__backwardButton.setToolButtonStyle( + Qt.ToolButtonStyle.ToolButtonIconOnly) + self.__backwardButton.setAutoRaise(True) + self.__backwardButton.clicked.connect(self.__backward) + + # TODO: add forward button + self.__forwardButton = QToolButton(self) + self.__forwardButton.setIcon(UI.PixmapCache.getIcon("forward")) + self.__forwardButton.setToolTip(self.tr("Move one page forward")) + self.__forwardButton.setToolButtonStyle( + Qt.ToolButtonStyle.ToolButtonIconOnly) + self.__forwardButton.setAutoRaise(True) + self.__forwardButton.clicked.connect(self.__forward) + + self.__backForButtonLayout = QHBoxLayout() + self.__backForButtonLayout.setContentsMargins(0, 0, 0, 0) + self.__backForButtonLayout.setSpacing(0) + self.__backForButtonLayout.addWidget(self.__backwardButton) + self.__backForButtonLayout.addWidget(self.__forwardButton) + self.__navButtonsLayout.addLayout(self.__backForButtonLayout) + + # TODO: add reload button + self.__reloadButton = QToolButton(self) + self.__reloadButton.setIcon(UI.PixmapCache.getIcon("reload")) + self.__reloadButton.setToolTip(self.tr("Reload the current page")) + self.__reloadButton.clicked.connect(self.__reload) + self.__navButtonsLayout.addWidget(self.__reloadButton) + + # TODO: add zoom in button + # TODO: add zoom out button + # TODO: add zoom reset button + + self.__navButtonsLayout.addStretch() + + self.__layout.addLayout(self.__navButtonsLayout) + + self.__backMenu = QMenu(self) + self.__backMenu.triggered.connect(self.__navigationMenuActionTriggered) + self.__backwardButton.setMenu(self.__backMenu) + self.__backMenu.aboutToShow.connect(self.__showBackMenu) + + self.__forwardMenu = QMenu(self) + self.__forwardMenu.triggered.connect( + self.__navigationMenuActionTriggered) + self.__forwardButton.setMenu(self.__forwardMenu) + self.__forwardMenu.aboutToShow.connect(self.__showForwardMenu) + + ################################################################### + self.__helpStack = QStackedWidget(self) self.__helpStack.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) @@ -82,12 +159,10 @@ self.__buttonLayout.addStretch() - self.__openPagesButton = QToolButton(self) - self.__openPagesButton.setIcon(UI.PixmapCache.getIcon("fileMisc")) - self.__openPagesButton.setToolTip(self.tr("Show list of open pages")) - self.__openPagesButton.setCheckable(True) - self.__buttonGroup.addButton(self.__openPagesButton) - self.__buttonLayout.addWidget(self.__openPagesButton) + self.__openPagesButton = self.__addNavigationButton( + "fileMisc", self.tr("Show list of open pages")) + + # TODO: add buttons for the QHelp related widgets self.__buttonLayout.addStretch() @@ -99,13 +174,41 @@ self.__openPagesButton.setChecked(True) + self.__initHelpEngine() + + self.__ui.preferencesChanged.connect(self.__populateHelpSelector) + + self.__initActionsMenu() + self.addPage() + self.__checkActionButtons(0) + + def __addNavigationButton(self, iconName, toolTip): + """ + Private method to create and add a navigation button. + + @param iconName name of the icon + @type str + @param toolTip tooltip to be shown + @type str + @return reference to the created button + @rtype QToolButton + """ + button = QToolButton(self) + button.setIcon(UI.PixmapCache.getIcon(iconName)) + button.setToolTip(toolTip) + button.setCheckable(True) + self.__buttonGroup.addButton(button) + self.__buttonLayout.addWidget(button) + + return button def __populateNavigationStack(self): """ Private method to populate the stack of navigation widgets. """ self.__openPagesList = OpenPagesWidget(self.__helpStack, self) + self.__openPagesList.currentChanged.connect(self.__checkActionButtons) self.__helpNavigationStack.addWidget(self.__openPagesList) # TODO: not yet implemented @@ -127,7 +230,33 @@ """ Private method to populate the help selection combo box. """ - # TODO: not yet implemented + self.__helpSelector.clear() + + self.__helpSelector.addItem("", "") + + for key, topic in [ + ("EricDocDir", self.tr("eric API Documentation")), + ("PythonDocDir", self.tr("Python 3 Documentation")), + ("Qt5DocDir", self.tr("Qt5 Documentation")), + ("Qt6DocDir", self.tr("Qt6 Documentation")), + ("PyQt5DocDir", self.tr("PyQt5 Documentation")), + ("PyQt6DocDir", self.tr("PyQt6 Documentation")), + ("PySide2DocDir", self.tr("PySide2 Documentation")), + ("PySide6DocDir", self.tr("PySide6 Documentation")), + ]: + urlStr = Preferences.getHelp(key) + if urlStr: + self.__helpSelector.addItem(topic, urlStr) + + @pyqtSlot() + def __helpTopicSelected(self): + """ + Private slot handling the selection of a new help topic. + """ + urlStr = self.__helpSelector.currentData() + if urlStr: + url = QUrl(urlStr) + self.currentViewer().setUrl(url) def searchQtHelp(self, searchExpression): """ @@ -155,7 +284,14 @@ """ Private slot to open a local help file (*.html). """ - # TODO: not yet implemented + htmlFile = EricFileDialog.getOpenFileName( + self, + self.tr("Open HTML File"), + "", + self.tr("HTML Files (*.htm *.html);;All Files (*)") + ) + if htmlFile: + self.currentViewer().setUrl(QUrl.fromLocalFile(htmlFile)) def addPage(self, url=QUrl("about:blank")): """ @@ -164,14 +300,225 @@ @param url requested URL (defaults to QUrl("about:blank")) @type QUrl (optional) """ - try: - from .HelpViewerImpl_qwe import HelpViewerImpl_qwe - viewer = HelpViewerImpl_qwe(self) - except ImportError: - from .HelpViewerImpl_qtb import HelpViewerImpl_qtb - viewer = HelpViewerImpl_qtb(self) + viewer = self.__newViewer() + viewer.setUrl(url) self.__helpStack.addWidget(viewer) self.__openPagesList.addPage(viewer) + + def __newViewer(self): + """ + Private method to create a new help viewer. - viewer.setSource(url, QTextDocument.ResourceType.HtmlResource) + @return help viewer + @rtype HelpViewerImpl + """ + try: + from .HelpViewerImpl_qwe import HelpViewerImpl_qwe + viewer = HelpViewerImpl_qwe(self.__helpEngine, self) + except ImportError: + from .HelpViewerImpl_qtb import HelpViewerImpl_qtb + viewer = HelpViewerImpl_qtb(self.__helpEngine, self) + return viewer + + def currentViewer(self): + """ + Public method to get the active viewer. + + @return reference to the active help viewer + @rtype HelpViewerImpl + """ + return self.__helpStack.currentWidget() + + ####################################################################### + ## QtHelp related code below + ####################################################################### + + def __initHelpEngine(self): + """ + Private method to initialize the QtHelp related stuff. + """ + self.__helpEngine = QHelpEngine( + self.__getQtHelpCollectionFileName(), + self) + self.__helpEngine.setReadOnly(False) + self.__helpEngine.setupData() + self.__helpEngine.setUsesFilterEngine(True) + self.__removeOldDocumentation() + self.__helpEngine.warning.connect(self.__warning) + + def __getQtHelpCollectionFileName(cls): + """ + Private method to determine the name of the QtHelp collection file. + + @return path of the QtHelp collection file + @rtype str + """ + qthelpDir = os.path.join(Utilities.getConfigDir(), "qthelp") + if not os.path.exists(qthelpDir): + os.makedirs(qthelpDir) + return os.path.join(qthelpDir, "eric7help.qhc") + + @pyqtSlot(str) + def __warning(self, msg): + """ + Private slot handling warnings of the help engine. + + @param msg message sent by the help engine + @type str + """ + EricMessageBox.warning( + self, + self.tr("Help Engine"), msg) + + @pyqtSlot() + def __removeOldDocumentation(self): + """ + Private slot to remove non-existing documentation from the help engine. + """ + for namespace in self.__helpEngine.registeredDocumentations(): + docFile = self.__helpEngine.documentationFileName(namespace) + if not os.path.exists(docFile): + self.__helpEngine.unregisterDocumentation(namespace) + + @pyqtSlot() + def __manageQtHelpDocuments(self): + """ + Private slot to manage the QtHelp documentation database. + """ + from WebBrowser.QtHelp.QtHelpDocumentationConfigurationDialog import ( + QtHelpDocumentationConfigurationDialog + ) + dlg = QtHelpDocumentationConfigurationDialog( + self.__helpEngine, self) + dlg.exec() + + @pyqtSlot() + def __reindexDocumentation(self): + """ + Private slot + """ + + ####################################################################### + ## Actions Menu related methods + ####################################################################### + + def __initActionsMenu(self): + """ + Private method to initialize the actions menu. + """ + self.__actionsMenu = QMenu() + self.__actionsMenu.setToolTipsVisible(True) + + self.__actionsMenu.addAction(self.tr("Manage QtHelp Documents"), + self.__manageQtHelpDocuments) + act = self.__actionsMenu.addAction(self.tr("Reindex Documentation"), + self.__reindexDocumentation) +## act.triggered.connect(self.__searchEngine.reindexDocumentation) + + self.__actionsButton.setMenu(self.__actionsMenu) + + ####################################################################### + ## Navigation related methods below + ####################################################################### + + @pyqtSlot() + def __backward(self): + """ + Private slot to move one page backward. + """ + cv = self.currentViewer() + if cv: + cv.backward() + + @pyqtSlot() + def __forward(self): + """ + Private slot to move one page foreward. + """ + cv = self.currentViewer() + if cv: + cv.forward() + + @pyqtSlot() + def __reload(self): + """ + Private slot to reload the current page. + """ + cv = self.currentViewer() + if cv: + cv.reload() + + @pyqtSlot(int) + def __checkActionButtons(self, row): + """ + Private slot to set the enabled state of the action buttons. + + @param row index of the current page + @type int + """ + cv = self.currentViewer() + self.__backwardButton.setEnabled(cv and cv.isBackwardAvailable()) + self.__forwardButton.setEnabled(cv and cv.isForwardAvailable()) + + def __showBackMenu(self): + """ + Private slot showing the backward navigation menu. + """ + cv = self.currentViewer() + if cv: + self.__backMenu.clear() + backwardHistoryCount = max(cv.backwardHistoryCount(), 20) + # show max. 20 items + + for index in range(1, backwardHistoryCount + 1): + act = QAction(self) + act.setData(-index) + act.setText(cv.historyTitle(-index)) + self.__backMenu.addAction(act) + + self.__backMenu.addSeparator() + self.__backMenu.addAction(self.tr("Clear History"), + self.__clearHistory) + + def __showForwardMenu(self): + """ + Private slot showing the forward navigation menu. + """ + cv = self.currentViewer() + if cv: + self.__forwardMenu.clear() + forwardHistoryCount = max(cv.forwardHistoryCount(), 20) + # show max. 20 items + + for index in range(1, forwardHistoryCount + 1): + act = QAction(self) + act.setData(index) + act.setText(cv.historyTitle(index)) + self.__forwardMenu.addAction(act) + + self.__forwardMenu.addSeparator() + self.__forwardMenu.addAction(self.tr("Clear History"), + self.__clearHistory) + + def __navigationMenuActionTriggered(self, act): + """ + Private slot to go to the selected page. + + @param act reference to the action selected in the navigation menu + @type QAction + """ + cv = self.currentViewer() + if cv: + index = act.data() + cv.gotoHistory(index) + + def __clearHistory(self): + """ + Private slot to clear the history of the current viewer. + """ + cb = self.__mw.currentBrowser() + if cb is not None: + cb.history().clear() + self.__mw.setForwardAvailable(cb.isForwardAvailable()) + self.__mw.setBackwardAvailable(cb.isBackwardAvailable())
--- a/eric7/HelpViewer/OpenPagesWidget.py Thu Oct 07 20:22:02 2021 +0200 +++ b/eric7/HelpViewer/OpenPagesWidget.py Mon Oct 11 19:59:45 2021 +0200 @@ -7,9 +7,10 @@ Module implementing a widget showing the list of open pages. """ -from PyQt6.QtCore import pyqtSlot +from PyQt6.QtCore import pyqtSlot, pyqtSignal from PyQt6.QtWidgets import ( - QWidget, QLabel, QListWidget, QVBoxLayout, QAbstractItemView + QWidget, QLabel, QListWidget, QListWidgetItem, QVBoxLayout, + QAbstractItemView ) @@ -17,6 +18,8 @@ """ Class implementing a widget showing the list of open pages. """ + currentChanged = pyqtSignal(int) + def __init__(self, stack, parent=None): """ Constructor @@ -76,12 +79,29 @@ @type int """ self.__stack.setCurrentIndex(row) + self.currentChanged.emit(row) def addPage(self, viewer): """ Public method to add a viewer page to our list. - @param viewer DESCRIPTION - @type TYPE + @param viewer reference to the viewer object + @type HelpViewerImpl """ - # TODO: not yet implemented + QListWidgetItem(viewer.title(), self.__openPagesList) + viewer.titleChanged.connect( + lambda: self.__viewerTitleChanged(viewer)) + + self.__currentPageChanged(self.__openPagesList.count() - 1) + + def __viewerTitleChanged(self, viewer): + """ + Private method to handle the change of a viewer title. + + @param viewer reference to the viewer that change title + @type HelpViewerImpl + """ + index = self.__stack.indexOf(viewer) + itm = self.__openPagesList.item(index) + itm.setText(viewer.title()) + self.currentChanged.emit(index)
--- a/eric7/UI/UserInterface.py Thu Oct 07 20:22:02 2021 +0200 +++ b/eric7/UI/UserInterface.py Mon Oct 11 19:59:45 2021 +0200 @@ -3155,14 +3155,14 @@ Private slot to initialize the action to show the eric documentation. """ self.ericDocAct = EricAction( - self.tr("Eric API Documentation"), - self.tr('Eric API Documentation'), + self.tr("eric API Documentation"), + self.tr('eric API Documentation'), 0, 0, self, 'eric_documentation') self.ericDocAct.setStatusTip(self.tr( - "Open Eric API Documentation")) + "Open eric API Documentation")) self.ericDocAct.setWhatsThis(self.tr( - """<b>Eric API Documentation</b>""" - """<p>Display the Eric API documentation. The location for the""" + """<b>eric API Documentation</b>""" + """<p>Display the eric API documentation. The location for the""" """ documentation is the Documentation/Source subdirectory of""" """ the eric installation directory.</p>""" ))