Sun, 17 Oct 2021 15:26:31 +0200
Continued implementing the embedded help viewer widget. Added context menus to the help viewer variants.
eric7.epj | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/HelpViewerImpl.py | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/HelpViewerImplQTB.py | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/HelpViewerImplQWE.py | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/HelpViewerImpl_qtb.py | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/HelpViewerImpl_qwe.py | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/HelpViewerWidget.py | file | annotate | diff | comparison | revisions | |
eric7/HelpViewer/OpenPagesWidget.py | file | annotate | diff | comparison | revisions |
--- a/eric7.epj Sun Oct 17 15:26:01 2021 +0200 +++ b/eric7.epj Sun Oct 17 15:26:31 2021 +0200 @@ -2284,8 +2284,8 @@ "eric7/HelpViewer/HelpViewerWidget.py", "eric7/HelpViewer/OpenPagesWidget.py", "eric7/HelpViewer/HelpViewerImpl.py", - "eric7/HelpViewer/HelpViewerImpl_qtb.py", - "eric7/HelpViewer/HelpViewerImpl_qwe.py" + "eric7/HelpViewer/HelpViewerImplQTB.py", + "eric7/HelpViewer/HelpViewerImplQWE.py" ], "SPELLEXCLUDES": "Dictionaries/excludes.dic", "SPELLLANGUAGE": "en_US",
--- a/eric7/HelpViewer/HelpViewerImpl.py Sun Oct 17 15:26:01 2021 +0200 +++ b/eric7/HelpViewer/HelpViewerImpl.py Sun Oct 17 15:26:31 2021 +0200 @@ -7,23 +7,7 @@ Module implementing the help viewer base class. """ -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>""") +from PyQt6.QtCore import pyqtSignal, QUrl class HelpViewerImpl: @@ -32,6 +16,10 @@ This is the base class of help viewer implementations and defines the interface. Als subclasses must implement the these methods. + + @signal titleChanged() emitted to indicate a change of the page title + @signal loadFinished(ok) emitted to indicate the completion of a page load + @signal zoomChanged() emitted to indicate a change of the zoom level """ titleChanged = pyqtSignal() loadFinished = pyqtSignal(bool) @@ -46,7 +34,7 @@ """ self._engine = engine - def setUrl(self, url): + def setLink(self, url): """ Public method to set the URL of the document to be shown. @@ -56,53 +44,18 @@ """ raise RuntimeError("Not implemented") - def url(self): + def link(self): """ Public method to get the URL of the shown document. - @return url URL of the document + @return URL of the document @rtype QUrl @exception RuntimeError raised when not implemented """ raise RuntimeError("Not implemented") - return None + return QUrl() - 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 - """ - 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): + def pageTitle(self): """ Public method get the page title. @@ -128,6 +81,8 @@ Public method to check, if stepping backward through the history is available. + @return flag indicating backward stepping is available + @rtype bool @exception RuntimeError raised when not implemented """ raise RuntimeError("Not implemented") @@ -138,6 +93,8 @@ Public method to check, if stepping forward through the history is available. + @return flag indicating forward stepping is available + @rtype bool @exception RuntimeError raised when not implemented """ raise RuntimeError("Not implemented")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/HelpViewer/HelpViewerImplQTB.py Sun Oct 17 15:26:31 2021 +0200 @@ -0,0 +1,615 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the QTextBrowser based help viewer class. +""" + +import functools + +from PyQt6.QtCore import ( + pyqtSlot, Qt, QByteArray, QUrl, QEvent, QCoreApplication, QPoint +) +from PyQt6.QtGui import QDesktopServices, QImage, QGuiApplication, QClipboard +from PyQt6.QtWidgets import QTextBrowser, QMenu + +from .HelpViewerImpl import HelpViewerImpl + +import UI.PixmapCache + + +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 HelpViewerImplQTB(HelpViewerImpl, QTextBrowser): + """ + Class implementing the QTextBrowser based help viewer class. + """ + 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 + """ + QTextBrowser.__init__(self, parent=parent) + HelpViewerImpl.__init__(self, engine) + + self.__helpViewerWidget = parent + + self.__zoomCount = 0 + + self.__menu = QMenu(self) + self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.customContextMenuRequested.connect(self.__showContextMenu) + + self.sourceChanged.connect(self.titleChanged) + + self.grabGesture(Qt.GestureType.PinchGesture) + + def setLink(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 link(self): + """ + Public method to get the URL of the shown document. + + @return URL of the document + @rtype QUrl + """ + return self.source() + + def doSetSource(self, url, type_): + """ + Public method to load the data and show it. + + @param url URL of resource to load + @type QUrl + @param type_ type of the resource to load + @type QTextDocument.ResourceType + """ + 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 __getData(self, url): + """ + Private method to get the data to be shown. + + @param url URL to be loaded + @type QUrl + @return data to be shown + @rtype str + """ + 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 pageTitle(self): + """ + Public method get the page title. + + @return page title + @rtype str + """ + titleStr = self.documentTitle() + if not titleStr: + url = self.link() + + titleStr = url.host() + if not titleStr: + titleStr = url.toString( + QUrl.UrlFormattingOption.RemoveFragment) + + if not titleStr or titleStr == "about:blank": + titleStr = self.tr("Empty Page") + + return titleStr + + 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 + @return byte array containing the loaded data + @rtype QByteArray + """ + ba = QByteArray() + + if type_ < 4: # QTextDocument.ResourceType.MarkdownResource + # TODO: change to use getData() + 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() + evt.accept() + elif evt.button() == Qt.MouseButton.XButton2: + self.forward() + evt.accept() + else: + super().mousePressEvent(evt) + + def mouseReleaseEvent(self, evt): + """ + Protected method called by a mouse release event. + + @param evt reference to the mouse event + @type QMouseEvent + """ + hasModifier = evt.modifiers() != Qt.KeyboardModifier.NoModifier + if evt.button() == Qt.MouseButton.LeftButton and hasModifier: + + anchor = self.anchorAt(evt.pos()) + if anchor: + url = self.link().resolved(QUrl(anchor)) + if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: + self.__helpViewerWidget.openUrlNewBackgroundPage(url) + else: + self.__helpViewerWidget.openUrlNewPage(url) + evt.accept() + 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() + + def isBackwardAvailable(self): + """ + Public method to check, if stepping backward through the history is + available. + + @return flag indicating backward stepping is available + @rtype bool + """ + return QTextBrowser.isBackwardAvailable(self) + + def isForwardAvailable(self): + """ + Public method to check, if stepping forward through the history is + available. + + @return flag indicating forward stepping is available + @rtype bool + """ + return QTextBrowser.isForwardAvailable(self) + + def scaleUp(self): + """ + Public method to zoom in. + """ + if self.__zoomCount < 10: + self.__zoomCount += 1 + self.zoomIn() + self.zoomChanged.emit() + + def scaleDown(self): + """ + Public method to zoom out. + """ + if self.__zoomCount > -5: + self.__zoomCount -= 1 + self.zoomOut() + self.zoomChanged.emit() + + def setScale(self, scale): + """ + Public method to set the zoom level. + + @param scale zoom level to set + @type int + """ + if -5 <= scale <= 10: + self.zoomOut(scale) + self.__zoomCount = scale + self.zoomChanged.emit() + + def resetScale(self): + """ + Public method to reset the zoom level. + """ + if self.__zoomCount != 0: + self.zoomOut(self.__zoomCount) + self.zoomChanged.emit() + self.__zoomCount = 0 + + def scale(self): + """ + Public method to get the zoom level. + + @return current zoom level + @rtype int + """ + return self.__zoomCount + + def isScaleUpAvailable(self): + """ + Public method to check, if the max. zoom level is reached. + + @return flag indicating scale up is available + @rtype bool + """ + return self.__zoomCount < 10 + + def isScaleDownAvailable(self): + """ + Public method to check, if the min. zoom level is reached. + + @return flag indicating scale down is available + @rtype bool + """ + return self.__zoomCount > -5 + + def wheelEvent(self, evt): + """ + Protected method to handle wheel event to zoom. + + @param evt reference to the event object + @type QWheelEvent + """ + delta = evt.angleDelta().y() + if evt.modifiers() == Qt.KeyboardModifier.ControlModifier: + if delta > 0: + self.scaleUp() + else: + self.scaleDown() + evt.accept() + + elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: + if delta < 0: + self.backward() + elif delta > 0: + self.forward() + evt.accept() + + else: + QTextBrowser.wheelEvent(self, evt) + + def keyPressEvent(self, evt): + """ + Protected method to handle key press events. + + @param evt reference to the key event + @type QKeyEvent + """ + key = evt.key() + isControlModifier = bool( + evt.modifiers() & Qt.KeyboardModifier.ControlModifier) + + if ( + key == Qt.Key.Key_ZoomIn or + (key == Qt.Key.Key_Plus and isControlModifier) + ): + self.scaleUp() + evt.accept() + elif ( + key == Qt.Key.Key_ZoomOut or + (key == Qt.Key.Key_Minus and isControlModifier) + ): + self.scaleDown() + evt.accept() + elif key == Qt.Key.Key_0 and isControlModifier: + self.resetScale() + evt.accept() + elif ( + key == Qt.Key.Key_Backspace or + (key == Qt.Key.Key_Left and isControlModifier) + ): + self.backward() + evt.accept() + elif key == Qt.Key.Key_Right and isControlModifier: + self.forward() + evt.accept() + else: + super().keyPressEvent(evt) + + def event(self, evt): + """ + Public method handling events. + + @param evt reference to the event + @type QEvent + @return flag indicating the event was handled + @rtype bool + """ + if evt.type() == QEvent.Type.Gesture: + self.gestureEvent(evt) + return True + + return super().event(evt) + + def gestureEvent(self, evt): + """ + Protected method handling gesture events. + + @param evt reference to the gesture event + @type QGestureEvent + """ + pinch = evt.gesture(Qt.GestureType.PinchGesture) + if pinch: + if pinch.state() == Qt.GestureState.GestureStarted: + zoom = (self.getZoom() + 6) / 10.0 + pinch.setTotalScaleFactor(zoom) + elif pinch.state() == Qt.GestureState.GestureUpdated: + zoom = int(pinch.totalScaleFactor() * 10) - 6 + if zoom <= -5: + zoom = -5 + pinch.setTotalScaleFactor(0.1) + elif zoom >= 10: + zoom = 10 + pinch.setTotalScaleFactor(1.6) + self.setScale(zoom) + evt.accept() + + # TODO: add Ctrl+LMB action (open link in new page) + + ####################################################################### + ## Context menu related methods below + ####################################################################### + + @pyqtSlot(QPoint) + def __showContextMenu(self, pos): + """ + Private slot to show the context menu. + + @param pos position to show the context menu at + @type QPoint + """ + self.__menu.clear() + anchor = self.anchorAt(pos) + linkUrl = self.link().resolved(QUrl(anchor)) if anchor else QUrl() + selectedText = self.textCursor().selectedText() + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("back"), + self.tr("Backward"), + self.backward) + act.setEnabled(self.isBackwardAvailable()) + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("forward"), + self.tr("Forward"), + self.forward) + act.setEnabled(self.isForwardAvailable()) + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("reload"), + self.tr("Reload"), + self.reload) + + if not linkUrl.isEmpty() and linkUrl.scheme() != "javascript": + self.__createLinkContextMenu(self.__menu, linkUrl) + + self.__menu.addSeparator() + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy Page URL to Clipboard")) + act.setData(self.link()) + act.triggered.connect( + functools.partial(self.__copyLink, act)) + + self.__menu.addSeparator() + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("zoomIn"), + self.tr("Zoom in"), + self.scaleUp) + act.setEnabled(self.isScaleUpAvailable()) + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("zoomOut"), + self.tr("Zoom out"), + self.scaleDown) + act.setEnabled(self.isScaleDownAvailable()) + + self.__menu.addAction( + UI.PixmapCache.getIcon("zoomReset"), + self.tr("Zoom reset"), + self.resetScale) + + self.__menu.addSeparator() + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy"), + self.copy) + act.setEnabled(bool(selectedText)) + + self.__menu.addAction( + UI.PixmapCache.getIcon("editSelectAll"), + self.tr("Select All"), + self.selectAll) + + self.__menu.addSeparator() + + self.__menu.addAction( + UI.PixmapCache.getIcon("tabClose"), + self.tr('Close'), + self.__closePage) + + act = self.__menu.addAction( + UI.PixmapCache.getIcon("tabCloseOther"), + self.tr("Close Others"), + self.__closeOtherPages) + act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1) + + self.__menu.popup(self.mapToGlobal(pos)) + + def __createLinkContextMenu(self, menu, linkUrl): + """ + Private method to populate the context menu for URLs. + + @param menu reference to the menu to be populated + @type QMenu + @param linkUrl URL to create the menu part for + @type QUrl + """ + if not menu.isEmpty(): + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("openNewTab"), + self.tr("Open Link in New Page")) + act.setData(linkUrl) + act.triggered.connect( + functools.partial(self.__openLinkInNewPage, act)) + + act = menu.addAction( + UI.PixmapCache.getIcon("newWindow"), + self.tr("Open Link in Background Page")) + act.setData(linkUrl) + act.triggered.connect( + functools.partial(self.__openLinkInBackgroundPage, act)) + + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy URL to Clipboard")) + act.setData(linkUrl) + act.triggered.connect( + functools.partial(self.__copyLink, act)) + + def __openLinkInNewPage(self, act): + """ + Private method called by the context menu to open a link in a new page. + + @param act reference to the action that triggered + @type QAction + """ + url = act.data() + if url.isEmpty(): + return + + self.__helpViewerWidget.openUrlNewPage(url) + + def __openLinkInBackgroundPage(self, act): + """ + Private method called by the context menu to open a link in a + background page. + + @param act reference to the action that triggered + @type QAction + """ + url = act.data() + if url.isEmpty(): + return + + self.__helpViewerWidget.openUrlNewBackgroundPage(url) + + def __copyLink(self, act): + """ + Private method called by the context menu to copy a link to the + clipboard. + + @param act reference to the action that triggered + @type QAction + """ + data = act.data() + if isinstance(data, QUrl) and data.isEmpty(): + return + + if isinstance(data, QUrl): + data = data.toString() + + # copy the URL to both clipboard areas + QGuiApplication.clipboard().setText(data, QClipboard.Mode.Clipboard) + QGuiApplication.clipboard().setText(data, QClipboard.Mode.Selection) + + def __closePage(self): + """ + Private method called by the context menu to close the current page. + """ + self.__helpViewerWidget.closeCurrentPage() + + def __closeOtherPages(self): + """ + Private method called by the context menu to close all other pages. + """ + self.__helpViewerWidget.closeOtherPages()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/HelpViewer/HelpViewerImplQWE.py Sun Oct 17 15:26:31 2021 +0200 @@ -0,0 +1,739 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> +# + +""" +Module implementing the help viewer base class. +""" + +import functools + +from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QTimer, QUrl, QPoint +from PyQt6.QtGui import QGuiApplication, QClipboard, QContextMenuEvent +from PyQt6.QtWidgets import QMenu +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineNewWindowRequest + +from .HelpViewerWidget import HelpViewerWidget +from .HelpViewerImpl import HelpViewerImpl + +import UI.PixmapCache + + +class HelpViewerImplQWE(HelpViewerImpl, QWebEngineView): + """ + Class implementing the QTextBrowser based help viewer class. + """ + ZoomLevels = [ + 30, 40, 50, 67, 80, 90, + 100, + 110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300, + ] + ZoomLevelDefault = 100 + + 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 + """ + QWebEngineView.__init__(self, parent=parent) + HelpViewerImpl.__init__(self, engine) + + self.__helpViewerWidget = parent + + self.__rwhvqt = None + self.installEventFilter(self) + + self.__page = None + self.__createNewPage() + + self.__currentScale = 100 + + self.__menu = QMenu(self) + + def __createNewPage(self): + """ + Private method to create a new page object. + """ + self.__page = QWebEnginePage(self.__helpViewerWidget.webProfile()) + self.setPage(self.__page) + + self.__page.titleChanged.connect(self.__titleChanged) + self.__page.urlChanged.connect(self.__titleChanged) + self.__page.newWindowRequested.connect(self.__newWindowRequested) + + def __newWindowRequested(self, request): + """ + Private slot handling new window requests of the web page. + + @param request reference to the new window request + @type QWebEngineNewWindowRequest + """ + background = ( + request.destination() == + QWebEngineNewWindowRequest.DestinationType.InNewBackgroundTab + ) + newViewer = self.__helpViewerWidget.addPage(background=background) + request.openIn(newViewer.page()) + + def __setRwhvqt(self): + """ + Private slot to set widget that receives input events. + """ + self.grabGesture(Qt.GestureType.PinchGesture) + self.__rwhvqt = self.focusProxy() + if self.__rwhvqt: + self.__rwhvqt.grabGesture(Qt.GestureType.PinchGesture) + self.__rwhvqt.installEventFilter(self) + else: + print("Focus proxy is null!") # __IGNORE_WARNING_M801__ + + def setLink(self, url): + """ + Public method to set the URL of the document to be shown. + + @param url URL of the document + @type QUrl + """ + super().setUrl(url) + + def link(self): + """ + Public method to get the URL of the shown document. + + @return url URL of the document + @rtype QUrl + """ + return super().url() + + @pyqtSlot() + def __titleChanged(self): + """ + Private method to handle a change of the web page title. + """ + super().titleChanged.emit() + + def pageTitle(self): + """ + Public method get the page title. + + @return page title + @rtype str + """ + titleStr = super().title() + if not titleStr: + if self.link().isEmpty(): + url = self.__page.requestedUrl() + else: + url = self.link() + + titleStr = url.host() + if not titleStr: + titleStr = url.toString( + QUrl.UrlFormattingOption.RemoveFragment) + + if not titleStr or titleStr == "about:blank": + titleStr = self.tr("Empty Page") + + return titleStr + + ####################################################################### + ## History related methods below + ####################################################################### + + def isBackwardAvailable(self): + """ + Public method to check, if stepping backward through the history is + available. + + @return flag indicating backward stepping is available + @rtype bool + """ + return self.history().canGoBack() + + def isForwardAvailable(self): + """ + Public method to check, if stepping forward through the history is + available. + + @return flag indicating forward stepping is available + @rtype bool + """ + return self.history().canGoForward() + + def backward(self): + """ + Public slot to move backwards in history. + """ + self.triggerPageAction(QWebEnginePage.WebAction.Back) + + def forward(self): + """ + Public slot to move forward in history. + """ + self.triggerPageAction(QWebEnginePage.WebAction.Forward) + + def reload(self): + """ + Public slot to reload the current page. + """ + self.triggerPageAction(QWebEnginePage.WebAction.Reload) + + def backwardHistoryCount(self): + """ + Public method to get the number of available back history items. + + Note: For performance reasons this is limited to the maximum number of + history items the help viewer is interested in. + + @return count of available back history items + @rtype int + """ + history = self.history() + return len(history.backItems(HelpViewerWidget.MaxHistoryItems)) + + def forwardHistoryCount(self): + """ + Public method to get the number of available forward history items. + + Note: For performance reasons this is limited to the maximum number of + history items the help viewer is interested in. + + @return count of available forward history items + @rtype int + """ + history = self.history() + return len(history.forwardItems(HelpViewerWidget.MaxHistoryItems)) + + def historyTitle(self, offset): + """ + Public method to get the title of a history item. + + @param offset offset of the item with respect to the current page + @type int + @return title of the requeted item in history + @rtype str + """ + history = self.history() + currentIndex = history.currentItemIndex() + itm = self.history().itemAt(currentIndex + offset) + return itm.title() + + def gotoHistory(self, offset): + """ + Public method to go to a history item. + + @param offset offset of the item with respect to the current page + @type int + """ + history = self.history() + currentIndex = history.currentItemIndex() + itm = self.history().itemAt(currentIndex + offset) + history.goToItem(itm) + + def clearHistory(self): + """ + Public method to clear the history. + """ + self.history().clear() + + ####################################################################### + ## Zoom related methods below + ####################################################################### + + def __levelForScale(self, scale): + """ + Private method determining the zoom level index given a zoom factor. + + @param scale zoom factor + @type int + @return index of zoom factor + @rtype int + """ + try: + index = self.ZoomLevels.index(scale) + except ValueError: + for _index in range(len(self.ZoomLevels)): + if scale <= self.ZoomLevels[scale]: + break + return index + + def scaleUp(self): + """ + Public method to zoom in. + """ + index = self.__levelForScale(self.__currentScale) + if index < len(self.ZoomLevels) - 1: + self.setScale(self.ZoomLevels[index + 1]) + + def scaleDown(self): + """ + Public method to zoom out. + """ + index = self.__levelForScale(self.__currentScale) + if index > 0: + self.setScale(self.ZoomLevels[index - 1]) + + def setScale(self, scale): + """ + Public method to set the zoom level. + + @param scale zoom level to set + @type int + """ + if scale != self.__currentScale: + self.setZoomFactor(scale / 100.0) + self.__currentScale = scale + self.zoomChanged.emit() + + def resetScale(self): + """ + Public method to reset the zoom level. + """ + index = self.__levelForScale(self.ZoomLevelDefault) + self.setScale(self.ZoomLevels[index]) + + def scale(self): + """ + Public method to get the zoom level. + + @return current zoom level + @rtype int + """ + return self.__currentScale + + def isScaleUpAvailable(self): + """ + Public method to check, if the max. zoom level is reached. + + @return flag indicating scale up is available + @rtype bool + """ + index = self.__levelForScale(self.__currentScale) + return index < len(self.ZoomLevels) - 1 + + def isScaleDownAvailable(self): + """ + Public method to check, if the min. zoom level is reached. + + @return flag indicating scale down is available + @rtype bool + """ + index = self.__levelForScale(self.__currentScale) + return index > 0 + + ####################################################################### + ## Event handlers below + ####################################################################### + + def eventFilter(self, obj, evt): + """ + Public method to process event for other objects. + + @param obj reference to object to process events for + @type QObject + @param evt reference to event to be processed + @type QEvent + @return flag indicating that the event should be filtered out + @rtype bool + """ + if ( + obj is self and + evt.type() == QEvent.Type.ParentChange and + self.parentWidget() is not None + ): + self.parentWidget().installEventFilter(self) + + # find the render widget receiving events for the web page + if obj is self and evt.type() == QEvent.Type.ChildAdded: + QTimer.singleShot(0, self.__setRwhvqt) + + # forward events to WebBrowserView + if ( + obj is self.__rwhvqt and + evt.type() in [QEvent.Type.KeyPress, + QEvent.Type.MouseButtonRelease, + QEvent.Type.Wheel, + QEvent.Type.Gesture] + ): + wasAccepted = evt.isAccepted() + evt.setAccepted(False) + if evt.type() == QEvent.Type.KeyPress: + self._keyPressEvent(evt) + elif evt.type() == QEvent.Type.MouseButtonRelease: + self._mouseReleaseEvent(evt) + elif evt.type() == QEvent.Type.Wheel: + self._wheelEvent(evt) + elif evt.type() == QEvent.Type.Gesture: + self._gestureEvent(evt) + ret = evt.isAccepted() + evt.setAccepted(wasAccepted) + return ret + + if ( + obj is self.parentWidget() and + evt.type() in [QEvent.Type.KeyPress, QEvent.Type.KeyRelease] + ): + wasAccepted = evt.isAccepted() + evt.setAccepted(False) + if evt.type() == QEvent.Type.KeyPress: + self._keyPressEvent(evt) + ret = evt.isAccepted() + evt.setAccepted(wasAccepted) + return ret + + # block already handled events + if ( + obj is self and + evt.type() in [QEvent.Type.KeyPress, + QEvent.Type.MouseButtonRelease, + QEvent.Type.Wheel, + QEvent.Type.Gesture] + ): + return True + + return super().eventFilter(obj, evt) + + def _keyPressEvent(self, evt): + """ + Protected method called by a key press. + + @param evt reference to the key event + @type QKeyEvent + """ + key = evt.key() + isControlModifier = bool( + evt.modifiers() & Qt.KeyboardModifier.ControlModifier) + + if ( + key == Qt.Key.Key_ZoomIn or + (key == Qt.Key.Key_Plus and isControlModifier) + ): + self.scaleUp() + evt.accept() + elif ( + key == Qt.Key.Key_ZoomOut or + (key == Qt.Key.Key_Minus and isControlModifier) + ): + self.scaleDown() + evt.accept() + elif key == Qt.Key.Key_0 and isControlModifier: + self.resetScale() + evt.accept() + elif ( + key == Qt.Key.Key_Backspace or + (key == Qt.Key.Key_Left and isControlModifier) + ): + self.backward() + evt.accept() + elif key == Qt.Key.Key_Right and isControlModifier: + self.forward() + evt.accept() + + def _mouseReleaseEvent(self, evt): + """ + Protected method called by a mouse release event. + + @param evt reference to the mouse event + @type QMouseEvent + """ + accepted = evt.isAccepted() + self.__page.event(evt) + if ( + not evt.isAccepted() and + evt.button() == Qt.MouseButton.MiddleButton + ): + url = QUrl(QGuiApplication.clipboard().text( + QClipboard.Mode.Selection)) + if ( + not url.isEmpty() and + url.isValid() and + url.scheme() != "" + ): + self.setLink(url) + accepted = True + evt.setAccepted(accepted) + + def _wheelEvent(self, evt): + """ + Protected method to handle wheel events. + + @param evt reference to the wheel event + @type QWheelEvent + """ + delta = evt.angleDelta().y() + if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: + if delta < 0: + self.scaleDown() + elif delta > 0: + self.scaleUp() + evt.accept() + + elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: + if delta < 0: + self.backward() + elif delta > 0: + self.forward() + evt.accept() + + def _gestureEvent(self, evt): + """ + Protected method handling gesture events. + + @param evt reference to the gesture event + @type QGestureEvent + """ + pinch = evt.gesture(Qt.GestureType.PinchGesture) + if pinch: + if pinch.state() == Qt.GestureState.GestureStarted: + pinch.setTotalScaleFactor(self.__currentScale / 100.0) + elif pinch.state() == Qt.GestureState.GestureUpdated: + scaleFactor = pinch.totalScaleFactor() + self.setScale(int(scaleFactor * 100)) + evt.accept() + + def event(self, evt): + """ + Public method handling events. + + @param evt reference to the event (QEvent) + @return flag indicating, if the event was handled (boolean) + """ + if evt.type() == QEvent.Type.Gesture: + self._gestureEvent(evt) + return True + + return super().event(evt) + + ####################################################################### + ## Context menu related methods below + ####################################################################### + + def contextMenuEvent(self, evt): + """ + Protected method called to create a context menu. + + This method is overridden from QWebEngineView. + + @param evt reference to the context menu event object + @type QContextMenuEvent + """ + pos = evt.pos() + reason = evt.reason() + QTimer.singleShot( + 0, + lambda: self._contextMenuEvent(QContextMenuEvent(reason, pos))) + # needs to be done this way because contextMenuEvent is blocking + # the main loop + + def _contextMenuEvent(self, evt): + """ + Protected method called to create a context menu. + + @param evt reference to the context menu event object + (QContextMenuEvent) + """ + self.__menu.clear() + + self.__createContextMenu(self.__menu) + + if not self.__menu.isEmpty(): + pos = evt.globalPos() + self.__menu.popup(QPoint(pos.x(), pos.y() + 1)) + + def __createContextMenu(self, menu): + """ + Private method to populate the context menu. + + @param menu reference to the menu to be populated + @type QMenu + """ + contextMenuData = self.lastContextMenuRequest() + + act = menu.addAction( + UI.PixmapCache.getIcon("back"), + self.tr("Backward"), + self.backward) + act.setEnabled(self.isBackwardAvailable()) + + act = menu.addAction( + UI.PixmapCache.getIcon("forward"), + self.tr("Forward"), + self.forward) + act.setEnabled(self.isForwardAvailable()) + + act = menu.addAction( + UI.PixmapCache.getIcon("reload"), + self.tr("Reload"), + self.reload) + + if ( + not contextMenuData.linkUrl().isEmpty() and + contextMenuData.linkUrl().scheme() != "javascript" + ): + self.__createLinkContextMenu(menu, contextMenuData) + + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy Page URL to Clipboard")) + act.setData(self.link()) + act.triggered.connect( + functools.partial(self.__copyLink, act)) + + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("zoomIn"), + self.tr("Zoom in"), + self.scaleUp) + act.setEnabled(self.isScaleUpAvailable()) + + act = menu.addAction( + UI.PixmapCache.getIcon("zoomOut"), + self.tr("Zoom out"), + self.scaleDown) + act.setEnabled(self.isScaleDownAvailable()) + + menu.addAction( + UI.PixmapCache.getIcon("zoomReset"), + self.tr("Zoom reset"), + self.resetScale) + + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy"), + self.__copyText) + act.setEnabled(bool(contextMenuData.selectedText())) + + menu.addAction( + UI.PixmapCache.getIcon("editSelectAll"), + self.tr("Select All"), + self.__selectAll) + + menu.addSeparator() + + menu.addAction( + UI.PixmapCache.getIcon("tabClose"), + self.tr('Close'), + self.__closePage) + + act = menu.addAction( + UI.PixmapCache.getIcon("tabCloseOther"), + self.tr("Close Others"), + self.__closeOtherPages) + act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1) + + def __createLinkContextMenu(self, menu, contextMenuData): + """ + Private method to populate the context menu for URLs. + + @param menu reference to the menu to be populated + @type QMenu + @param contextMenuData data of the last context menu request + @type QWebEngineContextMenuRequest + """ + if not menu.isEmpty(): + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("openNewTab"), + self.tr("Open Link in New Page")) + act.setData(contextMenuData.linkUrl()) + act.triggered.connect( + functools.partial(self.__openLinkInNewPage, act)) + + act = menu.addAction( + UI.PixmapCache.getIcon("newWindow"), + self.tr("Open Link in Background Page")) + act.setData(contextMenuData.linkUrl()) + act.triggered.connect( + functools.partial(self.__openLinkInBackgroundPage, act)) + + menu.addSeparator() + + act = menu.addAction( + UI.PixmapCache.getIcon("editCopy"), + self.tr("Copy URL to Clipboard")) + act.setData(contextMenuData.linkUrl()) + act.triggered.connect( + functools.partial(self.__copyLink, act)) + + def __openLinkInNewPage(self, act): + """ + Private method called by the context menu to open a link in a new page. + + @param act reference to the action that triggered + @type QAction + """ + url = act.data() + if url.isEmpty(): + return + + self.__helpViewerWidget.openUrlNewPage(url) + + def __openLinkInBackgroundPage(self, act): + """ + Private method called by the context menu to open a link in a + background page. + + @param act reference to the action that triggered + @type QAction + """ + url = act.data() + if url.isEmpty(): + return + + self.__helpViewerWidget.openUrlNewBackgroundPage(url) + + def __copyLink(self, act): + """ + Private method called by the context menu to copy a link to the + clipboard. + + @param act reference to the action that triggered + @type QAction + """ + data = act.data() + if isinstance(data, QUrl) and data.isEmpty(): + return + + if isinstance(data, QUrl): + data = data.toString() + + # copy the URL to both clipboard areas + QGuiApplication.clipboard().setText(data, QClipboard.Mode.Clipboard) + QGuiApplication.clipboard().setText(data, QClipboard.Mode.Selection) + + def __copyText(self): + """ + Private method called by the context menu to copy selected text to the + clipboard. + """ + self.triggerPageAction(QWebEnginePage.WebAction.Copy) + + def __selectAll(self): + """ + Private method called by the context menu to select all text. + """ + self.triggerPageAction(QWebEnginePage.WebAction.SelectAll) + + def __closePage(self): + """ + Private method called by the context menu to close the current page. + """ + self.__helpViewerWidget.closeCurrentPage() + + def __closeOtherPages(self): + """ + Private method called by the context menu to close all other pages. + """ + self.__helpViewerWidget.closeOtherPages()
--- a/eric7/HelpViewer/HelpViewerImpl_qtb.py Sun Oct 17 15:26:01 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,334 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing the QTextBrowser based help viewer class. -""" - -from PyQt6.QtCore import Qt, QByteArray, QUrl, QEvent -from PyQt6.QtGui import QDesktopServices, QImage -from PyQt6.QtWidgets import QTextBrowser - -from .HelpViewerImpl import HelpViewerImpl - - -class HelpViewerImpl_qtb(HelpViewerImpl, QTextBrowser): - """ - Class implementing the QTextBrowser based help viewer class. - """ - 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 - """ - QTextBrowser.__init__(self, parent=parent) - HelpViewerImpl.__init__(self, engine) - - self.__zoomCount = 0 - - self.sourceChanged.connect(self.titleChanged) - - self.grabGesture(Qt.GestureType.PinchGesture) - - 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 url(self): - """ - Public method to get the URL of the shown document. - - @return url URL of the document - @rtype QUrl - """ - return self.source() - - 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 - """ - titleStr = self.documentTitle() - if not titleStr: - url = self.url() - - titleStr = url.host() - if not titleStr: - titleStr = url.toString( - QUrl.UrlFormattingOption.RemoveFragment) - - if not titleStr or titleStr == "about:blank": - titleStr = self.tr("Empty Page") - - return titleStr - - 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() - - def isBackwardAvailable(self): - """ - Public method to check, if stepping backward through the history is - available. - """ - return QTextBrowser.isBackwardAvailable(self) - - def isForwardAvailable(self): - """ - Public method to check, if stepping forward through the history is - available. - """ - return QTextBrowser.isForwardAvailable(self) - - def scaleUp(self): - """ - Public method to zoom in. - """ - if self.__zoomCount < 10: - self.__zoomCount += 1 - self.zoomIn() - self.zoomChanged.emit() - - def scaleDown(self): - """ - Public method to zoom out. - """ - if self.__zoomCount > -5: - self.__zoomCount -= 1 - self.zoomOut() - self.zoomChanged.emit() - - def setScale(self, scale): - """ - Public method to set the zoom level. - - @param scale zoom level to set - @type int - """ - if -5 <= scale <= 10: - self.zoomOut(scale) - self.__zoomCount = scale - self.zoomChanged.emit() - - def resetScale(self): - """ - Public method to reset the zoom level. - """ - if self.__zoomCount != 0: - self.zoomOut(self.__zoomCount) - self.zoomChanged.emit() - self.__zoomCount = 0 - - def scale(self): - """ - Public method to get the zoom level. - - @return current zoom level - @rtype int - """ - return self.__zoomCount - - def isScaleUpAvailable(self): - """ - Public method to check, if the max. zoom level is reached. - - @return flag indicating scale up is available - @rtype bool - """ - return self.__zoomCount < 10 - - def isScaleDownAvailable(self): - """ - Public method to check, if the min. zoom level is reached. - - @return flag indicating scale down is available - @rtype bool - """ - return self.__zoomCount > -5 - - def wheelEvent(self, evt): - """ - Public method to handle wheel event to zoom. - - @param evt reference to the event object - @type QWheelEvent - """ - delta = evt.angleDelta().y() - if evt.modifiers() == Qt.KeyboardModifier.ControlModifier: - if delta > 0: - self.scaleUp() - else: - self.scaleDown() - evt.accept() - - elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: - if delta < 0: - self.backward() - elif delta > 0: - self.forward() - evt.accept() - - else: - QTextBrowser.wheelEvent(self, evt) - - def keyPressEvent(self, evt): - """ - Public method to handle key press events. - - @param evt reference to the key event - @type QKeyEvent - """ - if evt.key() == Qt.Key.Key_ZoomIn: - self.scaleUp() - evt.accept() - elif evt.key() == Qt.Key.Key_ZoomOut: - self.scaleDown() - evt.accept() - elif evt.key() == Qt.Key.Key_Plus: - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - self.scaleUp() - evt.accept() - elif evt.key() == Qt.Key.Key_Minus: - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - self.scaleDown() - evt.accept() - elif evt.key() == Qt.Key.Key_0: - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - self.resetScale() - evt.accept() - elif evt.key() == Qt.Key.Key_Backspace: - self.backward() - evt.accept() - - def event(self, evt): - """ - Public method handling events. - - @param evt reference to the event - @type QEvent - @return flag indicating the event was handled - @rtype bool - """ - if evt.type() == QEvent.Type.Gesture: - self.gestureEvent(evt) - return True - - return super().event(evt) - - def gestureEvent(self, evt): - """ - Protected method handling gesture events. - - @param evt reference to the gesture event - @type QGestureEvent - """ - pinch = evt.gesture(Qt.GestureType.PinchGesture) - if pinch: - if pinch.state() == Qt.GestureState.GestureStarted: - zoom = (self.getZoom() + 6) / 10.0 - pinch.setTotalScaleFactor(zoom) - elif pinch.state() == Qt.GestureState.GestureUpdated: - zoom = int(pinch.totalScaleFactor() * 10) - 6 - if zoom <= -5: - zoom = -5 - pinch.setTotalScaleFactor(0.1) - elif zoom >= 10: - zoom = 10 - pinch.setTotalScaleFactor(1.6) - self.setScale(zoom) - evt.accept() - - # TODO: implement context menu - # - Backward - # - Forward - # - Reload - # - Open Link - # - Open Link in New Page - # - Open Link in Background Page - # - Copy - # - Copy Link Location - # - Select All - # TODO: add Ctrl+LMB action (open link in new page)
--- a/eric7/HelpViewer/HelpViewerImpl_qwe.py Sun Oct 17 15:26:01 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,688 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing the help viewer base class. -""" - -from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QTimer, QUrl, QPoint -from PyQt6.QtGui import QGuiApplication, QClipboard, QContextMenuEvent -from PyQt6.QtWidgets import QMenu -from PyQt6.QtWebEngineWidgets import QWebEngineView -from PyQt6.QtWebEngineCore import QWebEnginePage - -from .HelpViewerWidget import HelpViewerWidget -from .HelpViewerImpl import HelpViewerImpl - -import UI.PixmapCache - -class HelpViewerImpl_qwe(HelpViewerImpl, QWebEngineView): - """ - Class implementing the QTextBrowser based help viewer class. - """ - ZoomLevels = [ - 30, 40, 50, 67, 80, 90, - 100, - 110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300, - ] - ZoomLevelDefault = 100 - - 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 - """ - QWebEngineView.__init__(self, parent=parent) - HelpViewerImpl.__init__(self, engine) - - self.__helpViewerWidget = parent - - self.__rwhvqt = None - self.installEventFilter(self) - - self.__page = None - self.__createNewPage() - - self.__currentScale = 100 - - self.__menu = QMenu(self) - - def __createNewPage(self): - """ - Private method to create a new page object. - """ - self.__page = QWebEnginePage(self.__helpViewerWidget.webProfile()) - self.setPage(self.__page) - - self.__page.titleChanged.connect(self.__titleChanged) - self.__page.urlChanged.connect(self.__titleChanged) - - def __setRwhvqt(self): - """ - Private slot to set widget that receives input events. - """ - self.grabGesture(Qt.GestureType.PinchGesture) - self.__rwhvqt = self.focusProxy() - if self.__rwhvqt: - self.__rwhvqt.grabGesture(Qt.GestureType.PinchGesture) - self.__rwhvqt.installEventFilter(self) - else: - print("Focus proxy is null!") # __IGNORE_WARNING_M801__ - - def setUrl(self, url): - """ - Public method to set the URL of the document to be shown. - - @param url URL of the document - @type QUrl - """ - QWebEngineView.setUrl(self, url) - - def url(self): - """ - Public method to get the URL of the shown document. - - @return url URL of the document - @rtype QUrl - """ - return QWebEngineView.url(self) - - @pyqtSlot() - def __titleChanged(self): - """ - Private method to handle a change of the web page title. - """ - super().titleChanged.emit() - - def title(self): - """ - Public method get the page title. - - @return page title - @rtype str - """ - titleStr = QWebEngineView.title(self) - if not titleStr: - if self.url().isEmpty(): - url = self.__page.requestedUrl() - else: - url = self.url() - - titleStr = url.host() - if not titleStr: - titleStr = url.toString( - QUrl.UrlFormattingOption.RemoveFragment) - - if not titleStr or titleStr == "about:blank": - titleStr = self.tr("Empty Page") - - return titleStr - - def isBackwardAvailable(self): - """ - Public method to check, if stepping backward through the history is - available. - """ - return self.history().canGoBack() - - def isForwardAvailable(self): - """ - Public method to check, if stepping forward through the history is - available. - """ - return self.history().canGoForward() - - def backward(self): - """ - Public slot to move backwards in history. - """ - self.triggerPageAction(QWebEnginePage.WebAction.Back) - - def forward(self): - """ - Public slot to move forward in history. - """ - self.triggerPageAction(QWebEnginePage.WebAction.Forward) - - def reload(self): - """ - Public slot to reload the current page. - """ - self.triggerPageAction(QWebEnginePage.WebAction.Reload) - - def backwardHistoryCount(self): - """ - Public method to get the number of available back history items. - - Note: For performance reasons this is limited to the maximum number of - history items the help viewer is interested in. - - @return count of available back history items - @rtype int - """ - history = self.history() - return len(history.backItems(HelpViewerWidget.MaxHistoryItems)) - - def forwardHistoryCount(self): - """ - Public method to get the number of available forward history items. - - Note: For performance reasons this is limited to the maximum number of - history items the help viewer is interested in. - - @return count of available forward history items - @rtype int - """ - history = self.history() - return len(history.forwardItems(HelpViewerWidget.MaxHistoryItems)) - - def historyTitle(self, offset): - """ - Public method to get the title of a history item. - - @param offset offset of the item with respect to the current page - @type int - @return title of the requeted item in history - @rtype str - """ - history = self.history() - currentIndex = history.currentItemIndex() - itm = self.history().itemAt(currentIndex + offset) - return itm.title() - - def gotoHistory(self, offset): - """ - Public method to go to ahistory item - - @param offset offset of the item with respect to the current page - @type int - """ - history = self.history() - currentIndex = history.currentItemIndex() - itm = self.history().itemAt(currentIndex + offset) - history.goToItem(itm) - - def clearHistory(self): - """ - Public method to clear the history. - """ - self.history().clear() - - ####################################################################### - - def __levelForScale(self, scale): - """ - Private method determining the zoom level index given a zoom factor. - - @param zoom zoom factor - @type int - @return index of zoom factor - @rtype int - """ - try: - index = self.ZoomLevels.index(scale) - except ValueError: - for index in range(len(self.ZoomLevels)): - if scale <= self.ZoomLevels[scale]: - break - return index - - def scaleUp(self): - """ - Public method to zoom in. - """ - index = self.__levelForScale(self.__currentScale) - if index < len(self.ZoomLevels) - 1: - self.setScale(self.ZoomLevels[index + 1]) - - def scaleDown(self): - """ - Public method to zoom out. - """ - index = self.__levelForScale(self.__currentScale) - if index > 0: - self.setScale(self.ZoomLevels[index - 1]) - - def setScale(self, scale): - """ - Public method to set the zoom level. - - @param scale zoom level to set - @type int - """ - if scale != self.__currentScale: - self.setZoomFactor(scale / 100.0) - self.__currentScale = scale - self.zoomChanged.emit() - - def resetScale(self): - """ - Public method to reset the zoom level. - """ - index = self.__levelForScale(self.ZoomLevelDefault) - self.setScale(self.ZoomLevels[index]) - - def scale(self): - """ - Public method to get the zoom level. - - @return current zoom level - @rtype int - """ - return self.__currentScale - - def isScaleUpAvailable(self): - """ - Public method to check, if the max. zoom level is reached. - - @return flag indicating scale up is available - @rtype bool - """ - index = self.__levelForScale(self.__currentScale) - return index < len(self.ZoomLevels) - 1 - - def isScaleDownAvailable(self): - """ - Public method to check, if the min. zoom level is reached. - - @return flag indicating scale down is available - @rtype bool - """ - index = self.__levelForScale(self.__currentScale) - return index > 0 - - ####################################################################### - ## Event handlers below - ####################################################################### - - def eventFilter(self, obj, evt): - """ - Public method to process event for other objects. - - @param obj reference to object to process events for - @type QObject - @param evt reference to event to be processed - @type QEvent - @return flag indicating that the event should be filtered out - @rtype bool - """ - if ( - obj is self and - evt.type() == QEvent.Type.ParentChange and - self.parentWidget() is not None - ): - self.parentWidget().installEventFilter(self) - - # find the render widget receiving events for the web page - if obj is self and evt.type() == QEvent.Type.ChildAdded: - QTimer.singleShot(0, self.__setRwhvqt) - - # forward events to WebBrowserView - if ( - obj is self.__rwhvqt and - evt.type() in [QEvent.Type.KeyPress, - QEvent.Type.MouseButtonRelease, - QEvent.Type.Wheel, - QEvent.Type.Gesture] - ): - wasAccepted = evt.isAccepted() - evt.setAccepted(False) - if evt.type() == QEvent.Type.KeyPress: - self._keyPressEvent(evt) - elif evt.type() == QEvent.Type.MouseButtonRelease: - self._mouseReleaseEvent(evt) - elif evt.type() == QEvent.Type.Wheel: - self._wheelEvent(evt) - elif evt.type() == QEvent.Type.Gesture: - self._gestureEvent(evt) - ret = evt.isAccepted() - evt.setAccepted(wasAccepted) - return ret - - if ( - obj is self.parentWidget() and - evt.type() in [QEvent.Type.KeyPress, QEvent.Type.KeyRelease] - ): - wasAccepted = evt.isAccepted() - evt.setAccepted(False) - if evt.type() == QEvent.Type.KeyPress: - self._keyPressEvent(evt) - ret = evt.isAccepted() - evt.setAccepted(wasAccepted) - return ret - - # block already handled events - if obj is self: - if evt.type() in [QEvent.Type.KeyPress, - QEvent.Type.MouseButtonRelease, - QEvent.Type.Wheel, - QEvent.Type.Gesture]: - return True - - return super().eventFilter(obj, evt) - - def _keyPressEvent(self, evt): - """ - Protected method called by a key press. - - @param evt reference to the key event - @type QKeyEvent - """ - if evt.key() == Qt.Key.Key_ZoomIn: - self.scaleUp() - evt.accept() - elif evt.key() == Qt.Key.Key_ZoomOut: - self.scaleDown() - evt.accept() - elif evt.key() == Qt.Key.Key_Plus: - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - self.scaleUp() - evt.accept() - elif evt.key() == Qt.Key.Key_Minus: - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - self.scaleDown() - evt.accept() - elif evt.key() == Qt.Key.Key_0: - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - self.resetScale() - evt.accept() - elif evt.key() == Qt.Key.Key_Backspace: - self.pageAction(QWebEnginePage.WebAction.Back).trigger() - evt.accept() - - def _mouseReleaseEvent(self, evt): - """ - Protected method called by a mouse release event. - - @param evt reference to the mouse event - @type QMouseEvent - """ - accepted = evt.isAccepted() - self.__page.event(evt) - if ( - not evt.isAccepted() and - evt.button() == Qt.MouseButton.MiddleButton - ): - url = QUrl(QGuiApplication.clipboard().text( - QClipboard.Mode.Selection)) - if ( - not url.isEmpty() and - url.isValid() and - url.scheme() != "" - ): - self.setUrl(url) - accepted = True - evt.setAccepted(accepted) - - def _wheelEvent(self, evt): - """ - Protected method to handle wheel events. - - @param evt reference to the wheel event - @type QWheelEvent - """ - delta = evt.angleDelta().y() - if evt.modifiers() & Qt.KeyboardModifier.ControlModifier: - if delta < 0: - self.scaleDown() - elif delta > 0: - self.scaleUp() - evt.accept() - - elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier: - if delta < 0: - self.backward() - elif delta > 0: - self.forward() - evt.accept() - - def _gestureEvent(self, evt): - """ - Protected method handling gesture events. - - @param evt reference to the gesture event - @type QGestureEvent - """ - pinch = evt.gesture(Qt.GestureType.PinchGesture) - if pinch: - if pinch.state() == Qt.GestureState.GestureStarted: - pinch.setTotalScaleFactor(self.__currentScale / 100.0) - elif pinch.state() == Qt.GestureState.GestureUpdated: - scaleFactor = pinch.totalScaleFactor() - self.setScale(int(scaleFactor * 100)) - evt.accept() - - def event(self, evt): - """ - Public method handling events. - - @param evt reference to the event (QEvent) - @return flag indicating, if the event was handled (boolean) - """ - if evt.type() == QEvent.Type.Gesture: - self._gestureEvent(evt) - return True - - return super().event(evt) - - ####################################################################### - ## Context menu related methods below - ####################################################################### - # TODO: implement context menu - - def contextMenuEvent(self, evt): - """ - Protected method called to create a context menu. - - This method is overridden from QWebEngineView. - - @param evt reference to the context menu event object - @type QContextMenuEvent - """ - pos = evt.pos() - reason = evt.reason() - QTimer.singleShot( - 0, - lambda: self._contextMenuEvent(QContextMenuEvent(reason, pos))) - # needs to be done this way because contextMenuEvent is blocking - # the main loop - - def _contextMenuEvent(self, evt): - """ - Protected method called to create a context menu. - - @param evt reference to the context menu event object - (QContextMenuEvent) - """ - self.__menu.clear() - - self.__createContextMenu(self.__menu) - - if not self.__menu.isEmpty(): - pos = evt.globalPos() - self.__menu.popup(QPoint(pos.x(), pos.y() + 1)) - - def __createContextMenu(self, menu): - """ - Private method to populate the context menu. - - @param menu reference to the menu to be populated - @type QMenu - """ - contextMenuData = self.lastContextMenuRequest() - - act = menu.addAction( - UI.PixmapCache.getIcon("back"), - self.tr("Backward"), - self.backward) - act.setEnabled(self.isBackwardAvailable()) - - act = menu.addAction( - UI.PixmapCache.getIcon("forward"), - self.tr("Forward"), - self.forward) - act.setEnabled(self.isForwardAvailable()) - - act = menu.addAction( - UI.PixmapCache.getIcon("reload"), - self.tr("Reload"), - self.reload) - - if ( - not contextMenuData.linkUrl().isEmpty() and - contextMenuData.linkUrl().scheme() != "javascript" - ): - self.__createLinkContextMenu(menu, contextMenuData) - - menu.addSeparator() - - act = menu.addAction( - UI.PixmapCache.getIcon("editCopy"), - self.tr("Copy Page URL to Clipboard")) - act.setData(self.url()) - act.triggered.connect( - lambda: self.__copyLink(act)) - - menu.addSeparator() - - act = menu.addAction( - UI.PixmapCache.getIcon("zoomIn"), - self.tr("Zoom in"), - self.scaleUp) - act.setEnabled(self.isScaleUpAvailable()) - - act = menu.addAction( - UI.PixmapCache.getIcon("zoomOut"), - self.tr("Zoom out"), - self.scaleDown) - act.setEnabled(self.isScaleDownAvailable()) - - menu.addAction( - UI.PixmapCache.getIcon("zoomReset"), - self.tr("Zoom reset"), - self.resetScale) - - menu.addSeparator() - - act = menu.addAction( - UI.PixmapCache.getIcon("editCopy"), - self.tr("Copy")) - act.setData(contextMenuData.selectedText()) - act.triggered.connect( lambda: self.__copyText(act)) - act.setEnabled(bool(contextMenuData.selectedText())) - - menu.addAction( - UI.PixmapCache.getIcon("editSelectAll"), - self.tr("Select All"), - self.__selectAll) - - menu.addSeparator() - - menu.addAction( - UI.PixmapCache.getIcon("tabClose"), - self.tr('Close'), - self.__closePage) - - act = menu.addAction( - UI.PixmapCache.getIcon("tabCloseOther"), - self.tr("Close Others"), - self.__closeOtherPages) - act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1) - - def __createLinkContextMenu(self, menu, contextMenuData): - """ - Private method to populate the context menu for URLs. - - @param menu reference to the menu to be populated - @type QMenu - @param contextMenuData data of the last context menu request - @type QWebEngineContextMenuRequest - """ - if not menu.isEmpty(): - menu.addSeparator() - - act = menu.addAction( - UI.PixmapCache.getIcon("openNewTab"), - self.tr("Open Link in New Page")) - act.setData(contextMenuData.linkUrl()) - act.triggered.connect( - lambda: self.__openLinkInNewPage(act)) - - act = menu.addAction( - UI.PixmapCache.getIcon("newWindow"), - self.tr("Open Link in Background Page")) - act.setData(contextMenuData.linkUrl()) - act.triggered.connect( - lambda: self.__openLinkInBackgroundPage(act)) - - menu.addSeparator() - - act = menu.addAction( - UI.PixmapCache.getIcon("editCopy"), - self.tr("Copy URL to Clipboard")) - act.setData(contextMenuData.linkUrl()) - act.triggered.connect( - lambda: self.__copyLink(act)) - - def __openLinkInNewPage(self, act): - """ - Private method called by the context menu to open a link in a new page. - - @param act reference to the action that triggered - @type QAction - """ - # TODO: not yet implemented - - def __openLinkInBackgroundPage(self, act): - """ - Private method called by the context menu to open a link in a - background page. - - @param act reference to the action that triggered - @type QAction - """ - # TODO: not yet implemented - - def __copyLink(self, act): - """ - Private method called by the context menu to copy a link to the - clipboard. - - @param act reference to the action that triggered - @type QAction - """ - # TODO: not yet implemented - - def __copyText(self, act): - """ - Private method called by the context menu to copy selected text to the - clipboard. - - @param act reference to the action that triggered - @type QAction - """ - # TODO: not yet implemented - - def __selectAll(self): - """ - Private method called by the context menu to select all text. - """ - # TODO: not yet implemented - - def __closePage(self): - """ - Private method called by the context menu to close the current page. - """ - # TODO: not yet implemented - - def __closeOtherPages(self): - """ - Private method called by the context menu to close all other pages. - """ - # TODO: not yet implemented
--- a/eric7/HelpViewer/HelpViewerWidget.py Sun Oct 17 15:26:01 2021 +0200 +++ b/eric7/HelpViewer/HelpViewerWidget.py Sun Oct 17 15:26:31 2021 +0200 @@ -202,7 +202,7 @@ self.__helpTocButton = self.__addNavigationButton( "tableOfContents", self.tr("Show the table of contents")) self.__helpIndexButton = self.__addNavigationButton( - "helpIndex", self.tr("Show the help document index")) + "helpIndex", self.tr("Show the help document index")) self.__helpSearchButton = self.__addNavigationButton( "documentFind", self.tr("Show the help search window")) self.__openPagesButton.setChecked(True) @@ -394,9 +394,9 @@ self.tr("HTML Files (*.htm *.html);;All Files (*)") ) if htmlFile: - self.currentViewer().setUrl(QUrl.fromLocalFile(htmlFile)) + self.currentViewer().setLink(QUrl.fromLocalFile(htmlFile)) - def addPage(self, url=QUrl("about:blank"), background=False): + def addPage(self, url=None, background=False): """ Public method to add a new help page with the given URL. @@ -405,23 +405,28 @@ @param background flag indicating to open the page in the background (defaults to False) @type bool (optional) + @return reference to the created page + @rtype HelpViewerImpl """ + if url is None: + url = QUrl("about:blank") + viewer = self.__newViewer() viewer.setUrl(url) - if background: - cv = self.currentViewer() - if cv: - index = self.__helpStack.indexOf(cv) + 1 - self.__helpStack.insertWidget(index, viewer) - self.__openPagesList.insertPage( - index, viewer, background=background) - cv.setFocus(Qt.FocusReason.OtherFocusReason) - return + cv = self.currentViewer() + if background and bool(cv): + index = self.__helpStack.indexOf(cv) + 1 + self.__helpStack.insertWidget(index, viewer) + self.__openPagesList.insertPage( + index, viewer, background=background) + cv.setFocus(Qt.FocusReason.OtherFocusReason) + else: + self.__helpStack.addWidget(viewer) + self.__openPagesList.addPage(viewer, background=background) + viewer.setFocus(Qt.FocusReason.OtherFocusReason) - self.__helpStack.addWidget(viewer) - self.__openPagesList.addPage(viewer, background=background) - viewer.setFocus(Qt.FocusReason.OtherFocusReason) + return viewer @pyqtSlot(QUrl) def openUrl(self, url): @@ -433,7 +438,7 @@ """ cv = self.currentViewer() if cv: - cv.setUrl(url) + cv.setLink(url) cv.setFocus(Qt.FocusReason.OtherFocusReason) @pyqtSlot(QUrl) @@ -457,6 +462,27 @@ self.addPage(url=url, background=True) @pyqtSlot() + def closeCurrentPage(self): + """ + Public slot to close the current page. + """ + self.__openPagesList.closeCurrentPage() + + @pyqtSlot() + def closeOtherPages(self): + """ + Public slot to close all other pages. + """ + self.__openPagesList.closeOtherPages() + + @pyqtSlot() + def closeAllPages(self): + """ + Public slot to close all pages. + """ + self.__openPagesList.closeAllPages() + + @pyqtSlot() def __activateCurrentPage(self): """ Private slot to activate the current page. @@ -473,11 +499,11 @@ @rtype HelpViewerImpl """ if WEBENGINE_AVAILABLE: - from .HelpViewerImpl_qwe import HelpViewerImpl_qwe - viewer = HelpViewerImpl_qwe(self.__helpEngine, self) + from .HelpViewerImplQWE import HelpViewerImplQWE + viewer = HelpViewerImplQWE(self.__helpEngine, self) else: - from .HelpViewerImpl_qtb import HelpViewerImpl_qtb - viewer = HelpViewerImpl_qtb(self.__helpEngine, self) + from .HelpViewerImplQTB import HelpViewerImplQTB + viewer = HelpViewerImplQTB(self.__helpEngine, self) viewer.zoomChanged.connect(self.__checkActionButtons) @@ -511,7 +537,7 @@ self.__helpEngine.setupData() self.__removeOldDocumentation() - def __getQtHelpCollectionFileName(cls): + def __getQtHelpCollectionFileName(self): """ Private method to determine the name of the QtHelp collection file. @@ -906,14 +932,14 @@ self.__helpEngine.filterEngine().setActiveFilter(helpFilter) @pyqtSlot(str) - def __currentFilterChanged(self, filter): + def __currentFilterChanged(self, filter_): """ Private slot handling a change of the active QtHelp filter. - @param filter filter name + @param filter_ filter name @type str """ - index = self.__helpFilterCombo.findData(filter) + index = self.__helpFilterCombo.findData(filter_) if index < 0: index = 0 self.__helpFilterCombo.setCurrentIndex(index)
--- a/eric7/HelpViewer/OpenPagesWidget.py Sun Oct 17 15:26:01 2021 +0200 +++ b/eric7/HelpViewer/OpenPagesWidget.py Sun Oct 17 15:26:31 2021 +0200 @@ -19,6 +19,9 @@ class OpenPagesWidget(QWidget): """ Class implementing a widget showing the list of open pages. + + @signal currentChanged(index) emitted to signal a change of the current + page index """ currentChanged = pyqtSignal(int) @@ -134,7 +137,7 @@ (defaults to False) @type bool (optional) """ - self.__openPagesList.addItem(viewer.title()) + self.__openPagesList.addItem(viewer.pageTitle()) viewer.titleChanged.connect( lambda: self.__viewerTitleChanged(viewer)) @@ -157,7 +160,7 @@ @type bool (optional) """ currentRow = self.__openPagesList.currentRow() - self.__openPagesList.insertItem(index, viewer.title()) + self.__openPagesList.insertItem(index, viewer.pageTitle()) viewer.titleChanged.connect( lambda: self.__viewerTitleChanged(viewer)) @@ -175,7 +178,7 @@ """ index = self.__stack.indexOf(viewer) itm = self.__openPagesList.item(index) - itm.setText(viewer.title()) + itm.setText(viewer.pageTitle()) self.currentChanged.emit(index) ####################################################################### @@ -185,32 +188,23 @@ @pyqtSlot() def __contextMenuClose(self): """ - Private slot to close a page. + Private slot to close a page via the context menu. """ - row = self.__openPagesList.currentRow() - self.__removeViewer(row) - - if self.__openPagesList.count() == 0: - self.__helpViewer.addPage() + self.closeCurrentPage() @pyqtSlot() def __contextMenuCloseOthers(self): """ - Private slot to close all other pages. + Private slot to close all other pages via the context menu. """ - currentRow = self.__openPagesList.currentRow() - for row in range(self.__openPagesList.count() - 1, -1, -1): - if row != currentRow: - self.__removeViewer(row) + self.closeOtherPages() @pyqtSlot() def __contextMenuCloseAll(self): """ - Private slot to close all pages. + Private slot to close all pages via the context menu. """ - while self.__openPagesList.count() != 0: - self.__removeViewer(0) - self.__helpViewer.addPage() + self.closeAllPages() @pyqtSlot() def __contextMenuCopyUrlToClipboard(self): @@ -219,7 +213,7 @@ """ row = self.__openPagesList.currentRow() viewer = self.__stack.widget(row) - url = viewer.url() + url = viewer.link() if url.isValid(): urlStr = url.toString() @@ -242,3 +236,37 @@ itm = self.__openPagesList.takeItem(row) del itm + + ####################################################################### + ## Slots for external access below + ####################################################################### + + @pyqtSlot() + def closeCurrentPage(self): + """ + Public slot to close the current page. + """ + row = self.__openPagesList.currentRow() + self.__removeViewer(row) + + if self.__openPagesList.count() == 0: + self.__helpViewer.addPage() + + @pyqtSlot() + def closeOtherPages(self): + """ + Public slot to close all other pages. + """ + currentRow = self.__openPagesList.currentRow() + for row in range(self.__openPagesList.count() - 1, -1, -1): + if row != currentRow: + self.__removeViewer(row) + + @pyqtSlot() + def closeAllPages(self): + """ + Public slot to close all pages. + """ + while self.__openPagesList.count() != 0: + self.__removeViewer(0) + self.__helpViewer.addPage()