Sat, 16 Oct 2021 20:41:44 +0200
Fixed a stupid issue.
# -*- 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