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