eric7/HelpViewer/HelpViewerImplQWE.py

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

eric ide

mercurial