eric7/HelpViewer/HelpViewerImpl_qwe.py

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

eric ide

mercurial