|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a Table of Contents viewer widget. |
|
8 """ |
|
9 |
|
10 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QModelIndex, QSortFilterProxyModel |
|
11 from PyQt6.QtPdf import QPdfBookmarkModel, QPdfDocument |
|
12 from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel, QTreeView, QLineEdit |
|
13 |
|
14 |
|
15 class PdfToCModel(QPdfBookmarkModel): |
|
16 """ |
|
17 Class implementing a TOC model with page numbers. |
|
18 """ |
|
19 |
|
20 def __init__(self, parent): |
|
21 """ |
|
22 Constructor |
|
23 |
|
24 @param parent DESCRIPTION |
|
25 @type TYPE |
|
26 """ |
|
27 super().__init__(parent) |
|
28 |
|
29 def columnCount(self, index): |
|
30 """ |
|
31 Public method to define the number of columns to be shown. |
|
32 |
|
33 @param index index of the element |
|
34 @type QModelIndex |
|
35 @return column count (always 2) |
|
36 @rtype int |
|
37 """ |
|
38 return 2 |
|
39 |
|
40 def data(self, index, role): |
|
41 """ |
|
42 Public method to return the requested data. |
|
43 |
|
44 @param index index of the element |
|
45 @type QModelIndex |
|
46 @param role data role |
|
47 @type Qt.ItemDataRole |
|
48 @return requested data |
|
49 @rtype Any |
|
50 """ |
|
51 if not index.isValid(): |
|
52 return |
|
53 |
|
54 if index.column() == 1: |
|
55 if role == Qt.ItemDataRole.DisplayRole: |
|
56 page = index.data(QPdfBookmarkModel.Role.Page.value) |
|
57 return self.document().pageLabel(page) |
|
58 elif role == Qt.ItemDataRole.TextAlignmentRole: |
|
59 return Qt.AlignmentFlag.AlignRight |
|
60 |
|
61 return super().data(index, role) |
|
62 |
|
63 |
|
64 class PdfToCWidget(QWidget): |
|
65 """ |
|
66 Class implementing a Table of Contents viewer widget. |
|
67 |
|
68 @signal topicActivated(page, zoomFactor) emitted to navigate to the selected topic |
|
69 """ |
|
70 |
|
71 topicActivated = pyqtSignal(int, float) |
|
72 |
|
73 def __init__(self, document, parent=None): |
|
74 """ |
|
75 Constructor |
|
76 |
|
77 @param document reference to the PDF document object |
|
78 @type QPdfDocument |
|
79 @param parent reference to the parent widget (defaults to None) |
|
80 @type QWidget (optional) |
|
81 """ |
|
82 super().__init__(parent) |
|
83 |
|
84 self.__layout = QVBoxLayout(self) |
|
85 |
|
86 self.__header = QLabel("<h2>{0}</h2>".format(self.tr("Contents"))) |
|
87 self.__header.setAlignment(Qt.AlignmentFlag.AlignCenter) |
|
88 self.__layout.addWidget(self.__header) |
|
89 |
|
90 self.__searchEdit = QLineEdit(self) |
|
91 self.__searchEdit.setPlaceholderText(self.tr("Search ...")) |
|
92 self.__searchEdit.setClearButtonEnabled(True) |
|
93 self.__layout.addWidget(self.__searchEdit) |
|
94 |
|
95 self.__tocWidget = QTreeView(self) |
|
96 self.__tocWidget.setHeaderHidden(True) |
|
97 self.__tocWidget.setExpandsOnDoubleClick(False) |
|
98 self.__tocModel = PdfToCModel(self) |
|
99 self.__tocModel.setDocument(document) |
|
100 self.__tocFilterModel = QSortFilterProxyModel(self) |
|
101 self.__tocFilterModel.setRecursiveFilteringEnabled(True) |
|
102 self.__tocFilterModel.setSourceModel(self.__tocModel) |
|
103 self.__tocWidget.setModel(self.__tocFilterModel) |
|
104 self.__layout.addWidget(self.__tocWidget) |
|
105 |
|
106 self.setLayout(self.__layout) |
|
107 |
|
108 self.__tocWidget.activated.connect(self.__topicSelected) |
|
109 document.statusChanged.connect(self.__handleDocumentStatus) |
|
110 self.__searchEdit.textEdited.connect(self.__searchTextChanged) |
|
111 |
|
112 @pyqtSlot(QModelIndex) |
|
113 def __topicSelected(self, index): |
|
114 """ |
|
115 Private slot to handle the selection of a ToC entry. |
|
116 |
|
117 @param index index of the activated entry |
|
118 @type QModelIndex |
|
119 """ |
|
120 if not index.isValid(): |
|
121 return |
|
122 |
|
123 page = index.data(QPdfBookmarkModel.Role.Page.value) |
|
124 zoomFactor = index.data(QPdfBookmarkModel.Role.Zoom.value) |
|
125 |
|
126 self.topicActivated.emit(page, zoomFactor) |
|
127 |
|
128 @pyqtSlot(QPdfDocument.Status) |
|
129 def __handleDocumentStatus(self, status): |
|
130 """ |
|
131 Private slot to handle a change of the document status. |
|
132 |
|
133 @param status document status |
|
134 @type QPdfDocument.Status |
|
135 """ |
|
136 if status == QPdfDocument.Status.Ready: |
|
137 self.__tocWidget.expandAll() |
|
138 for column in range(self.__tocModel.columnCount(QModelIndex())): |
|
139 self.__tocWidget.resizeColumnToContents(column) |
|
140 |
|
141 @pyqtSlot(str) |
|
142 def __searchTextChanged(self, text): |
|
143 """ |
|
144 Private slot to handle a change of the search text. |
|
145 |
|
146 @param text search text |
|
147 @type str |
|
148 """ |
|
149 self.__tocFilterModel.setFilterWildcard("*{0}*".format(text)) |
|
150 self.__tocWidget.expandAll() |