src/eric7/UI/CodeDocumentationViewer.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
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)

eric ide

mercurial