eric7/HelpViewer/HelpViewerImplQTB.py

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

eric ide

mercurial