|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2017 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a widget to show some source code information provided by |
|
8 plug-ins. |
|
9 """ |
|
10 |
|
11 from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QTimer |
|
12 from PyQt5.QtGui import QCursor |
|
13 from PyQt5.QtWidgets import ( |
|
14 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QSizePolicy, |
|
15 QLineEdit, QTextBrowser, QToolTip |
|
16 ) |
|
17 |
|
18 from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget, E5TextEditType |
|
19 from E5Gui.E5Application import e5App |
|
20 |
|
21 import Preferences |
|
22 |
|
23 from .CodeDocumentationViewerTemplate import ( |
|
24 prepareDocumentationViewerHtmlDocument, |
|
25 prepareDocumentationViewerHtmlDocWarningDocument, |
|
26 prepareDocumentationViewerHtmlWarningDocument |
|
27 ) |
|
28 |
|
29 |
|
30 class DocumentationViewerWidget(QWidget): |
|
31 """ |
|
32 Class implementing a rich text documentation viewer. |
|
33 """ |
|
34 EmpytDocument_Light = ( |
|
35 '''<!DOCTYPE html>\n''' |
|
36 '''<html lang="EN">\n''' |
|
37 '''<head>\n''' |
|
38 '''<style type="text/css">\n''' |
|
39 '''html {background-color: #ffffff;}\n''' |
|
40 '''body {background-color: #ffffff;\n''' |
|
41 ''' color: #000000;\n''' |
|
42 ''' margin: 0px 10px 10px 10px;\n''' |
|
43 '''}\n''' |
|
44 '''</style''' |
|
45 '''</head>\n''' |
|
46 '''<body>\n''' |
|
47 '''</body>\n''' |
|
48 '''</html>''' |
|
49 ) |
|
50 EmpytDocument_Dark = ( |
|
51 '''<!DOCTYPE html>\n''' |
|
52 '''<html lang="EN">\n''' |
|
53 '''<head>\n''' |
|
54 '''<style type="text/css">\n''' |
|
55 '''html {background-color: #262626;}\n''' |
|
56 '''body {background-color: #262626;\n''' |
|
57 ''' color: #ffffff;\n''' |
|
58 ''' margin: 0px 10px 10px 10px;\n''' |
|
59 '''}\n''' |
|
60 '''</style''' |
|
61 '''</head>\n''' |
|
62 '''<body>\n''' |
|
63 '''</body>\n''' |
|
64 '''</html>''' |
|
65 ) |
|
66 |
|
67 def __init__(self, parent=None): |
|
68 """ |
|
69 Constructor |
|
70 |
|
71 @param parent reference to the parent widget |
|
72 @type QWidget |
|
73 """ |
|
74 super().__init__(parent) |
|
75 self.setObjectName("DocumentationViewerWidget") |
|
76 |
|
77 self.__verticalLayout = QVBoxLayout(self) |
|
78 self.__verticalLayout.setObjectName("verticalLayout") |
|
79 self.__verticalLayout.setContentsMargins(0, 0, 0, 0) |
|
80 |
|
81 try: |
|
82 from PyQt5.QtWebEngineWidgets import ( |
|
83 QWebEngineView, QWebEngineSettings |
|
84 ) |
|
85 self.__contents = QWebEngineView(self) |
|
86 self.__contents.page().linkHovered.connect(self.__showLink) |
|
87 self.__contents.settings().setAttribute( |
|
88 QWebEngineSettings.WebAttribute.FocusOnNavigationEnabled, |
|
89 False) |
|
90 self.__viewerType = E5TextEditType.QWEBENGINEVIEW |
|
91 except ImportError: |
|
92 self.__contents = QTextBrowser(self) |
|
93 self.__contents.setOpenExternalLinks(True) |
|
94 self.__viewerType = E5TextEditType.QTEXTBROWSER |
|
95 |
|
96 sizePolicy = QSizePolicy(QSizePolicy.Policy.Preferred, |
|
97 QSizePolicy.Policy.Expanding) |
|
98 sizePolicy.setHorizontalStretch(0) |
|
99 sizePolicy.setVerticalStretch(0) |
|
100 sizePolicy.setHeightForWidth( |
|
101 self.__contents.sizePolicy().hasHeightForWidth()) |
|
102 self.__contents.setSizePolicy(sizePolicy) |
|
103 self.__contents.setContextMenuPolicy( |
|
104 Qt.ContextMenuPolicy.NoContextMenu) |
|
105 if self.__viewerType != E5TextEditType.QTEXTBROWSER: |
|
106 self.__contents.setUrl(QUrl("about:blank")) |
|
107 self.__verticalLayout.addWidget(self.__contents) |
|
108 |
|
109 self.__searchWidget = E5TextEditSearchWidget(self, False) |
|
110 self.__searchWidget.setFocusPolicy(Qt.FocusPolicy.WheelFocus) |
|
111 self.__searchWidget.setObjectName("searchWidget") |
|
112 self.__verticalLayout.addWidget(self.__searchWidget) |
|
113 |
|
114 self.__searchWidget.attachTextEdit( |
|
115 self.__contents, editType=self.__viewerType) |
|
116 |
|
117 @pyqtSlot(str) |
|
118 def __showLink(self, urlStr): |
|
119 """ |
|
120 Private slot to show the hovered link in a tooltip. |
|
121 |
|
122 @param urlStr hovered URL |
|
123 @type str |
|
124 """ |
|
125 QToolTip.showText(QCursor.pos(), urlStr, self.__contents) |
|
126 |
|
127 def setHtml(self, html): |
|
128 """ |
|
129 Public method to set the HTML text of the widget. |
|
130 |
|
131 @param html HTML text to be shown |
|
132 @type str |
|
133 """ |
|
134 self.__contents.setEnabled(False) |
|
135 self.__contents.setHtml(html) |
|
136 self.__contents.setEnabled(True) |
|
137 |
|
138 def clear(self): |
|
139 """ |
|
140 Public method to clear the shown contents. |
|
141 """ |
|
142 if self.__viewerType == E5TextEditType.QTEXTBROWSER: |
|
143 self.__contents.clear() |
|
144 else: |
|
145 if e5App().usesDarkPalette(): |
|
146 self.__contents.setHtml(self.EmpytDocument_Dark) |
|
147 else: |
|
148 self.__contents.setHtml(self.EmpytDocument_Light) |
|
149 |
|
150 |
|
151 class CodeDocumentationViewer(QWidget): |
|
152 """ |
|
153 Class implementing a widget to show some source code information provided |
|
154 by plug-ins. |
|
155 |
|
156 @signal providerAdded() emitted to indicate the availability of a new |
|
157 provider |
|
158 @signal providerRemoved() emitted to indicate the removal of a provider |
|
159 """ |
|
160 providerAdded = pyqtSignal() |
|
161 providerRemoved = pyqtSignal() |
|
162 |
|
163 def __init__(self, parent=None): |
|
164 """ |
|
165 Constructor |
|
166 |
|
167 @param parent reference to the parent widget |
|
168 @type QWidget |
|
169 """ |
|
170 super().__init__(parent) |
|
171 self.__setupUi() |
|
172 |
|
173 self.__ui = parent |
|
174 |
|
175 self.__providers = {} |
|
176 self.__selectedProvider = "" |
|
177 self.__disabledProvider = "disabled" |
|
178 |
|
179 self.__shuttingDown = False |
|
180 self.__startingUp = True |
|
181 |
|
182 self.__lastDocumentation = None |
|
183 self.__requestingEditor = None |
|
184 |
|
185 self.__unregisterTimer = QTimer(self) |
|
186 self.__unregisterTimer.setInterval(30000) # 30 seconds |
|
187 self.__unregisterTimer.setSingleShot(True) |
|
188 self.__unregisterTimer.timeout.connect(self.__unregisterTimerTimeout) |
|
189 self.__mostRecentlyUnregisteredProvider = None |
|
190 |
|
191 def __setupUi(self): |
|
192 """ |
|
193 Private method to generate the UI layout. |
|
194 """ |
|
195 self.setObjectName("CodeDocumentationViewer") |
|
196 |
|
197 self.verticalLayout = QVBoxLayout(self) |
|
198 self.verticalLayout.setObjectName("verticalLayout") |
|
199 self.verticalLayout.setContentsMargins(3, 3, 3, 3) |
|
200 |
|
201 # top row 1 of widgets |
|
202 self.horizontalLayout1 = QHBoxLayout() |
|
203 self.horizontalLayout1.setObjectName("horizontalLayout1") |
|
204 |
|
205 self.label = QLabel(self) |
|
206 self.label.setObjectName("label") |
|
207 self.label.setText(self.tr("Code Info Provider:")) |
|
208 self.label.setAlignment(Qt.AlignmentFlag.AlignRight | |
|
209 Qt.AlignmentFlag.AlignVCenter) |
|
210 self.horizontalLayout1.addWidget(self.label) |
|
211 |
|
212 self.providerComboBox = QComboBox(self) |
|
213 sizePolicy = QSizePolicy( |
|
214 QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Fixed) |
|
215 sizePolicy.setHorizontalStretch(0) |
|
216 sizePolicy.setVerticalStretch(0) |
|
217 sizePolicy.setHeightForWidth( |
|
218 self.providerComboBox.sizePolicy().hasHeightForWidth()) |
|
219 self.providerComboBox.setSizePolicy(sizePolicy) |
|
220 self.providerComboBox.setSizeAdjustPolicy( |
|
221 QComboBox.SizeAdjustPolicy.AdjustToContents) |
|
222 self.providerComboBox.setObjectName("providerComboBox") |
|
223 self.providerComboBox.setToolTip( |
|
224 self.tr("Select the code info provider")) |
|
225 self.providerComboBox.addItem(self.tr("<disabled>"), "disabled") |
|
226 self.horizontalLayout1.addWidget(self.providerComboBox) |
|
227 |
|
228 # top row 2 of widgets |
|
229 self.objectLineEdit = QLineEdit(self) |
|
230 self.objectLineEdit.setReadOnly(True) |
|
231 self.objectLineEdit.setObjectName("objectLineEdit") |
|
232 |
|
233 self.verticalLayout.addLayout(self.horizontalLayout1) |
|
234 self.verticalLayout.addWidget(self.objectLineEdit) |
|
235 |
|
236 # Rich Text (Web) Viewer |
|
237 self.__viewerWidget = DocumentationViewerWidget(self) |
|
238 self.__viewerWidget.setObjectName("__viewerWidget") |
|
239 self.verticalLayout.addWidget(self.__viewerWidget) |
|
240 |
|
241 # backward compatibility for plug-ins before 2018-09-17 |
|
242 Preferences.setDocuViewer("ShowInfoAsRichText", True) |
|
243 |
|
244 self.providerComboBox.currentIndexChanged[int].connect( |
|
245 self.on_providerComboBox_currentIndexChanged) |
|
246 |
|
247 def finalizeSetup(self): |
|
248 """ |
|
249 Public method to finalize the setup of the documentation viewer. |
|
250 """ |
|
251 self.__startingUp = False |
|
252 provider = Preferences.getDocuViewer("Provider") |
|
253 if provider in self.__providers: |
|
254 index = self.providerComboBox.findData(provider) |
|
255 else: |
|
256 index = 0 |
|
257 provider = self.__disabledProvider |
|
258 self.providerComboBox.setCurrentIndex(index) |
|
259 self.__selectedProvider = provider |
|
260 if index == 0: |
|
261 self.__showDisabledMessage() |
|
262 |
|
263 def registerProvider(self, providerName, providerDisplay, provider, |
|
264 supported): |
|
265 """ |
|
266 Public method register a source docu provider. |
|
267 |
|
268 @param providerName name of the provider (must be unique) |
|
269 @type str |
|
270 @param providerDisplay visible name of the provider |
|
271 @type str |
|
272 @param provider function to be called to determine source docu |
|
273 @type function(editor) |
|
274 @param supported function to be called to determine, if a language is |
|
275 supported |
|
276 @type function(language) |
|
277 @exception KeyError raised if a provider with the given name was |
|
278 already registered |
|
279 """ |
|
280 if providerName in self.__providers: |
|
281 raise KeyError( |
|
282 "Provider '{0}' already registered.".format(providerName)) |
|
283 |
|
284 self.__providers[providerName] = (provider, supported) |
|
285 self.providerComboBox.addItem(providerDisplay, providerName) |
|
286 |
|
287 self.providerAdded.emit() |
|
288 |
|
289 if ( |
|
290 self.__unregisterTimer.isActive() and |
|
291 providerName == self.__mostRecentlyUnregisteredProvider |
|
292 ): |
|
293 # this is assumed to be a plug-in reload |
|
294 self.__unregisterTimer.stop() |
|
295 self.__mostRecentlyUnregisteredProvider = None |
|
296 self.__selectProvider(providerName) |
|
297 |
|
298 def unregisterProvider(self, providerName): |
|
299 """ |
|
300 Public method register a source docu provider. |
|
301 |
|
302 @param providerName name of the provider (must be unique) |
|
303 @type str |
|
304 """ |
|
305 if providerName in self.__providers: |
|
306 if providerName == self.__selectedProvider: |
|
307 self.providerComboBox.setCurrentIndex(0) |
|
308 |
|
309 # in case this is just a temporary unregistration (< 30s) |
|
310 # e.g. when the plug-in is re-installed or updated |
|
311 self.__mostRecentlyUnregisteredProvider = providerName |
|
312 self.__unregisterTimer.start() |
|
313 |
|
314 del self.__providers[providerName] |
|
315 index = self.providerComboBox.findData(providerName) |
|
316 self.providerComboBox.removeItem(index) |
|
317 |
|
318 self.providerRemoved.emit() |
|
319 |
|
320 @pyqtSlot() |
|
321 def __unregisterTimerTimeout(self): |
|
322 """ |
|
323 Private slot handling the timeout signal of the unregister timer. |
|
324 """ |
|
325 self.__mostRecentlyUnregisteredProvider = None |
|
326 |
|
327 def isSupportedLanguage(self, language): |
|
328 """ |
|
329 Public method to check, if the given language is supported by the |
|
330 selected provider. |
|
331 |
|
332 @param language editor programming language to check |
|
333 @type str |
|
334 @return flag indicating the support status |
|
335 @rtype bool |
|
336 """ |
|
337 supported = False |
|
338 |
|
339 if self.__selectedProvider != self.__disabledProvider: |
|
340 supported = self.__providers[self.__selectedProvider][1](language) |
|
341 |
|
342 return supported |
|
343 |
|
344 def getProviders(self): |
|
345 """ |
|
346 Public method to get a list of providers and their visible strings. |
|
347 |
|
348 @return list containing the providers and their visible strings |
|
349 @rtype list of tuple of (str,str) |
|
350 """ |
|
351 providers = [] |
|
352 for index in range(1, self.providerComboBox.count()): |
|
353 provider = self.providerComboBox.itemData(index) |
|
354 text = self.providerComboBox.itemText(index) |
|
355 providers.append((provider, text)) |
|
356 |
|
357 return providers |
|
358 |
|
359 def showInfo(self, editor): |
|
360 """ |
|
361 Public method to request code documentation data from a provider. |
|
362 |
|
363 @param editor reference to the editor to request code docu for |
|
364 @type Editor |
|
365 """ |
|
366 line, index = editor.getCursorPosition() |
|
367 word = editor.getWord(line, index) |
|
368 if not word: |
|
369 # try again one index before |
|
370 word = editor.getWord(line, index - 1) |
|
371 self.objectLineEdit.setText(word) |
|
372 |
|
373 if self.__selectedProvider != self.__disabledProvider: |
|
374 self.__viewerWidget.clear() |
|
375 self.__providers[self.__selectedProvider][0](editor) |
|
376 |
|
377 def documentationReady(self, documentationInfo, isWarning=False, |
|
378 isDocWarning=False): |
|
379 """ |
|
380 Public method to provide the documentation info to the viewer. |
|
381 |
|
382 If documentationInfo is a dictionary, it should contain these |
|
383 (optional) keys and data: |
|
384 |
|
385 name: the name of the inspected object |
|
386 argspec: its arguments specification |
|
387 note: A phrase describing the type of object (function or method) and |
|
388 the module it belongs to. |
|
389 docstring: its documentation string |
|
390 typ: its type information |
|
391 |
|
392 @param documentationInfo dictionary containing the source docu data |
|
393 @type dict or str |
|
394 @param isWarning flag indicating a warning page |
|
395 @type bool |
|
396 @param isDocWarning flag indicating a documentation warning page |
|
397 @type bool |
|
398 """ |
|
399 self.__ui.activateCodeDocumentationViewer(switchFocus=False) |
|
400 |
|
401 if not isWarning and not isDocWarning: |
|
402 self.__lastDocumentation = documentationInfo |
|
403 |
|
404 if not documentationInfo: |
|
405 if self.__selectedProvider == self.__disabledProvider: |
|
406 self.__showDisabledMessage() |
|
407 else: |
|
408 self.documentationReady(self.tr("No documentation available"), |
|
409 isDocWarning=True) |
|
410 else: |
|
411 if isWarning: |
|
412 html = prepareDocumentationViewerHtmlWarningDocument( |
|
413 documentationInfo) |
|
414 elif isDocWarning: |
|
415 html = prepareDocumentationViewerHtmlDocWarningDocument( |
|
416 documentationInfo) |
|
417 elif isinstance(documentationInfo, dict): |
|
418 html = prepareDocumentationViewerHtmlDocument( |
|
419 documentationInfo) |
|
420 else: |
|
421 html = documentationInfo |
|
422 self.__viewerWidget.setHtml(html) |
|
423 |
|
424 def __showDisabledMessage(self): |
|
425 """ |
|
426 Private method to show a message giving the reason for being disabled. |
|
427 """ |
|
428 if len(self.__providers) == 0: |
|
429 self.documentationReady( |
|
430 self.tr("No source code documentation provider has been" |
|
431 " registered. This function has been disabled."), |
|
432 isWarning=True) |
|
433 else: |
|
434 self.documentationReady( |
|
435 self.tr("This function has been disabled."), |
|
436 isWarning=True) |
|
437 |
|
438 @pyqtSlot(int) |
|
439 def on_providerComboBox_currentIndexChanged(self, index): |
|
440 """ |
|
441 Private slot to handle the selection of a provider. |
|
442 |
|
443 @param index index of the selected provider |
|
444 @type int |
|
445 """ |
|
446 if not self.__shuttingDown and not self.__startingUp: |
|
447 self.__viewerWidget.clear() |
|
448 self.objectLineEdit.clear() |
|
449 |
|
450 provider = self.providerComboBox.itemData(index) |
|
451 if provider == self.__disabledProvider: |
|
452 self.__showDisabledMessage() |
|
453 else: |
|
454 self.__lastDocumentation = None |
|
455 |
|
456 Preferences.setDocuViewer("Provider", provider) |
|
457 self.__selectedProvider = provider |
|
458 |
|
459 def shutdown(self): |
|
460 """ |
|
461 Public method to perform shutdown actions. |
|
462 """ |
|
463 self.__shuttingDown = True |
|
464 Preferences.setDocuViewer("Provider", self.__selectedProvider) |
|
465 |
|
466 def preferencesChanged(self): |
|
467 """ |
|
468 Public slot to handle a change of preferences. |
|
469 """ |
|
470 provider = Preferences.getDocuViewer("Provider") |
|
471 self.__selectProvider(provider) |
|
472 |
|
473 def __selectProvider(self, provider): |
|
474 """ |
|
475 Private method to select a provider programmatically. |
|
476 |
|
477 @param provider name of the provider to be selected |
|
478 @type str |
|
479 """ |
|
480 if provider != self.__selectedProvider: |
|
481 index = self.providerComboBox.findData(provider) |
|
482 if index < 0: |
|
483 index = 0 |
|
484 self.providerComboBox.setCurrentIndex(index) |