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