1# -*- coding: utf-8 -*- 2 3# Copyright (c) 2017 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> 4# 5 6""" 7Module implementing a widget to show some source code information provided by 8plug-ins. 9""" 10 11from PyQt5.QtCore import pyqtSlot, pyqtSignal, Qt, QUrl, QTimer 12from PyQt5.QtGui import QCursor 13from PyQt5.QtWidgets import ( 14 QWidget, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QSizePolicy, 15 QLineEdit, QTextBrowser, QToolTip 16) 17 18from E5Gui.E5TextEditSearchWidget import E5TextEditSearchWidget, E5TextEditType 19from E5Gui.E5Application import e5App 20 21import Preferences 22 23from .CodeDocumentationViewerTemplate import ( 24 prepareDocumentationViewerHtmlDocument, 25 prepareDocumentationViewerHtmlDocWarningDocument, 26 prepareDocumentationViewerHtmlWarningDocument 27) 28 29 30class 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 151class 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) 485