eric7/UI/CodeDocumentationViewer.py

branch
eric7
changeset 8312
800c432b34c8
parent 8268
6b8128e0c9d1
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
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)

eric ide

mercurial