--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/eric7/HelpViewer/HelpViewerImpl_qwe.py Sat Oct 16 20:38:23 2021 +0200 @@ -0,0 +1,688 @@ +# -*- 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