src/eric7/PdfViewer/PdfView.py

branch
pdf_viewer
changeset 9707
717f95e35ca8
parent 9704
6e1650b9b3b5
child 9708
8956a005f478
equal deleted inserted replaced
9706:c0ff0b4d5657 9707:717f95e35ca8
5 5
6 """ 6 """
7 Module implementing a specialized PDF view class. 7 Module implementing a specialized PDF view class.
8 """ 8 """
9 9
10 from PyQt6.QtCore import QSize, Qt, pyqtSlot, QEvent 10 import collections
11 from PyQt6.QtGui import QGuiApplication 11 import enum
12
13 from dataclasses import dataclass
14
15 from PyQt6.QtCore import (
16 QSize, Qt, pyqtSlot, QEvent, QSizeF, QRect, QPoint, QPointF, QRectF,
17 pyqtSignal
18 )
19 from PyQt6.QtGui import QGuiApplication, QPainter, QColor, QPen
20 from PyQt6.QtPdf import QPdfDocument, QPdfLink
12 from PyQt6.QtPdfWidgets import QPdfView 21 from PyQt6.QtPdfWidgets import QPdfView
22 from PyQt6.QtWidgets import QRubberBand
13 23
14 from .PdfZoomSelector import PdfZoomSelector 24 from .PdfZoomSelector import PdfZoomSelector
15 25
16 26
27 class PdfMarkerType(enum.Enum):
28 """
29 Class defining the various marker types.
30 """
31
32 SearchResult = 0
33 Selection = 1
34
35
36 @dataclass
37 class PdfMarker:
38 """
39 Class defining the data structure for markers.
40 """
41
42 rectangle: QRectF
43 markerType: PdfMarkerType
44
45
46 @dataclass
47 class PdfMarkerGeometry:
48 """
49 Class defining the data structure for marker geometries.
50 """
51
52 rectangle: QRect
53 markerType: PdfMarkerType
54
55
17 class PdfView(QPdfView): 56 class PdfView(QPdfView):
18 """ 57 """
19 Class implementing a specialized PDF view. 58 Class implementing a specialized PDF view.
20 """ 59 """
60
61 MarkerColors = {
62 # merker type: (pen color, brush color)
63 PdfMarkerType.SearchResult: (QColor(255, 200, 0, 255), QColor(255, 200, 0, 64)),
64 PdfMarkerType.Selection: (QColor(0, 0, 255, 255), QColor(0, 0, 255, 64)),
65 }
66
67 selectionAvailable = pyqtSignal(bool)
21 68
22 def __init__(self, parent): 69 def __init__(self, parent):
23 """ 70 """
24 Constructor 71 Constructor
25 72
30 77
31 self.__screenResolution = ( 78 self.__screenResolution = (
32 QGuiApplication.primaryScreen().logicalDotsPerInch() / 72.0 79 QGuiApplication.primaryScreen().logicalDotsPerInch() / 72.0
33 ) 80 )
34 81
82 self.__documentViewport = QRect()
83 self.__documentSize = QSize()
84 self.__pageGeometries = {}
85 self.__markers = collections.defaultdict(list)
86 self.__markerGeometries = collections.defaultdict(list)
87 self.__rubberBand = None
88
89 self.pageModeChanged.connect(self.__calculateDocumentLayout)
90 self.zoomModeChanged.connect(self.__calculateDocumentLayout)
91 self.zoomFactorChanged.connect(self.__calculateDocumentLayout)
92 self.pageSpacingChanged.connect(self.__calculateDocumentLayout)
93 self.documentMarginsChanged.connect(self.__calculateDocumentLayout)
94
95 self.pageNavigator().currentPageChanged.connect(self.__currentPageChanged)
96
35 self.grabGesture(Qt.GestureType.PinchGesture) 97 self.grabGesture(Qt.GestureType.PinchGesture)
98
99 def setDocument(self, document):
100 """
101 Public method to set the PDF document.
102
103 @param document reference to the PDF document object
104 @type QPdfDocument
105 """
106 super().setDocument(document)
107
108 document.statusChanged.connect(self.__calculateDocumentLayout)
36 109
37 def __zoomInOut(self, zoomIn): 110 def __zoomInOut(self, zoomIn):
38 """ 111 """
39 Private method to zoom into or out of the view. 112 Private method to zoom into or out of the view.
40 113
76 @param zoomMode zoom mode to get the zoom factor for 149 @param zoomMode zoom mode to get the zoom factor for
77 @type QPdfView.ZoomMode 150 @type QPdfView.ZoomMode
78 @return zoom factor 151 @return zoom factor
79 @rtype float 152 @rtype float
80 """ 153 """
154 self.__calculateDocumentViewport()
155
81 if zoomMode == QPdfView.ZoomMode.Custom: 156 if zoomMode == QPdfView.ZoomMode.Custom:
82 return self.zoomFactor() 157 return self.zoomFactor()
83 else: 158 else:
84 viewport = self.viewport()
85 curPage = self.pageNavigator().currentPage() 159 curPage = self.pageNavigator().currentPage()
86 margins = self.documentMargins() 160 margins = self.documentMargins()
87 if zoomMode == QPdfView.ZoomMode.FitToWidth: 161 if zoomMode == QPdfView.ZoomMode.FitToWidth:
88 pageSize = ( 162 pageSize = (
89 self.document().pagePointSize(curPage) * self.__screenResolution 163 self.document().pagePointSize(curPage) * self.__screenResolution
90 ).toSize() 164 ).toSize()
91 factor = ( 165 factor = (
92 viewport.width() - margins.left() - margins.right() 166 self.__documentViewport.width() - margins.left() - margins.right()
93 ) / pageSize.width() 167 ) / pageSize.width()
94 pageSize *= factor 168 pageSize *= factor
95 else: 169 else:
96 # QPdfView.ZoomMode.FitInView 170 # QPdfView.ZoomMode.FitInView
97 viewportSize = viewport.size() + QSize( 171 viewportSize = self.__documentViewport.size() + QSize(
98 -margins.left() - margins.right(), -self.pageSpacing() 172 -margins.left() - margins.right(), -self.pageSpacing()
99 ) 173 )
100 pageSize = ( 174 pageSize = (
101 self.document().pagePointSize(curPage) * self.__screenResolution 175 self.document().pagePointSize(curPage) * self.__screenResolution
102 ).toSize() 176 ).toSize()
157 evt.accept() 231 evt.accept()
158 return 232 return
159 233
160 super().wheelEvent(evt) 234 super().wheelEvent(evt)
161 235
236 def keyPressEvent(self, evt):
237 """
238 Protected method handling key press events.
239
240 @param evt reference to the key event
241 @type QKeyEvent
242 """
243 if evt.key() == Qt.Key.Key_Escape:
244 self.clearSelection()
245
246 def mousePressEvent(self, evt):
247 """
248 Protected method to handle mouse press events.
249
250 @param evt reference to the mouse event
251 @type QMouseEvent
252 """
253 if evt.button() == Qt.MouseButton.LeftButton:
254 self.clearMarkers(PdfMarkerType.Selection)
255 self.selectionAvailable.emit(False)
256
257 self.__rubberBandOrigin = evt.pos()
258 if self.__rubberBand is None:
259 self.__rubberBand = QRubberBand(
260 QRubberBand.Shape.Rectangle, self.viewport()
261 )
262 self.__rubberBand.setGeometry(QRect(self.__rubberBandOrigin, QSize()))
263 self.__rubberBand.show()
264
265 super().mousePressEvent(evt)
266
267 def mouseMoveEvent(self, evt):
268 """
269 Protected method to handle mouse move events.
270
271 @param evt reference to the mouse event
272 @type QMouseEvent
273 """
274 if evt.buttons() & Qt.MouseButton.LeftButton:
275 self.__rubberBand.setGeometry(
276 QRect(self.__rubberBandOrigin, evt.pos()).normalized()
277 )
278
279 super().mousePressEvent(evt)
280
281 def mouseReleaseEvent(self, evt):
282 """
283 Protected method to handle mouse release events.
284
285 @param evt reference to the mouse event
286 @type QMouseEvent
287 """
288 if evt.button() == Qt.MouseButton.LeftButton:
289 self.__rubberBand.hide()
290 translatedRubber = self.__rubberBand.geometry().translated(
291 self.__documentViewport.topLeft()
292 )
293 for page in self.__pageGeometries:
294 if self.__pageGeometries[page].intersects(translatedRubber):
295 translatedRubber = translatedRubber.translated(
296 -self.__pageGeometries[page].topLeft()
297 )
298 factor = self.__zoomFactorForMode(self.zoomMode())
299 selectionSize = (
300 QSizeF(translatedRubber.size()) / factor
301 / self.__screenResolution
302 )
303 selectionTopLeft = (
304 QPointF(translatedRubber.topLeft()) / factor
305 / self.__screenResolution
306 )
307 selectionRect = QRectF(selectionTopLeft, selectionSize)
308 selection = self.document().getSelection(
309 page, selectionRect.topLeft(), selectionRect.bottomRight()
310 )
311 if selection.isValid():
312 for bound in selection.bounds():
313 self.addMarker(
314 page, bound.boundingRect(), PdfMarkerType.Selection
315 )
316 self.selectionAvailable.emit(True)
317
318 super().mousePressEvent(evt)
319
162 def event(self, evt): 320 def event(self, evt):
163 """ 321 """
164 Public method handling events. 322 Public method handling events.
165 323
166 @param evt reference to the event 324 @param evt reference to the event
191 self.zoomModeChanged.emit(QPdfView.ZoomMode.Custom) 349 self.zoomModeChanged.emit(QPdfView.ZoomMode.Custom)
192 zoomFactor = pinch.totalScaleFactor() 350 zoomFactor = pinch.totalScaleFactor()
193 self.setZoomFactor(zoomFactor) 351 self.setZoomFactor(zoomFactor)
194 self.zoomFactorChanged.emit(zoomFactor) 352 self.zoomFactorChanged.emit(zoomFactor)
195 evt.accept() 353 evt.accept()
354
355 def resizeEvent(self, evt):
356 """
357 Protected method to handle a widget resize.
358
359 @param evt reference to the resize event
360 @type QResizeEvent
361 """
362 super().resizeEvent(evt)
363
364 self.__calculateDocumentViewport()
365
366 def paintEvent(self, evt):
367 """
368 Protected method to paint the view.
369
370 This event handler calls the original paint event handler of the super class
371 and paints the markers on top of the result.
372
373 @param evt reference to the paint event
374 @type QPaintEvent
375 """
376 super().paintEvent(evt)
377
378 painter = QPainter(self.viewport())
379 painter.translate(-self.__documentViewport.x(), -self.__documentViewport.y())
380 for page in self.__markerGeometries:
381 for markerGeom in self.__markerGeometries[page]:
382 if markerGeom.rectangle.intersects(self.__documentViewport):
383 painter.setPen(QPen(
384 PdfView.MarkerColors[markerGeom.markerType][0], 2
385 ))
386 painter.setBrush(PdfView.MarkerColors[markerGeom.markerType][1])
387 painter.drawRect(markerGeom.rectangle)
388 painter.end()
389
390 def __calculateDocumentViewport(self):
391 """
392 Private method to calculate the document viewport.
393
394 This is a PyQt implementation of the code found in the QPdfView class
395 because it is calculated in a private part and not accessible.
396 """
397 x = self.horizontalScrollBar().value()
398 y = self.verticalScrollBar().value()
399 width = self.viewport().width()
400 height = self.viewport().height()
401
402 docViewport = QRect(x, y, width, height)
403 if self.__documentViewport == docViewport:
404 return
405
406 oldSize = self.__documentViewport.size()
407
408 self.__documentViewport = docViewport
409
410 if oldSize != self.__documentViewport.size():
411 self.__calculateDocumentLayout()
412
413 @pyqtSlot()
414 def __calculateDocumentLayout(self):
415 """
416 Private slot to calculate the document layout data.
417
418 This is a PyQt implementation of the code found in the QPdfView class
419 because it is calculated in a private part and not accessible.
420 """
421 self.__documentSize = QSize()
422 self.__pageGeometries.clear()
423 self.__markerGeometries.clear()
424
425 document = self.document()
426 margins = self.documentMargins()
427
428 if document is None or document.status() != QPdfDocument.Status.Ready:
429 return
430
431 pageCount = document.pageCount()
432
433 totalWidth = 0
434
435 startPage = (
436 self.pageNavigator().currentPage()
437 if self.pageMode == QPdfView.PageMode.SinglePage
438 else 0
439 )
440 endPage = (
441 self.pageNavigator().currentPage() + 1
442 if self.pageMode == QPdfView.PageMode.SinglePage
443 else pageCount
444 )
445
446 # calculate pageSizes
447 for page in range(startPage, endPage):
448 if self.zoomMode() == QPdfView.ZoomMode.Custom:
449 pageSize = QSizeF(
450 document.pagePointSize(page) * self.__screenResolution
451 * self.zoomFactor()
452 ).toSize()
453 elif self.zoomMode() == QPdfView.ZoomMode.FitToWidth:
454 pageSize = QSizeF(
455 document.pagePointSize(page) * self.__screenResolution
456 ).toSize()
457 factor = (
458 self.__documentViewport.width() - margins.left() - margins.right()
459 ) / pageSize.width()
460 pageSize *= factor
461 elif self.zoomMode() == QPdfView.ZoomMode.FitInView:
462 viewportSize = (
463 self.__documentViewport.size()
464 + QSize(-margins.left() - margins.right(), -self.pageSpacing())
465 )
466 pageSize = QSizeF(
467 document.pagePointSize(page) * self.__screenResolution
468 ).toSize()
469 pageSize = pageSize.scaled(
470 viewportSize, Qt.AspectRatioMode.KeepAspectRatio
471 )
472
473 totalWidth = max(totalWidth, pageSize.width())
474
475 self.__pageGeometries[page] = QRect(QPoint(0, 0), pageSize)
476
477 totalWidth += margins.left() + margins.right()
478
479 pageY = margins.top()
480
481 # calculate page positions
482 for page in range(startPage, endPage):
483 pageSize = self.__pageGeometries[page].size()
484
485 # center horizontally inside the viewport
486 pageX = (
487 max(totalWidth, self.__documentViewport.width()) - pageSize.width()
488 ) // 2
489 self.__pageGeometries[page].moveTopLeft(QPoint(pageX, pageY))
490
491 self.__calculateMarkerGeometries(page, QPoint(pageX, pageY))
492
493 pageY += pageSize.height() + self.pageSpacing()
494
495 pageY += margins.bottom()
496
497 self.__documentSize = QSize(totalWidth, pageY)
498
499 @pyqtSlot()
500 def __currentPageChanged(self):
501 """
502 Private slot to handle a change of the current page.
503 """
504 if self.pageMode() == QPdfView.PageMode.SinglePage:
505 self.__calculateDocumentLayout()
506
507 def __calculateMarkerGeometries(self, page, offset):
508 """
509 Private method to calculate the marker geometries.
510
511 @param page page number
512 @type int
513 @param offset page offset
514 @type QPoint or QPointF
515 """
516 # calculate search marker sizes
517 if page in self.__markers:
518 factor = self.__zoomFactorForMode(self.zoomMode())
519 for marker in self.__markers[page]:
520 markerSize = (
521 QSizeF(marker.rectangle.size()) * factor * self.__screenResolution
522 ).toSize()
523 markerTopLeft = (
524 QPointF(marker.rectangle.topLeft()) * factor
525 * self.__screenResolution
526 ).toPoint()
527
528 markerGeometry = QRect(markerTopLeft, markerSize)
529 self.__markerGeometries[page].append(
530 PdfMarkerGeometry(
531 rectangle=markerGeometry.translated(offset),
532 markerType=marker.markerType
533 )
534 )
535
536 def scrollContentsBy(self, dx, dy):
537 """
538 Public method called when the scrollbars are moved.
539
540 @param dx change of the horizontal scroll bar
541 @type int
542 @param dy change of the vertical scroll bar
543 @type int
544 """
545 super().scrollContentsBy(dx, dy)
546
547 self.__calculateDocumentViewport()
548
549 def __updateView(self):
550 """
551 Private method to update the view.
552 """
553 self.__calculateDocumentLayout()
554 self.update()
555
556 @pyqtSlot(int, QRectF, PdfMarkerType)
557 @pyqtSlot(int, QRect, PdfMarkerType)
558 def addMarker(self, page, rect, markerType):
559 """
560 Public slot to add a marker.
561
562 @param page page number for the marker
563 @type int
564 @param rect marker rectangle
565 @type QRect or QRectF
566 @param markerType type of the marker
567 @type PdfMarkerType
568 """
569 marker = PdfMarker(rectangle=QRectF(rect), markerType=markerType)
570 if marker not in self.__markers[page]:
571 self.__markers[page].append(marker)
572 self.__updateView()
573
574 @pyqtSlot(PdfMarkerType)
575 def clearMarkers(self, markerType):
576 """
577 Public slot to clear the markers of a specific type.
578
579 @param markerType type of the marker
580 @type PdfMarkerType
581 """
582 markers = collections.defaultdict(list)
583 for page in self.__markers:
584 markersList = [
585 m for m in self.__markers[page] if m.markerType != markerType
586 ]
587 if markersList:
588 markers[page] = markersList
589
590 self.__markers = markers
591 self.__updateView()
592
593 @pyqtSlot()
594 def clearAllMarkers(self):
595 """
596 Public slot to clear all markers.
597 """
598 self.__markers.clear()
599 self.__updateView()
600
601 @pyqtSlot(QPdfLink)
602 def addSearchMarker(self, link):
603 """
604 Public slot to add a search marker given a PDF link.
605
606 @param link reference to the PDF link object
607 @type QPdfLink
608 """
609 for rect in link.rectangles():
610 self.addMarker(link.page(), rect, PdfMarkerType.SearchResult)
611
612 @pyqtSlot()
613 def clearSearchMarkers(self):
614 """
615 Public slot to clear the search markers.
616 """
617 self.clearMarkers(PdfMarkerType.SearchResult)
618
619 def hasSelection(self):
620 """
621 Public method to check the presence of a selection.
622
623 @return flag indicating the presence of a selection
624 @rtype bool
625 """
626 return any(
627 m.markerType == PdfMarkerType.Selection
628 for p in self.__markers
629 for m in self.__markers[p]
630 )
631
632 def getSelection(self):
633 """
634 Public method to get a PDF selection object.
635
636 @return reference to the PDF selection object
637 @rtype QPdfSelection
638 """
639 for page in self.__markers:
640 markersList = [
641 m for m in self.__markers[page]
642 if m.markerType == PdfMarkerType.Selection
643 ]
644 if markersList:
645 selection = self.document().getSelection(
646 page,
647 markersList[0].rectangle.topLeft(),
648 markersList[-1].rectangle.bottomRight(),
649 )
650 if selection.isValid():
651 return selection
652
653 return None
654
655 @pyqtSlot()
656 def clearSelection(self):
657 """
658 Public slot to clear the current selection.
659 """
660 self.clearMarkers(PdfMarkerType.Selection)

eric ide

mercurial