eric6/UI/CodeDocumentationViewer.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
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)

eric ide

mercurial