|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2009 - 2023 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a window for showing the QtHelp index. |
|
8 """ |
|
9 |
|
10 from PyQt6.QtCore import QEvent, Qt, QUrl, pyqtSignal, pyqtSlot |
|
11 from PyQt6.QtGui import QClipboard, QGuiApplication |
|
12 from PyQt6.QtHelp import QHelpLink |
|
13 from PyQt6.QtWidgets import ( |
|
14 QApplication, |
|
15 QDialog, |
|
16 QHBoxLayout, |
|
17 QLabel, |
|
18 QLineEdit, |
|
19 QMenu, |
|
20 QVBoxLayout, |
|
21 QWidget, |
|
22 ) |
|
23 |
|
24 |
|
25 class HelpIndexWidget(QWidget): |
|
26 """ |
|
27 Class implementing a window for showing the QtHelp index. |
|
28 |
|
29 @signal escapePressed() emitted when the ESC key was pressed |
|
30 @signal openUrl(QUrl, str) emitted to open an entry in the current tab |
|
31 @signal newTab(QUrl, str) emitted to open an entry in a new tab |
|
32 @signal newBackgroundTab(QUrl, str) emitted to open an entry in a |
|
33 new background tab |
|
34 @signal newWindow(QUrl, str) emitted to open an entry in a new window |
|
35 """ |
|
36 |
|
37 escapePressed = pyqtSignal() |
|
38 openUrl = pyqtSignal(QUrl) |
|
39 newTab = pyqtSignal(QUrl) |
|
40 newBackgroundTab = pyqtSignal(QUrl) |
|
41 newWindow = pyqtSignal(QUrl) |
|
42 |
|
43 def __init__(self, engine, internal=False, parent=None): |
|
44 """ |
|
45 Constructor |
|
46 |
|
47 @param engine reference to the help engine |
|
48 @type QHelpEngine |
|
49 @param internal flag indicating the internal help viewer |
|
50 @type bool |
|
51 @param parent reference to the parent widget |
|
52 @type QWidget |
|
53 """ |
|
54 super().__init__(parent) |
|
55 |
|
56 self.__engine = engine |
|
57 self.__internal = internal |
|
58 |
|
59 self.__searchEdit = None |
|
60 self.__index = None |
|
61 |
|
62 self.__layout = QVBoxLayout(self) |
|
63 if internal: |
|
64 # no margins for the internal variant |
|
65 self.__layout.setContentsMargins(0, 0, 0, 0) |
|
66 |
|
67 self.__searchEditLayout = QHBoxLayout() |
|
68 label = QLabel(self.tr("&Look for:")) |
|
69 self.__searchEditLayout.addWidget(label) |
|
70 |
|
71 self.__searchEdit = QLineEdit() |
|
72 self.__searchEdit.setClearButtonEnabled(True) |
|
73 label.setBuddy(self.__searchEdit) |
|
74 self.__searchEdit.textChanged.connect(self.__filterIndices) |
|
75 self.__searchEdit.installEventFilter(self) |
|
76 self.__searchEditLayout.addWidget(self.__searchEdit) |
|
77 self.__layout.addLayout(self.__searchEditLayout) |
|
78 |
|
79 self.__index = self.__engine.indexWidget() |
|
80 self.__index.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) |
|
81 |
|
82 self.__engine.indexModel().indexCreationStarted.connect( |
|
83 self.__disableSearchEdit |
|
84 ) |
|
85 self.__engine.indexModel().indexCreated.connect(self.__enableSearchEdit) |
|
86 self.__index.documentActivated.connect(self.__documentActivated) |
|
87 self.__index.documentsActivated.connect(self.__documentsActivated) |
|
88 self.__index.customContextMenuRequested.connect(self.__showContextMenu) |
|
89 self.__searchEdit.returnPressed.connect(self.__index.activateCurrentItem) |
|
90 self.__layout.addWidget(self.__index) |
|
91 |
|
92 @pyqtSlot(QHelpLink, str) |
|
93 def __documentActivated(self, document, keyword, modifiers=None): |
|
94 """ |
|
95 Private slot to handle the activation of a keyword entry. |
|
96 |
|
97 @param document reference to a data structure containing the |
|
98 document info |
|
99 @type QHelpLink |
|
100 @param keyword keyword for the URL |
|
101 @type str |
|
102 @param modifiers keyboard modifiers |
|
103 @type Qt.KeyboardModifiers or None |
|
104 """ |
|
105 if modifiers is None: |
|
106 modifiers = QApplication.keyboardModifiers() |
|
107 if not document.url.isEmpty() and document.url.isValid(): |
|
108 if modifiers & ( |
|
109 Qt.KeyboardModifier.ControlModifier |
|
110 | Qt.KeyboardModifier.ControlModifier |
|
111 ): |
|
112 self.newBackgroundTab.emit(document.url) |
|
113 elif modifiers & Qt.KeyboardModifier.ControlModifier: |
|
114 self.newTab.emit(document.url) |
|
115 elif modifiers & Qt.KeyboardModifier.ShiftModifier and not self.__internal: |
|
116 self.newWindow.emit(document.url) |
|
117 else: |
|
118 self.openUrl.emit(document.url) |
|
119 |
|
120 def __documentsActivated(self, documents, helpKeyword): |
|
121 """ |
|
122 Private slot to handle the activation of an entry with multiple help |
|
123 documents. |
|
124 |
|
125 @param documents list of help document link data structures |
|
126 @type list of QHelpLink |
|
127 @param helpKeyword keyword for the entry |
|
128 @type str |
|
129 """ |
|
130 modifiers = QApplication.keyboardModifiers() |
|
131 document = ( |
|
132 documents[0] |
|
133 if len(documents) == 1 |
|
134 else self.__selectDocument(documents, helpKeyword) |
|
135 ) |
|
136 self.__documentActivated(document, helpKeyword, modifiers) |
|
137 |
|
138 def __selectDocument(self, documents, helpKeyword): |
|
139 """ |
|
140 Private method to give the user a chance to select among the |
|
141 given documents. |
|
142 |
|
143 @param documents list of help document link data structures |
|
144 @type list of QHelpLink |
|
145 @param helpKeyword keyword for the documents |
|
146 @type str |
|
147 @return selected document |
|
148 @rtype QHelpLink |
|
149 """ |
|
150 from .HelpTopicDialog import HelpTopicDialog |
|
151 |
|
152 document = QHelpLink() |
|
153 |
|
154 dlg = HelpTopicDialog(self, helpKeyword, documents) |
|
155 if dlg.exec() == QDialog.DialogCode.Accepted: |
|
156 document = dlg.document() |
|
157 |
|
158 return document |
|
159 |
|
160 def __filterIndices(self, indexFilter): |
|
161 """ |
|
162 Private slot to filter the indexes according to the given filter. |
|
163 |
|
164 @param indexFilter filter to be used |
|
165 @type str |
|
166 """ |
|
167 if "*" in indexFilter: |
|
168 self.__index.filterIndices(indexFilter, indexFilter) |
|
169 else: |
|
170 self.__index.filterIndices(indexFilter) |
|
171 |
|
172 def __enableSearchEdit(self): |
|
173 """ |
|
174 Private slot to enable the search edit. |
|
175 """ |
|
176 self.__searchEdit.setEnabled(True) |
|
177 self.__filterIndices(self.__searchEdit.text()) |
|
178 |
|
179 def __disableSearchEdit(self): |
|
180 """ |
|
181 Private slot to enable the search edit. |
|
182 """ |
|
183 self.__searchEdit.setEnabled(False) |
|
184 |
|
185 def focusInEvent(self, evt): |
|
186 """ |
|
187 Protected method handling focus in events. |
|
188 |
|
189 @param evt reference to the focus event object |
|
190 @type QFocusEvent |
|
191 """ |
|
192 if evt.reason() != Qt.FocusReason.MouseFocusReason: |
|
193 self.__searchEdit.selectAll() |
|
194 self.__searchEdit.setFocus() |
|
195 |
|
196 def eventFilter(self, watched, event): |
|
197 """ |
|
198 Public method called to filter the event queue. |
|
199 |
|
200 @param watched the QObject being watched |
|
201 @type QObject |
|
202 @param event the event that occurred |
|
203 @type QEvent |
|
204 @return flag indicating whether the event was handled |
|
205 @rtype bool |
|
206 """ |
|
207 if ( |
|
208 self.__searchEdit |
|
209 and watched == self.__searchEdit |
|
210 and event.type() == QEvent.Type.KeyPress |
|
211 ): |
|
212 idx = self.__index.currentIndex() |
|
213 if event.key() == Qt.Key.Key_Up: |
|
214 idx = self.__index.model().index( |
|
215 idx.row() - 1, idx.column(), idx.parent() |
|
216 ) |
|
217 if idx.isValid(): |
|
218 self.__index.setCurrentIndex(idx) |
|
219 elif event.key() == Qt.Key.Key_Down: |
|
220 idx = self.__index.model().index( |
|
221 idx.row() + 1, idx.column(), idx.parent() |
|
222 ) |
|
223 if idx.isValid(): |
|
224 self.__index.setCurrentIndex(idx) |
|
225 elif event.key() == Qt.Key.Key_Escape: |
|
226 self.escapePressed.emit() |
|
227 |
|
228 return QWidget.eventFilter(self, watched, event) |
|
229 |
|
230 def __showContextMenu(self, pos): |
|
231 """ |
|
232 Private slot showing the context menu. |
|
233 |
|
234 @param pos position to show the menu at |
|
235 @type QPoint |
|
236 """ |
|
237 idx = self.__index.indexAt(pos) |
|
238 if idx.isValid(): |
|
239 menu = QMenu() |
|
240 curTab = menu.addAction(self.tr("Open Link")) |
|
241 if self.__internal: |
|
242 newTab = menu.addAction(self.tr("Open Link in New Page")) |
|
243 newBackgroundTab = menu.addAction( |
|
244 self.tr("Open Link in Background Page") |
|
245 ) |
|
246 else: |
|
247 newTab = menu.addAction(self.tr("Open Link in New Tab")) |
|
248 newBackgroundTab = menu.addAction( |
|
249 self.tr("Open Link in Background Tab") |
|
250 ) |
|
251 newWindow = menu.addAction(self.tr("Open Link in New Window")) |
|
252 menu.addSeparator() |
|
253 copyLink = menu.addAction(self.tr("Copy URL to Clipboard")) |
|
254 menu.move(self.__index.mapToGlobal(pos)) |
|
255 |
|
256 act = menu.exec() |
|
257 model = self.__index.model() |
|
258 if model is not None: |
|
259 helpKeyword = model.data(idx, Qt.ItemDataRole.DisplayRole) |
|
260 helpLinks = self.__engine.documentsForKeyword(helpKeyword, "") |
|
261 if len(helpLinks) == 1: |
|
262 link = helpLinks[0].url |
|
263 else: |
|
264 link = self.__selectDocument(helpLinks, helpKeyword).url |
|
265 |
|
266 if not link.isEmpty() and link.isValid(): |
|
267 if act == curTab: |
|
268 self.openUrl.emit(link) |
|
269 elif act == newTab: |
|
270 self.newTab.emit(link) |
|
271 elif act == newBackgroundTab: |
|
272 self.newBackgroundTab.emit(link) |
|
273 elif not self.__internal and act == newWindow: |
|
274 self.newWindow.emit(link) |
|
275 elif act == copyLink: |
|
276 # copy the URL to both clipboard areas |
|
277 QGuiApplication.clipboard().setText( |
|
278 link.toString(), QClipboard.Mode.Clipboard |
|
279 ) |
|
280 QGuiApplication.clipboard().setText( |
|
281 link.toString(), QClipboard.Mode.Selection |
|
282 ) |