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