src/eric7/HelpViewer/HelpViewerImplQTB.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 QTextBrowser based help viewer class.
8 """
9
10 import contextlib
11 import functools
12
13 from PyQt6.QtCore import (
14 pyqtSlot, Qt, QByteArray, QUrl, QEvent, QCoreApplication, QPoint
15 )
16 from PyQt6.QtGui import QDesktopServices, QImage, QGuiApplication, QClipboard
17 from PyQt6.QtWidgets import QTextBrowser, QMenu
18
19 from .HelpViewerImpl import HelpViewerImpl
20
21 import UI.PixmapCache
22
23
24 AboutBlank = QCoreApplication.translate(
25 "HelpViewer",
26 "<html>"
27 "<head><title>about:blank</title></head>"
28 "<body></body>"
29 "</html>")
30
31 PageNotFound = QCoreApplication.translate(
32 "HelpViewer",
33 """<html>"""
34 """<head><title>Error 404...</title></head>"""
35 """<body><div align="center"><br><br>"""
36 """<h1>The page could not be found</h1><br>"""
37 """<h3>'{0}'</h3></div></body>"""
38 """</html>""")
39
40
41 class HelpViewerImplQTB(HelpViewerImpl, QTextBrowser):
42 """
43 Class implementing the QTextBrowser based help viewer class.
44 """
45 def __init__(self, engine, parent=None):
46 """
47 Constructor
48
49 @param engine reference to the help engine
50 @type QHelpEngine
51 @param parent reference to the parent widget
52 @type QWidget
53 """
54 QTextBrowser.__init__(self, parent=parent)
55 HelpViewerImpl.__init__(self, engine)
56
57 self.__helpViewerWidget = parent
58
59 self.__zoomCount = 0
60
61 self.__menu = QMenu(self)
62 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
63 self.customContextMenuRequested.connect(self.__showContextMenu)
64
65 self.sourceChanged.connect(self.titleChanged)
66
67 self.grabGesture(Qt.GestureType.PinchGesture)
68
69 def setLink(self, url):
70 """
71 Public method to set the URL of the document to be shown.
72
73 @param url source of the document
74 @type QUrl
75 """
76 if url.toString() == "about:blank":
77 self.setHtml(self.__helpViewerWidget.emptyDocument())
78 else:
79 self.setSource(url)
80
81 def link(self):
82 """
83 Public method to get the URL of the shown document.
84
85 @return URL of the document
86 @rtype QUrl
87 """
88 return self.source()
89
90 def doSetSource(self, url, type_):
91 """
92 Public method to load the data and show it.
93
94 @param url URL of resource to load
95 @type QUrl
96 @param type_ type of the resource to load
97 @type QTextDocument.ResourceType
98 """
99 if not self.__canLoadResource(url):
100 QDesktopServices.openUrl(url)
101 return
102
103 super().doSetSource(url, type_)
104
105 self.sourceChanged.emit(url)
106 self.loadFinished.emit(True)
107
108 def loadResource(self, type_, name):
109 """
110 Public method to load data of the specified type from the resource with
111 the given name.
112
113 @param type_ resource type
114 @type int
115 @param name resource name
116 @type QUrl
117 @return byte array containing the loaded data
118 @rtype QByteArray
119 """
120 ba = QByteArray()
121 scheme = name.scheme()
122
123 if type_ < 4: # QTextDocument.ResourceType.MarkdownResource
124 if scheme == "about":
125 if name.toString() == "about:blank":
126 return QByteArray(AboutBlank.encode("utf-8"))
127 elif scheme in ("file", ""):
128 filePath = name.toLocalFile()
129 with contextlib.suppress(OSError), open(filePath, "rb") as f:
130 ba = QByteArray(f.read())
131 elif scheme == "qthelp":
132 url = self._engine.findFile(name)
133 if url.isValid():
134 ba = self._engine.fileData(url)
135
136 if name.toString().lower().endswith(".svg"):
137 image = QImage()
138 image.loadFromData(ba, "svg")
139 if not image.isNull():
140 return image
141
142 if ba.isEmpty():
143 ba = QByteArray(
144 PageNotFound.format(name.toString()).encode("utf-8")
145 )
146
147 return ba
148
149 def __canLoadResource(self, url):
150 """
151 Private method to check, if the given resource can be loaded.
152
153 @param url URL of resource to be loaded
154 @type QUrl
155 @return flag indicating, that the given URL can be handled
156 @rtype bool
157 """
158 scheme = url.scheme()
159 return scheme in ("about", "qthelp", "file", "")
160
161 def pageTitle(self):
162 """
163 Public method get the page title.
164
165 @return page title
166 @rtype str
167 """
168 titleStr = self.documentTitle()
169 if not titleStr:
170 url = self.link()
171
172 titleStr = url.host()
173 if not titleStr:
174 titleStr = url.toString(
175 QUrl.UrlFormattingOption.RemoveFragment)
176
177 if not titleStr or titleStr == "about:blank":
178 titleStr = self.tr("Empty Page")
179
180 return titleStr
181
182 def isEmptyPage(self):
183 """
184 Public method to check, if the current page is the empty page.
185
186 @return flag indicating an empty page is loaded
187 @rtype bool
188 """
189 return self.pageTitle() == self.tr("Empty Page")
190
191 def mousePressEvent(self, evt):
192 """
193 Protected method called by a mouse press event.
194
195 @param evt reference to the mouse event
196 @type QMouseEvent
197 """
198 if evt.button() == Qt.MouseButton.XButton1:
199 self.backward()
200 evt.accept()
201 elif evt.button() == Qt.MouseButton.XButton2:
202 self.forward()
203 evt.accept()
204 else:
205 super().mousePressEvent(evt)
206
207 def mouseReleaseEvent(self, evt):
208 """
209 Protected method called by a mouse release event.
210
211 @param evt reference to the mouse event
212 @type QMouseEvent
213 """
214 hasModifier = evt.modifiers() != Qt.KeyboardModifier.NoModifier
215 if evt.button() == Qt.MouseButton.LeftButton and hasModifier:
216
217 anchor = self.anchorAt(evt.pos())
218 if anchor:
219 url = self.link().resolved(QUrl(anchor))
220 if evt.modifiers() & Qt.KeyboardModifier.ControlModifier:
221 self.__helpViewerWidget.openUrlNewBackgroundPage(url)
222 else:
223 self.__helpViewerWidget.openUrlNewPage(url)
224 evt.accept()
225 else:
226 super().mousePressEvent(evt)
227
228 def gotoHistory(self, index):
229 """
230 Public method to step through the history.
231
232 @param index history index (<0 backward, >0 forward)
233 @type int
234 """
235 if index < 0:
236 # backward
237 for _ind in range(-index):
238 self.backward()
239 else:
240 # forward
241 for _ind in range(index):
242 self.forward()
243
244 def isBackwardAvailable(self):
245 """
246 Public method to check, if stepping backward through the history is
247 available.
248
249 @return flag indicating backward stepping is available
250 @rtype bool
251 """
252 return QTextBrowser.isBackwardAvailable(self)
253
254 def isForwardAvailable(self):
255 """
256 Public method to check, if stepping forward through the history is
257 available.
258
259 @return flag indicating forward stepping is available
260 @rtype bool
261 """
262 return QTextBrowser.isForwardAvailable(self)
263
264 def scaleUp(self):
265 """
266 Public method to zoom in.
267 """
268 if self.__zoomCount < 10:
269 self.__zoomCount += 1
270 self.zoomIn()
271 self.zoomChanged.emit()
272
273 def scaleDown(self):
274 """
275 Public method to zoom out.
276 """
277 if self.__zoomCount > -5:
278 self.__zoomCount -= 1
279 self.zoomOut()
280 self.zoomChanged.emit()
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 -5 <= scale <= 10:
290 self.zoomOut(scale)
291 self.__zoomCount = scale
292 self.zoomChanged.emit()
293
294 def resetScale(self):
295 """
296 Public method to reset the zoom level.
297 """
298 if self.__zoomCount != 0:
299 self.zoomOut(self.__zoomCount)
300 self.zoomChanged.emit()
301 self.__zoomCount = 0
302
303 def scale(self):
304 """
305 Public method to get the zoom level.
306
307 @return current zoom level
308 @rtype int
309 """
310 return self.__zoomCount
311
312 def isScaleUpAvailable(self):
313 """
314 Public method to check, if the max. zoom level is reached.
315
316 @return flag indicating scale up is available
317 @rtype bool
318 """
319 return self.__zoomCount < 10
320
321 def isScaleDownAvailable(self):
322 """
323 Public method to check, if the min. zoom level is reached.
324
325 @return flag indicating scale down is available
326 @rtype bool
327 """
328 return self.__zoomCount > -5
329
330 def wheelEvent(self, evt):
331 """
332 Protected method to handle wheel event to zoom.
333
334 @param evt reference to the event object
335 @type QWheelEvent
336 """
337 delta = evt.angleDelta().y()
338 if evt.modifiers() == Qt.KeyboardModifier.ControlModifier:
339 if delta > 0:
340 self.scaleUp()
341 else:
342 self.scaleDown()
343 evt.accept()
344
345 elif evt.modifiers() & Qt.KeyboardModifier.ShiftModifier:
346 if delta < 0:
347 self.backward()
348 elif delta > 0:
349 self.forward()
350 evt.accept()
351
352 else:
353 QTextBrowser.wheelEvent(self, evt)
354
355 def keyPressEvent(self, evt):
356 """
357 Protected method to handle key press events.
358
359 @param evt reference to the key event
360 @type QKeyEvent
361 """
362 key = evt.key()
363 isControlModifier = (
364 evt.modifiers() == Qt.KeyboardModifier.ControlModifier
365 )
366
367 if (
368 key == Qt.Key.Key_ZoomIn or
369 (key == Qt.Key.Key_Plus and isControlModifier)
370 ):
371 self.scaleUp()
372 evt.accept()
373 elif (
374 key == Qt.Key.Key_ZoomOut or
375 (key == Qt.Key.Key_Minus and isControlModifier)
376 ):
377 self.scaleDown()
378 evt.accept()
379 elif key == Qt.Key.Key_0 and isControlModifier:
380 self.resetScale()
381 evt.accept()
382 elif (
383 key == Qt.Key.Key_Backspace or
384 (key == Qt.Key.Key_Left and isControlModifier)
385 ):
386 self.backward()
387 evt.accept()
388 elif key == Qt.Key.Key_Right and isControlModifier:
389 self.forward()
390 evt.accept()
391 elif key == Qt.Key.Key_F and isControlModifier:
392 self.__helpViewerWidget.showHideSearch(True)
393 evt.accept()
394 elif (
395 key == Qt.Key.Key_F3 and
396 evt.modifiers() == Qt.KeyboardModifier.NoModifier
397 ):
398 self.__helpViewerWidget.searchNext()
399 evt.accept()
400 elif (
401 key == Qt.Key.Key_F3 and
402 evt.modifiers() == Qt.KeyboardModifier.ShiftModifier
403 ):
404 self.__helpViewerWidget.searchPrev()
405 evt.accept()
406 else:
407 super().keyPressEvent(evt)
408
409 def event(self, evt):
410 """
411 Public method handling events.
412
413 @param evt reference to the event
414 @type QEvent
415 @return flag indicating the event was handled
416 @rtype bool
417 """
418 if evt.type() == QEvent.Type.Gesture:
419 self.gestureEvent(evt)
420 return True
421
422 return super().event(evt)
423
424 def gestureEvent(self, evt):
425 """
426 Protected method handling gesture events.
427
428 @param evt reference to the gesture event
429 @type QGestureEvent
430 """
431 pinch = evt.gesture(Qt.GestureType.PinchGesture)
432 if pinch:
433 if pinch.state() == Qt.GestureState.GestureStarted:
434 zoom = (self.getZoom() + 6) / 10.0
435 pinch.setTotalScaleFactor(zoom)
436 elif pinch.state() == Qt.GestureState.GestureUpdated:
437 zoom = int(pinch.totalScaleFactor() * 10) - 6
438 if zoom <= -5:
439 zoom = -5
440 pinch.setTotalScaleFactor(0.1)
441 elif zoom >= 10:
442 zoom = 10
443 pinch.setTotalScaleFactor(1.6)
444 self.setScale(zoom)
445 evt.accept()
446
447 #######################################################################
448 ## Context menu related methods below
449 #######################################################################
450
451 @pyqtSlot(QPoint)
452 def __showContextMenu(self, pos):
453 """
454 Private slot to show the context menu.
455
456 @param pos position to show the context menu at
457 @type QPoint
458 """
459 self.__menu.clear()
460 anchor = self.anchorAt(pos)
461 linkUrl = self.link().resolved(QUrl(anchor)) if anchor else QUrl()
462 selectedText = self.textCursor().selectedText()
463
464 act = self.__menu.addAction(
465 UI.PixmapCache.getIcon("back"),
466 self.tr("Backward"),
467 self.backward)
468 act.setEnabled(self.isBackwardAvailable())
469
470 act = self.__menu.addAction(
471 UI.PixmapCache.getIcon("forward"),
472 self.tr("Forward"),
473 self.forward)
474 act.setEnabled(self.isForwardAvailable())
475
476 act = self.__menu.addAction(
477 UI.PixmapCache.getIcon("reload"),
478 self.tr("Reload"),
479 self.reload)
480
481 if not linkUrl.isEmpty() and linkUrl.scheme() != "javascript":
482 self.__createLinkContextMenu(self.__menu, linkUrl)
483
484 self.__menu.addSeparator()
485
486 act = self.__menu.addAction(
487 UI.PixmapCache.getIcon("editCopy"),
488 self.tr("Copy Page URL to Clipboard"))
489 act.setData(self.link())
490 act.triggered.connect(
491 functools.partial(self.__copyLink, act))
492
493 act = self.__menu.addAction(
494 UI.PixmapCache.getIcon("bookmark22"),
495 self.tr("Bookmark Page"))
496 act.setData({
497 "title": self.pageTitle(),
498 "url": self.link()
499 })
500 act.triggered.connect(
501 functools.partial(self.__bookmarkPage, act))
502
503 self.__menu.addSeparator()
504
505 act = self.__menu.addAction(
506 UI.PixmapCache.getIcon("zoomIn"),
507 self.tr("Zoom in"),
508 self.scaleUp)
509 act.setEnabled(self.isScaleUpAvailable())
510
511 act = self.__menu.addAction(
512 UI.PixmapCache.getIcon("zoomOut"),
513 self.tr("Zoom out"),
514 self.scaleDown)
515 act.setEnabled(self.isScaleDownAvailable())
516
517 self.__menu.addAction(
518 UI.PixmapCache.getIcon("zoomReset"),
519 self.tr("Zoom reset"),
520 self.resetScale)
521
522 self.__menu.addSeparator()
523
524 act = self.__menu.addAction(
525 UI.PixmapCache.getIcon("editCopy"),
526 self.tr("Copy"),
527 self.copy)
528 act.setEnabled(bool(selectedText))
529
530 self.__menu.addAction(
531 UI.PixmapCache.getIcon("editSelectAll"),
532 self.tr("Select All"),
533 self.selectAll)
534
535 self.__menu.addSeparator()
536
537 self.__menu.addAction(
538 UI.PixmapCache.getIcon("tabClose"),
539 self.tr('Close'),
540 self.__closePage)
541
542 act = self.__menu.addAction(
543 UI.PixmapCache.getIcon("tabCloseOther"),
544 self.tr("Close Others"),
545 self.__closeOtherPages)
546 act.setEnabled(self.__helpViewerWidget.openPagesCount() > 1)
547
548 self.__menu.popup(self.mapToGlobal(pos))
549
550 def __createLinkContextMenu(self, menu, linkUrl):
551 """
552 Private method to populate the context menu for URLs.
553
554 @param menu reference to the menu to be populated
555 @type QMenu
556 @param linkUrl URL to create the menu part for
557 @type QUrl
558 """
559 if not menu.isEmpty():
560 menu.addSeparator()
561
562 act = menu.addAction(
563 UI.PixmapCache.getIcon("openNewTab"),
564 self.tr("Open Link in New Page"))
565 act.setData(linkUrl)
566 act.triggered.connect(
567 functools.partial(self.__openLinkInNewPage, act))
568
569 act = menu.addAction(
570 UI.PixmapCache.getIcon("newWindow"),
571 self.tr("Open Link in Background Page"))
572 act.setData(linkUrl)
573 act.triggered.connect(
574 functools.partial(self.__openLinkInBackgroundPage, act))
575
576 menu.addSeparator()
577
578 act = menu.addAction(
579 UI.PixmapCache.getIcon("editCopy"),
580 self.tr("Copy URL to Clipboard"))
581 act.setData(linkUrl)
582 act.triggered.connect(
583 functools.partial(self.__copyLink, act))
584
585 def __openLinkInNewPage(self, act):
586 """
587 Private method called by the context menu to open a link in a new page.
588
589 @param act reference to the action that triggered
590 @type QAction
591 """
592 url = act.data()
593 if url.isEmpty():
594 return
595
596 self.__helpViewerWidget.openUrlNewPage(url)
597
598 def __openLinkInBackgroundPage(self, act):
599 """
600 Private method called by the context menu to open a link in a
601 background page.
602
603 @param act reference to the action that triggered
604 @type QAction
605 """
606 url = act.data()
607 if url.isEmpty():
608 return
609
610 self.__helpViewerWidget.openUrlNewBackgroundPage(url)
611
612 def __bookmarkPage(self, act):
613 """
614 Private method called by the context menu to bookmark the page.
615
616 @param act reference to the action that triggered
617 @type QAction
618 """
619 data = act.data()
620 if data:
621 with contextlib.suppress(KeyError):
622 url = data["url"]
623 title = data["title"]
624
625 self.__helpViewerWidget.bookmarkPage(title, url)
626
627 def __copyLink(self, act):
628 """
629 Private method called by the context menu to copy a link to the
630 clipboard.
631
632 @param act reference to the action that triggered
633 @type QAction
634 """
635 data = act.data()
636 if isinstance(data, QUrl) and data.isEmpty():
637 return
638
639 if isinstance(data, QUrl):
640 data = data.toString()
641
642 # copy the URL to both clipboard areas
643 QGuiApplication.clipboard().setText(data, QClipboard.Mode.Clipboard)
644 QGuiApplication.clipboard().setText(data, QClipboard.Mode.Selection)
645
646 def __closePage(self):
647 """
648 Private method called by the context menu to close the current page.
649 """
650 self.__helpViewerWidget.closeCurrentPage()
651
652 def __closeOtherPages(self):
653 """
654 Private method called by the context menu to close all other pages.
655 """
656 self.__helpViewerWidget.closeOtherPages()

eric ide

mercurial