src/eric7/HelpViewer/HelpViewerImplQWE.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 9172
4bac907a4c74
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2021 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing the help viewer base class.
8 """
9
10 import contextlib
11 import functools
12
13 from PyQt6.QtCore import pyqtSlot, Qt, QEvent, QTimer, QUrl, QPoint
14 from PyQt6.QtGui import QGuiApplication, QClipboard, QContextMenuEvent
15 from PyQt6.QtWidgets import QMenu
16 from PyQt6.QtWebEngineWidgets import QWebEngineView
17 from PyQt6.QtWebEngineCore import QWebEnginePage, QWebEngineNewWindowRequest
18
19 from .HelpViewerWidget import HelpViewerWidget
20 from .HelpViewerImpl import HelpViewerImpl
21
22 import UI.PixmapCache
23
24
25 class HelpViewerImplQWE(HelpViewerImpl, QWebEngineView):
26 """
27 Class implementing the QTextBrowser based help viewer class.
28 """
29 ZoomLevels = [
30 30, 40, 50, 67, 80, 90,
31 100,
32 110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300,
33 ]
34 ZoomLevelDefault = 100
35
36 def __init__(self, engine, parent=None):
37 """
38 Constructor
39
40 @param engine reference to the help engine
41 @type QHelpEngine
42 @param parent reference to the parent widget
43 @type QWidget
44 """
45 QWebEngineView.__init__(self, parent=parent)
46 HelpViewerImpl.__init__(self, engine)
47
48 self.__helpViewerWidget = parent
49
50 self.__rwhvqt = None
51 self.installEventFilter(self)
52
53 self.__page = None
54 self.__createNewPage()
55
56 self.__currentScale = 100
57
58 self.__menu = QMenu(self)
59
60 def __createNewPage(self):
61 """
62 Private method to create a new page object.
63 """
64 self.__page = QWebEnginePage(self.__helpViewerWidget.webProfile())
65 self.setPage(self.__page)
66
67 self.__page.titleChanged.connect(self.__titleChanged)
68 self.__page.urlChanged.connect(self.__titleChanged)
69 self.__page.newWindowRequested.connect(self.__newWindowRequested)
70
71 def __newWindowRequested(self, request):
72 """
73 Private slot handling new window requests of the web page.
74
75 @param request reference to the new window request
76 @type QWebEngineNewWindowRequest
77 """
78 background = (
79 request.destination() ==
80 QWebEngineNewWindowRequest.DestinationType.InNewBackgroundTab
81 )
82 newViewer = self.__helpViewerWidget.addPage(background=background)
83 request.openIn(newViewer.page())
84
85 def __setRwhvqt(self):
86 """
87 Private slot to set widget that receives input events.
88 """
89 self.grabGesture(Qt.GestureType.PinchGesture)
90 self.__rwhvqt = self.focusProxy()
91 if self.__rwhvqt:
92 self.__rwhvqt.grabGesture(Qt.GestureType.PinchGesture)
93 self.__rwhvqt.installEventFilter(self)
94 else:
95 print("Focus proxy is null!") # __IGNORE_WARNING_M801__
96
97 def setLink(self, url):
98 """
99 Public method to set the URL of the document to be shown.
100
101 @param url URL of the document
102 @type QUrl
103 """
104 if url.toString() == "about:blank":
105 self.setHtml(self.__helpViewerWidget.emptyDocument())
106 else:
107 super().setUrl(url)
108
109 def link(self):
110 """
111 Public method to get the URL of the shown document.
112
113 @return url URL of the document
114 @rtype QUrl
115 """
116 return super().url()
117
118 @pyqtSlot()
119 def __titleChanged(self):
120 """
121 Private method to handle a change of the web page title.
122 """
123 super().titleChanged.emit()
124
125 def pageTitle(self):
126 """
127 Public method get the page title.
128
129 @return page title
130 @rtype str
131 """
132 titleStr = super().title()
133 if not titleStr:
134 if self.link().isEmpty():
135 url = self.__page.requestedUrl()
136 else:
137 url = self.link()
138
139 titleStr = url.host()
140 if not titleStr:
141 titleStr = url.toString(
142 QUrl.UrlFormattingOption.RemoveFragment)
143
144 if not titleStr or titleStr == "about:blank":
145 titleStr = self.tr("Empty Page")
146
147 return titleStr
148
149 def isEmptyPage(self):
150 """
151 Public method to check, if the current page is the empty page.
152
153 @return flag indicating an empty page is loaded
154 @rtype bool
155 """
156 return self.pageTitle() == self.tr("Empty Page")
157
158 #######################################################################
159 ## History related methods below
160 #######################################################################
161
162 def isBackwardAvailable(self):
163 """
164 Public method to check, if stepping backward through the history is
165 available.
166
167 @return flag indicating backward stepping is available
168 @rtype bool
169 """
170 return self.history().canGoBack()
171
172 def isForwardAvailable(self):
173 """
174 Public method to check, if stepping forward through the history is
175 available.
176
177 @return flag indicating forward stepping is available
178 @rtype bool
179 """
180 return self.history().canGoForward()
181
182 def backward(self):
183 """
184 Public slot to move backwards in history.
185 """
186 self.triggerPageAction(QWebEnginePage.WebAction.Back)
187
188 def forward(self):
189 """
190 Public slot to move forward in history.
191 """
192 self.triggerPageAction(QWebEnginePage.WebAction.Forward)
193
194 def reload(self):
195 """
196 Public slot to reload the current page.
197 """
198 self.triggerPageAction(QWebEnginePage.WebAction.Reload)
199
200 def backwardHistoryCount(self):
201 """
202 Public method to get the number of available back history items.
203
204 Note: For performance reasons this is limited to the maximum number of
205 history items the help viewer is interested in.
206
207 @return count of available back history items
208 @rtype int
209 """
210 history = self.history()
211 return len(history.backItems(HelpViewerWidget.MaxHistoryItems))
212
213 def forwardHistoryCount(self):
214 """
215 Public method to get the number of available forward history items.
216
217 Note: For performance reasons this is limited to the maximum number of
218 history items the help viewer is interested in.
219
220 @return count of available forward history items
221 @rtype int
222 """
223 history = self.history()
224 return len(history.forwardItems(HelpViewerWidget.MaxHistoryItems))
225
226 def historyTitle(self, offset):
227 """
228 Public method to get the title of a history item.
229
230 @param offset offset of the item with respect to the current page
231 @type int
232 @return title of the requeted item in history
233 @rtype str
234 """
235 history = self.history()
236 currentIndex = history.currentItemIndex()
237 itm = self.history().itemAt(currentIndex + offset)
238 return itm.title()
239
240 def gotoHistory(self, offset):
241 """
242 Public method to go to a history item.
243
244 @param offset offset of the item with respect to the current page
245 @type int
246 """
247 history = self.history()
248 currentIndex = history.currentItemIndex()
249 itm = self.history().itemAt(currentIndex + offset)
250 history.goToItem(itm)
251
252 def clearHistory(self):
253 """
254 Public method to clear the history.
255 """
256 self.history().clear()
257
258 #######################################################################
259 ## Zoom related methods below
260 #######################################################################
261
262 def __levelForScale(self, scale):
263 """
264 Private method determining the zoom level index given a zoom factor.
265
266 @param scale zoom factor
267 @type int
268 @return index of zoom factor
269 @rtype int
270 """
271 try:
272 index = self.ZoomLevels.index(scale)
273 except ValueError:
274 for _index in range(len(self.ZoomLevels)):
275 if scale <= self.ZoomLevels[scale]:
276 break
277 return index
278
279 def scaleUp(self):
280 """
281 Public method to zoom in.
282 """
283 index = self.__levelForScale(self.__currentScale)
284 if index < len(self.ZoomLevels) - 1:
285 self.setScale(self.ZoomLevels[index + 1])
286
287 def scaleDown(self):
288 """
289 Public method to zoom out.
290 """
291 index = self.__levelForScale(self.__currentScale)
292 if index > 0:
293 self.setScale(self.ZoomLevels[index - 1])
294
295 def setScale(self, scale):
296 """
297 Public method to set the zoom level.
298
299 @param scale zoom level to set
300 @type int
301 """
302 if scale != self.__currentScale:
303 self.setZoomFactor(scale / 100.0)
304 self.__currentScale = scale
305 self.zoomChanged.emit()
306
307 def resetScale(self):
308 """
309 Public method to reset the zoom level.
310 """
311 index = self.__levelForScale(self.ZoomLevelDefault)
312 self.setScale(self.ZoomLevels[index])
313
314 def scale(self):
315 """
316 Public method to get the zoom level.
317
318 @return current zoom level
319 @rtype int
320 """
321 return self.__currentScale
322
323 def isScaleUpAvailable(self):
324 """
325 Public method to check, if the max. zoom level is reached.
326
327 @return flag indicating scale up is available
328 @rtype bool
329 """
330 index = self.__levelForScale(self.__currentScale)
331 return index < len(self.ZoomLevels) - 1
332
333 def isScaleDownAvailable(self):
334 """
335 Public method to check, if the min. zoom level is reached.
336
337 @return flag indicating scale down is available
338 @rtype bool
339 """
340 index = self.__levelForScale(self.__currentScale)
341 return index > 0
342
343 #######################################################################
344 ## Event handlers below
345 #######################################################################
346
347 def eventFilter(self, obj, evt):
348 """
349 Public method to process event for other objects.
350
351 @param obj reference to object to process events for
352 @type QObject
353 @param evt reference to event to be processed
354 @type QEvent
355 @return flag indicating that the event should be filtered out
356 @rtype bool
357 """
358 if (
359 obj is self and
360 evt.type() == QEvent.Type.ParentChange and
361 self.parentWidget() is not None
362 ):
363 self.parentWidget().installEventFilter(self)
364
365 # find the render widget receiving events for the web page
366 if obj is self and evt.type() == QEvent.Type.ChildAdded:
367 QTimer.singleShot(0, self.__setRwhvqt)
368
369 # forward events to WebBrowserView
370 if (
371 obj is self.__rwhvqt and
372 evt.type() in [QEvent.Type.KeyPress,
373 QEvent.Type.MouseButtonRelease,
374 QEvent.Type.Wheel,
375 QEvent.Type.Gesture]
376 ):
377 wasAccepted = evt.isAccepted()
378 evt.setAccepted(False)
379 if evt.type() == QEvent.Type.KeyPress:
380 self._keyPressEvent(evt)
381 elif evt.type() == QEvent.Type.MouseButtonRelease:
382 self._mouseReleaseEvent(evt)
383 elif evt.type() == QEvent.Type.Wheel:
384 self._wheelEvent(evt)
385 elif evt.type() == QEvent.Type.Gesture:
386 self._gestureEvent(evt)
387 ret = evt.isAccepted()
388 evt.setAccepted(wasAccepted)
389 return ret
390
391 if (
392 obj is self.parentWidget() and
393 evt.type() in [QEvent.Type.KeyPress, QEvent.Type.KeyRelease]
394 ):
395 wasAccepted = evt.isAccepted()
396 evt.setAccepted(False)
397 if evt.type() == QEvent.Type.KeyPress:
398 self._keyPressEvent(evt)
399 ret = evt.isAccepted()
400 evt.setAccepted(wasAccepted)
401 return ret
402
403 # block already handled events
404 if (
405 obj is self and
406 evt.type() in [QEvent.Type.KeyPress,
407 QEvent.Type.MouseButtonRelease,
408 QEvent.Type.Wheel,
409 QEvent.Type.Gesture]
410 ):
411 return True
412
413 return super().eventFilter(obj, evt)
414
415 def _keyPressEvent(self, evt):
416 """
417 Protected method called by a key press.
418
419 @param evt reference to the key event
420 @type QKeyEvent
421 """
422 key = evt.key()
423 isControlModifier = (
424 evt.modifiers() == Qt.KeyboardModifier.ControlModifier
425 )
426
427 if (
428 key == Qt.Key.Key_ZoomIn or
429 (key == Qt.Key.Key_Plus and isControlModifier)
430 ):
431 self.scaleUp()
432 evt.accept()
433 elif (
434 key == Qt.Key.Key_ZoomOut or
435 (key == Qt.Key.Key_Minus and isControlModifier)
436 ):
437 self.scaleDown()
438 evt.accept()
439 elif key == Qt.Key.Key_0 and isControlModifier:
440 self.resetScale()
441 evt.accept()
442 elif (
443 key == Qt.Key.Key_Backspace or
444 (key == Qt.Key.Key_Left and isControlModifier)
445 ):
446 self.backward()
447 evt.accept()
448 elif key == Qt.Key.Key_Right and isControlModifier:
449 self.forward()
450 evt.accept()
451 elif key == Qt.Key.Key_F and isControlModifier:
452 self.__helpViewerWidget.showHideSearch(True)
453 evt.accept()
454 elif (
455 key == Qt.Key.Key_F3 and
456 evt.modifiers() == Qt.KeyboardModifier.NoModifier
457 ):
458 self.__helpViewerWidget.searchNext()
459 evt.accept()
460 elif (
461 key == Qt.Key.Key_F3 and
462 evt.modifiers() == Qt.KeyboardModifier.ShiftModifier
463 ):
464 self.__helpViewerWidget.searchPrev()
465 evt.accept()
466
467 def _mouseReleaseEvent(self, evt):
468 """
469 Protected method called by a mouse release event.
470
471 @param evt reference to the mouse event
472 @type QMouseEvent
473 """
474 accepted = evt.isAccepted()
475 self.__page.event(evt)
476 if (
477 not evt.isAccepted() and
478 evt.button() == Qt.MouseButton.MiddleButton
479 ):
480 url = QUrl(QGuiApplication.clipboard().text(
481 QClipboard.Mode.Selection))
482 if (
483 not url.isEmpty() and
484 url.isValid() and
485 url.scheme() != ""
486 ):
487 self.setLink(url)
488 accepted = True
489 evt.setAccepted(accepted)
490
491 def _wheelEvent(self, evt):
492 """
493 Protected method to handle wheel events.
494
495 @param evt reference to the wheel event
496 @type QWheelEvent
497 """
498 delta = evt.angleDelta().y()
499 if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
500 if delta < 0:
501 self.scaleDown()
502 elif delta > 0:
503 self.scaleUp()
504 evt.accept()
505
506 elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier:
507 if delta < 0:
508 self.backward()
509 elif delta > 0:
510 self.forward()
511 evt.accept()
512
513 def _gestureEvent(self, evt):
514 """
515 Protected method handling gesture events.
516
517 @param evt reference to the gesture event
518 @type QGestureEvent
519 """
520 pinch = evt.gesture(Qt.GestureType.PinchGesture)
521 if pinch:
522 if pinch.state() == Qt.GestureState.GestureStarted:
523 pinch.setTotalScaleFactor(self.__currentScale / 100.0)
524 elif pinch.state() == Qt.GestureState.GestureUpdated:
525 scaleFactor = pinch.totalScaleFactor()
526 self.setScale(int(scaleFactor * 100))
527 evt.accept()
528
529 def event(self, evt):
530 """
531 Public method handling events.
532
533 @param evt reference to the event (QEvent)
534 @return flag indicating, if the event was handled (boolean)
535 """
536 if evt.type() == QEvent.Type.Gesture:
537 self._gestureEvent(evt)
538 return True
539
540 return super().event(evt)
541
542 #######################################################################
543 ## Context menu related methods below
544 #######################################################################
545
546 def contextMenuEvent(self, evt):
547 """
548 Protected method called to create a context menu.
549
550 This method is overridden from QWebEngineView.
551
552 @param evt reference to the context menu event object
553 @type QContextMenuEvent
554 """
555 pos = evt.pos()
556 reason = evt.reason()
557 QTimer.singleShot(
558 0,
559 lambda: self._contextMenuEvent(QContextMenuEvent(reason, pos)))
560 # needs to be done this way because contextMenuEvent is blocking
561 # the main loop
562
563 def _contextMenuEvent(self, evt):
564 """
565 Protected method called to create a context menu.
566
567 @param evt reference to the context menu event object
568 (QContextMenuEvent)
569 """
570 self.__menu.clear()
571
572 self.__createContextMenu(self.__menu)
573
574 if not self.__menu.isEmpty():
575 pos = evt.globalPos()
576 self.__menu.popup(QPoint(pos.x(), pos.y() + 1))
577
578 def __createContextMenu(self, menu):
579 """
580 Private method to populate the context menu.
581
582 @param menu reference to the menu to be populated
583 @type QMenu
584 """
585 contextMenuData = self.lastContextMenuRequest()
586
587 act = menu.addAction(
588 UI.PixmapCache.getIcon("back"),
589 self.tr("Backward"),
590 self.backward)
591 act.setEnabled(self.isBackwardAvailable())
592
593 act = menu.addAction(
594 UI.PixmapCache.getIcon("forward"),
595 self.tr("Forward"),
596 self.forward)
597 act.setEnabled(self.isForwardAvailable())
598
599 act = menu.addAction(
600 UI.PixmapCache.getIcon("reload"),
601 self.tr("Reload"),
602 self.reload)
603
604 if (
605 not contextMenuData.linkUrl().isEmpty() and
606 contextMenuData.linkUrl().scheme() != "javascript"
607 ):
608 self.__createLinkContextMenu(menu, contextMenuData)
609
610 menu.addSeparator()
611
612 act = menu.addAction(
613 UI.PixmapCache.getIcon("editCopy"),
614 self.tr("Copy Page URL to Clipboard"))
615 act.setData(self.link())
616 act.triggered.connect(
617 functools.partial(self.__copyLink, act))
618
619 act = menu.addAction(
620 UI.PixmapCache.getIcon("bookmark22"),
621 self.tr("Bookmark Page"))
622 act.setData({
623 "title": self.pageTitle(),
624 "url": self.link()
625 })
626 act.triggered.connect(
627 functools.partial(self.__bookmarkPage, act))
628
629 menu.addSeparator()
630
631 act = menu.addAction(
632 UI.PixmapCache.getIcon("zoomIn"),
633 self.tr("Zoom in"),
634 self.scaleUp)
635 act.setEnabled(self.isScaleUpAvailable())
636
637 act = menu.addAction(
638 UI.PixmapCache.getIcon("zoomOut"),
639 self.tr("Zoom out"),
640 self.scaleDown)
641 act.setEnabled(self.isScaleDownAvailable())
642
643 menu.addAction(
644 UI.PixmapCache.getIcon("zoomReset"),
645 self.tr("Zoom reset"),
646 self.resetScale)
647
648 menu.addSeparator()
649
650 act = menu.addAction(
651 UI.PixmapCache.getIcon("editCopy"),
652 self.tr("Copy"),
653 self.__copyText)
654 act.setEnabled(bool(contextMenuData.selectedText()))
655
656 menu.addAction(
657 UI.PixmapCache.getIcon("editSelectAll"),
658 self.tr("Select All"),
659 self.__selectAll)
660
661 menu.addSeparator()
662
663 menu.addAction(
664 UI.PixmapCache.getIcon("tabClose"),
665 self.tr('Close'),
666 self.__closePage)
667
668 act = menu.addAction(
669 UI.PixmapCache.getIcon("tabCloseOther"),
670 self.tr("Close Others"),
671 self.__closeOtherPages)
672 act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1)
673
674 def __createLinkContextMenu(self, menu, contextMenuData):
675 """
676 Private method to populate the context menu for URLs.
677
678 @param menu reference to the menu to be populated
679 @type QMenu
680 @param contextMenuData data of the last context menu request
681 @type QWebEngineContextMenuRequest
682 """
683 if not menu.isEmpty():
684 menu.addSeparator()
685
686 act = menu.addAction(
687 UI.PixmapCache.getIcon("openNewTab"),
688 self.tr("Open Link in New Page"))
689 act.setData(contextMenuData.linkUrl())
690 act.triggered.connect(
691 functools.partial(self.__openLinkInNewPage, act))
692
693 act = menu.addAction(
694 UI.PixmapCache.getIcon("newWindow"),
695 self.tr("Open Link in Background Page"))
696 act.setData(contextMenuData.linkUrl())
697 act.triggered.connect(
698 functools.partial(self.__openLinkInBackgroundPage, act))
699
700 menu.addSeparator()
701
702 act = menu.addAction(
703 UI.PixmapCache.getIcon("editCopy"),
704 self.tr("Copy URL to Clipboard"))
705 act.setData(contextMenuData.linkUrl())
706 act.triggered.connect(
707 functools.partial(self.__copyLink, act))
708
709 def __openLinkInNewPage(self, act):
710 """
711 Private method called by the context menu to open a link in a new page.
712
713 @param act reference to the action that triggered
714 @type QAction
715 """
716 url = act.data()
717 if url.isEmpty():
718 return
719
720 self.__helpViewerWidget.openUrlNewPage(url)
721
722 def __openLinkInBackgroundPage(self, act):
723 """
724 Private method called by the context menu to open a link in a
725 background page.
726
727 @param act reference to the action that triggered
728 @type QAction
729 """
730 url = act.data()
731 if url.isEmpty():
732 return
733
734 self.__helpViewerWidget.openUrlNewBackgroundPage(url)
735
736 def __bookmarkPage(self, act):
737 """
738 Private method called by the context menu to bookmark the page.
739
740 @param act reference to the action that triggered
741 @type QAction
742 """
743 data = act.data()
744 if data:
745 with contextlib.suppress(KeyError):
746 url = data["url"]
747 title = data["title"]
748
749 self.__helpViewerWidget.bookmarkPage(title, url)
750
751 def __copyLink(self, act):
752 """
753 Private method called by the context menu to copy a link to the
754 clipboard.
755
756 @param act reference to the action that triggered
757 @type QAction
758 """
759 data = act.data()
760 if isinstance(data, QUrl) and data.isEmpty():
761 return
762
763 if isinstance(data, QUrl):
764 data = data.toString()
765
766 # copy the URL to both clipboard areas
767 QGuiApplication.clipboard().setText(data, QClipboard.Mode.Clipboard)
768 QGuiApplication.clipboard().setText(data, QClipboard.Mode.Selection)
769
770 def __copyText(self):
771 """
772 Private method called by the context menu to copy selected text to the
773 clipboard.
774 """
775 self.triggerPageAction(QWebEnginePage.WebAction.Copy)
776
777 def __selectAll(self):
778 """
779 Private method called by the context menu to select all text.
780 """
781 self.triggerPageAction(QWebEnginePage.WebAction.SelectAll)
782
783 def __closePage(self):
784 """
785 Private method called by the context menu to close the current page.
786 """
787 self.__helpViewerWidget.closeCurrentPage()
788
789 def __closeOtherPages(self):
790 """
791 Private method called by the context menu to close all other pages.
792 """
793 self.__helpViewerWidget.closeOtherPages()

eric ide

mercurial