1# -*- coding: utf-8 -*- 2# 3# Copyright © Spyder Project Contributors 4# Licensed under the terms of the MIT License 5# (see spyder/__init__.py for details) 6 7"""Help Plugin""" 8 9# Standard library imports 10import re 11import os.path as osp 12import socket 13import sys 14 15# Third party imports 16from qtpy import PYQT5 17from qtpy.QtCore import QThread, QUrl, Signal, Slot 18from qtpy.QtWidgets import (QActionGroup, QComboBox, QGroupBox, QHBoxLayout, 19 QLabel, QLineEdit, QMenu, QMessageBox, QSizePolicy, 20 QToolButton, QVBoxLayout, QWidget) 21from qtpy.QtWebEngineWidgets import QWebEnginePage, WEBENGINE 22 23# Local imports 24from spyder import dependencies 25from spyder.config.base import _, get_conf_path, get_module_source_path 26from spyder.config.fonts import DEFAULT_SMALL_DELTA 27from spyder.plugins import SpyderPluginWidget 28from spyder.plugins.configdialog import PluginConfigPage 29from spyder.py3compat import get_meth_class_inst, to_text_string 30from spyder.utils import icon_manager as ima 31from spyder.utils import programs 32from spyder.utils.help.sphinxify import (CSS_PATH, generate_context, 33 sphinxify, usage, warning) 34from spyder.utils.qthelpers import (add_actions, create_action, 35 create_toolbutton, create_plugin_layout) 36from spyder.widgets.browser import FrameWebView 37from spyder.widgets.comboboxes import EditableComboBox 38from spyder.widgets.findreplace import FindReplace 39from spyder.widgets.sourcecode import codeeditor 40 41 42# Sphinx dependency 43dependencies.add("sphinx", _("Show help for objects in the Editor and " 44 "Consoles in a dedicated pane"), 45 required_version='>=0.6.6') 46 47 48 49class ObjectComboBox(EditableComboBox): 50 """ 51 QComboBox handling object names 52 """ 53 # Signals 54 valid = Signal(bool, bool) 55 56 def __init__(self, parent): 57 EditableComboBox.__init__(self, parent) 58 self.help = parent 59 self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 60 self.tips = {True: '', False: ''} 61 62 def is_valid(self, qstr=None): 63 """Return True if string is valid""" 64 if not self.help.source_is_console(): 65 return True 66 if qstr is None: 67 qstr = self.currentText() 68 if not re.search(r'^[a-zA-Z0-9_\.]*$', str(qstr), 0): 69 return False 70 objtxt = to_text_string(qstr) 71 if self.help.get_option('automatic_import'): 72 shell = self.help.internal_shell 73 if shell is not None: 74 return shell.is_defined(objtxt, force_import=True) 75 shell = self.help.get_shell() 76 if shell is not None: 77 try: 78 return shell.is_defined(objtxt) 79 except socket.error: 80 shell = self.help.get_shell() 81 try: 82 return shell.is_defined(objtxt) 83 except socket.error: 84 # Well... too bad! 85 pass 86 87 def validate_current_text(self): 88 self.validate(self.currentText()) 89 90 def validate(self, qstr, editing=True): 91 """Reimplemented to avoid formatting actions""" 92 valid = self.is_valid(qstr) 93 if self.hasFocus() and valid is not None: 94 if editing: 95 # Combo box text is being modified: invalidate the entry 96 self.show_tip(self.tips[valid]) 97 self.valid.emit(False, False) 98 else: 99 # A new item has just been selected 100 if valid: 101 self.selected() 102 else: 103 self.valid.emit(False, False) 104 105 106class HelpConfigPage(PluginConfigPage): 107 def setup_page(self): 108 # Connections group 109 connections_group = QGroupBox(_("Automatic connections")) 110 connections_label = QLabel(_("This pane can automatically " 111 "show an object's help information after " 112 "a left parenthesis is written next to it. " 113 "Below you can decide to which plugin " 114 "you want to connect it to turn on this " 115 "feature.")) 116 connections_label.setWordWrap(True) 117 editor_box = self.create_checkbox(_("Editor"), 'connect/editor') 118 rope_installed = programs.is_module_installed('rope') 119 jedi_installed = programs.is_module_installed('jedi', '>=0.8.1') 120 editor_box.setEnabled(rope_installed or jedi_installed) 121 if not rope_installed and not jedi_installed: 122 editor_tip = _("This feature requires the Rope or Jedi libraries.\n" 123 "It seems you don't have either installed.") 124 editor_box.setToolTip(editor_tip) 125 ipython_box = self.create_checkbox(_("IPython Console"), 126 'connect/ipython_console') 127 128 connections_layout = QVBoxLayout() 129 connections_layout.addWidget(connections_label) 130 connections_layout.addWidget(editor_box) 131 connections_layout.addWidget(ipython_box) 132 connections_group.setLayout(connections_layout) 133 134 # Features group 135 features_group = QGroupBox(_("Additional features")) 136 math_box = self.create_checkbox(_("Render mathematical equations"), 137 'math') 138 req_sphinx = programs.is_module_installed('sphinx', '>=1.1') 139 math_box.setEnabled(req_sphinx) 140 if not req_sphinx: 141 sphinx_ver = programs.get_module_version('sphinx') 142 sphinx_tip = _("This feature requires Sphinx 1.1 or superior.") 143 sphinx_tip += "\n" + _("Sphinx %s is currently installed.") % sphinx_ver 144 math_box.setToolTip(sphinx_tip) 145 146 features_layout = QVBoxLayout() 147 features_layout.addWidget(math_box) 148 features_group.setLayout(features_layout) 149 150 # Source code group 151 sourcecode_group = QGroupBox(_("Source code")) 152 wrap_mode_box = self.create_checkbox(_("Wrap lines"), 'wrap') 153 154 sourcecode_layout = QVBoxLayout() 155 sourcecode_layout.addWidget(wrap_mode_box) 156 sourcecode_group.setLayout(sourcecode_layout) 157 158 # Final layout 159 vlayout = QVBoxLayout() 160 vlayout.addWidget(connections_group) 161 vlayout.addWidget(features_group) 162 vlayout.addWidget(sourcecode_group) 163 vlayout.addStretch(1) 164 self.setLayout(vlayout) 165 166 167class RichText(QWidget): 168 """ 169 WebView widget with find dialog 170 """ 171 def __init__(self, parent): 172 QWidget.__init__(self, parent) 173 174 self.webview = FrameWebView(self) 175 self.find_widget = FindReplace(self) 176 self.find_widget.set_editor(self.webview.web_widget) 177 self.find_widget.hide() 178 179 layout = QVBoxLayout() 180 layout.setContentsMargins(0, 0, 0, 0) 181 layout.addWidget(self.webview) 182 layout.addWidget(self.find_widget) 183 self.setLayout(layout) 184 185 def set_font(self, font, fixed_font=None): 186 """Set font""" 187 self.webview.set_font(font, fixed_font=fixed_font) 188 189 def set_html(self, html_text, base_url): 190 """Set html text""" 191 self.webview.setHtml(html_text, base_url) 192 193 def clear(self): 194 self.set_html('', self.webview.url()) 195 196 197class PlainText(QWidget): 198 """ 199 Read-only editor widget with find dialog 200 """ 201 # Signals 202 focus_changed = Signal() 203 204 def __init__(self, parent): 205 QWidget.__init__(self, parent) 206 self.editor = None 207 208 # Read-only editor 209 self.editor = codeeditor.CodeEditor(self) 210 self.editor.setup_editor(linenumbers=False, language='py', 211 scrollflagarea=False, edge_line=False) 212 self.editor.focus_changed.connect(lambda: self.focus_changed.emit()) 213 self.editor.setReadOnly(True) 214 215 # Find/replace widget 216 self.find_widget = FindReplace(self) 217 self.find_widget.set_editor(self.editor) 218 self.find_widget.hide() 219 220 layout = QVBoxLayout() 221 layout.setContentsMargins(0, 0, 0, 0) 222 layout.addWidget(self.editor) 223 layout.addWidget(self.find_widget) 224 self.setLayout(layout) 225 226 def set_font(self, font, color_scheme=None): 227 """Set font""" 228 self.editor.set_font(font, color_scheme=color_scheme) 229 230 def set_color_scheme(self, color_scheme): 231 """Set color scheme""" 232 self.editor.set_color_scheme(color_scheme) 233 234 def set_text(self, text, is_code): 235 self.editor.set_highlight_current_line(is_code) 236 self.editor.set_occurrence_highlighting(is_code) 237 if is_code: 238 self.editor.set_language('py') 239 else: 240 self.editor.set_language(None) 241 self.editor.set_text(text) 242 self.editor.set_cursor_position('sof') 243 244 def clear(self): 245 self.editor.clear() 246 247 248class SphinxThread(QThread): 249 """ 250 A worker thread for handling rich text rendering. 251 252 Parameters 253 ---------- 254 doc : str or dict 255 A string containing a raw rst text or a dict containing 256 the doc string components to be rendered. 257 See spyder.utils.dochelpers.getdoc for description. 258 context : dict 259 A dict containing the substitution variables for the 260 layout template 261 html_text_no_doc : unicode 262 Text to be rendered if doc string cannot be extracted. 263 math_option : bool 264 Use LaTeX math rendering. 265 266 """ 267 # Signals 268 error_msg = Signal(str) 269 html_ready = Signal(str) 270 271 def __init__(self, html_text_no_doc=''): 272 super(SphinxThread, self).__init__() 273 self.doc = None 274 self.context = None 275 self.html_text_no_doc = html_text_no_doc 276 self.math_option = False 277 278 def render(self, doc, context=None, math_option=False, img_path=''): 279 """Start thread to render a given documentation""" 280 # If the thread is already running wait for it to finish before 281 # starting it again. 282 if self.wait(): 283 self.doc = doc 284 self.context = context 285 self.math_option = math_option 286 self.img_path = img_path 287 # This causes run() to be executed in separate thread 288 self.start() 289 290 def run(self): 291 html_text = self.html_text_no_doc 292 doc = self.doc 293 if doc is not None: 294 if type(doc) is dict and 'docstring' in doc.keys(): 295 try: 296 context = generate_context(name=doc['name'], 297 argspec=doc['argspec'], 298 note=doc['note'], 299 math=self.math_option, 300 img_path=self.img_path) 301 html_text = sphinxify(doc['docstring'], context) 302 if doc['docstring'] == '': 303 if any([doc['name'], doc['argspec'], doc['note']]): 304 msg = _("No further documentation available") 305 html_text += '<div class="hr"></div>' 306 else: 307 msg = _("No documentation available") 308 html_text += '<div id="doc-warning">%s</div>' % msg 309 except Exception as error: 310 self.error_msg.emit(to_text_string(error)) 311 return 312 elif self.context is not None: 313 try: 314 html_text = sphinxify(doc, self.context) 315 except Exception as error: 316 self.error_msg.emit(to_text_string(error)) 317 return 318 self.html_ready.emit(html_text) 319 320 321class Help(SpyderPluginWidget): 322 """ 323 Docstrings viewer widget 324 """ 325 CONF_SECTION = 'help' 326 CONFIGWIDGET_CLASS = HelpConfigPage 327 LOG_PATH = get_conf_path(CONF_SECTION) 328 FONT_SIZE_DELTA = DEFAULT_SMALL_DELTA 329 330 # Signals 331 focus_changed = Signal() 332 333 def __init__(self, parent=None): 334 if PYQT5: 335 SpyderPluginWidget.__init__(self, parent, main = parent) 336 else: 337 SpyderPluginWidget.__init__(self, parent) 338 339 self.internal_shell = None 340 self.console = None 341 self.ipyconsole = None 342 self.editor = None 343 344 # Initialize plugin 345 self.initialize_plugin() 346 347 self.no_doc_string = _("No documentation available") 348 349 self._last_console_cb = None 350 self._last_editor_cb = None 351 352 self.plain_text = PlainText(self) 353 self.rich_text = RichText(self) 354 355 color_scheme = self.get_color_scheme() 356 self.set_plain_text_font(self.get_plugin_font(), color_scheme) 357 self.plain_text.editor.toggle_wrap_mode(self.get_option('wrap')) 358 359 # Add entries to read-only editor context-menu 360 self.wrap_action = create_action(self, _("Wrap lines"), 361 toggled=self.toggle_wrap_mode) 362 self.wrap_action.setChecked(self.get_option('wrap')) 363 self.plain_text.editor.readonly_menu.addSeparator() 364 add_actions(self.plain_text.editor.readonly_menu, (self.wrap_action,)) 365 366 self.set_rich_text_font(self.get_plugin_font('rich_text')) 367 368 self.shell = None 369 370 # locked = disable link with Console 371 self.locked = False 372 self._last_texts = [None, None] 373 self._last_editor_doc = None 374 375 # Object name 376 layout_edit = QHBoxLayout() 377 layout_edit.setContentsMargins(0, 0, 0, 0) 378 txt = _("Source") 379 if sys.platform == 'darwin': 380 source_label = QLabel(" " + txt) 381 else: 382 source_label = QLabel(txt) 383 layout_edit.addWidget(source_label) 384 self.source_combo = QComboBox(self) 385 self.source_combo.addItems([_("Console"), _("Editor")]) 386 self.source_combo.currentIndexChanged.connect(self.source_changed) 387 if (not programs.is_module_installed('rope') and 388 not programs.is_module_installed('jedi', '>=0.8.1')): 389 self.source_combo.hide() 390 source_label.hide() 391 layout_edit.addWidget(self.source_combo) 392 layout_edit.addSpacing(10) 393 layout_edit.addWidget(QLabel(_("Object"))) 394 self.combo = ObjectComboBox(self) 395 layout_edit.addWidget(self.combo) 396 self.object_edit = QLineEdit(self) 397 self.object_edit.setReadOnly(True) 398 layout_edit.addWidget(self.object_edit) 399 self.combo.setMaxCount(self.get_option('max_history_entries')) 400 self.combo.addItems( self.load_history() ) 401 self.combo.setItemText(0, '') 402 self.combo.valid.connect(lambda valid: self.force_refresh()) 403 404 # Plain text docstring option 405 self.docstring = True 406 self.rich_help = self.get_option('rich_mode', True) 407 self.plain_text_action = create_action(self, _("Plain Text"), 408 toggled=self.toggle_plain_text) 409 410 # Source code option 411 self.show_source_action = create_action(self, _("Show Source"), 412 toggled=self.toggle_show_source) 413 414 # Rich text option 415 self.rich_text_action = create_action(self, _("Rich Text"), 416 toggled=self.toggle_rich_text) 417 418 # Add the help actions to an exclusive QActionGroup 419 help_actions = QActionGroup(self) 420 help_actions.setExclusive(True) 421 help_actions.addAction(self.plain_text_action) 422 help_actions.addAction(self.rich_text_action) 423 424 # Automatic import option 425 self.auto_import_action = create_action(self, _("Automatic import"), 426 toggled=self.toggle_auto_import) 427 auto_import_state = self.get_option('automatic_import') 428 self.auto_import_action.setChecked(auto_import_state) 429 430 # Lock checkbox 431 self.locked_button = create_toolbutton(self, 432 triggered=self.toggle_locked) 433 layout_edit.addWidget(self.locked_button) 434 self._update_lock_icon() 435 436 # Option menu 437 options_button = create_toolbutton(self, text=_('Options'), 438 icon=ima.icon('tooloptions')) 439 options_button.setPopupMode(QToolButton.InstantPopup) 440 menu = QMenu(self) 441 add_actions(menu, [self.rich_text_action, self.plain_text_action, 442 self.show_source_action, None, 443 self.auto_import_action]) 444 options_button.setMenu(menu) 445 layout_edit.addWidget(options_button) 446 447 if self.rich_help: 448 self.switch_to_rich_text() 449 else: 450 self.switch_to_plain_text() 451 self.plain_text_action.setChecked(not self.rich_help) 452 self.rich_text_action.setChecked(self.rich_help) 453 self.source_changed() 454 455 # Main layout 456 layout = create_plugin_layout(layout_edit) 457 # we have two main widgets, but only one of them is shown at a time 458 layout.addWidget(self.plain_text) 459 layout.addWidget(self.rich_text) 460 self.setLayout(layout) 461 462 # Add worker thread for handling rich text rendering 463 self._sphinx_thread = SphinxThread( 464 html_text_no_doc=warning(self.no_doc_string)) 465 self._sphinx_thread.html_ready.connect( 466 self._on_sphinx_thread_html_ready) 467 self._sphinx_thread.error_msg.connect(self._on_sphinx_thread_error_msg) 468 469 # Handle internal and external links 470 view = self.rich_text.webview 471 if not WEBENGINE: 472 view.page().setLinkDelegationPolicy(QWebEnginePage.DelegateAllLinks) 473 view.linkClicked.connect(self.handle_link_clicks) 474 475 self._starting_up = True 476 477 #------ SpyderPluginWidget API --------------------------------------------- 478 def on_first_registration(self): 479 """Action to be performed on first plugin registration""" 480 self.main.tabify_plugins(self.main.variableexplorer, self) 481 482 def get_plugin_title(self): 483 """Return widget title""" 484 return _('Help') 485 486 def get_plugin_icon(self): 487 """Return widget icon""" 488 return ima.icon('help') 489 490 def get_focus_widget(self): 491 """ 492 Return the widget to give focus to when 493 this plugin's dockwidget is raised on top-level 494 """ 495 self.combo.lineEdit().selectAll() 496 return self.combo 497 498 def get_plugin_actions(self): 499 """Return a list of actions related to plugin""" 500 return [] 501 502 def register_plugin(self): 503 """Register plugin in Spyder's main window""" 504 self.focus_changed.connect(self.main.plugin_focus_changed) 505 self.main.add_dockwidget(self) 506 self.main.console.set_help(self) 507 508 self.internal_shell = self.main.console.shell 509 self.console = self.main.console 510 self.ipyconsole = self.main.ipyconsole 511 self.editor = self.main.editor 512 513 def closing_plugin(self, cancelable=False): 514 """Perform actions before parent main window is closed""" 515 return True 516 517 def refresh_plugin(self): 518 """Refresh widget""" 519 if self._starting_up: 520 self._starting_up = False 521 self.switch_to_rich_text() 522 self.show_intro_message() 523 524 def update_font(self): 525 """Update font from Preferences""" 526 color_scheme = self.get_color_scheme() 527 font = self.get_plugin_font() 528 rich_font = self.get_plugin_font(rich_text=True) 529 530 self.set_plain_text_font(font, color_scheme=color_scheme) 531 self.set_rich_text_font(rich_font) 532 533 def apply_plugin_settings(self, options): 534 """Apply configuration file's plugin settings""" 535 color_scheme_n = 'color_scheme_name' 536 color_scheme_o = self.get_color_scheme() 537 connect_n = 'connect_to_oi' 538 wrap_n = 'wrap' 539 wrap_o = self.get_option(wrap_n) 540 self.wrap_action.setChecked(wrap_o) 541 math_n = 'math' 542 math_o = self.get_option(math_n) 543 544 if color_scheme_n in options: 545 self.set_plain_text_color_scheme(color_scheme_o) 546 if wrap_n in options: 547 self.toggle_wrap_mode(wrap_o) 548 if math_n in options: 549 self.toggle_math_mode(math_o) 550 551 # To make auto-connection changes take place instantly 552 self.editor.apply_plugin_settings(options=[connect_n]) 553 self.ipyconsole.apply_plugin_settings(options=[connect_n]) 554 555 #------ Public API (related to Help's source) ------------------------- 556 def source_is_console(self): 557 """Return True if source is Console""" 558 return self.source_combo.currentIndex() == 0 559 560 def switch_to_editor_source(self): 561 self.source_combo.setCurrentIndex(1) 562 563 def switch_to_console_source(self): 564 self.source_combo.setCurrentIndex(0) 565 566 def source_changed(self, index=None): 567 if self.source_is_console(): 568 # Console 569 self.combo.show() 570 self.object_edit.hide() 571 self.show_source_action.setEnabled(True) 572 self.auto_import_action.setEnabled(True) 573 else: 574 # Editor 575 self.combo.hide() 576 self.object_edit.show() 577 self.show_source_action.setDisabled(True) 578 self.auto_import_action.setDisabled(True) 579 self.restore_text() 580 581 def save_text(self, callback): 582 if self.source_is_console(): 583 self._last_console_cb = callback 584 else: 585 self._last_editor_cb = callback 586 587 def restore_text(self): 588 if self.source_is_console(): 589 cb = self._last_console_cb 590 else: 591 cb = self._last_editor_cb 592 if cb is None: 593 if self.is_plain_text_mode(): 594 self.plain_text.clear() 595 else: 596 self.rich_text.clear() 597 else: 598 func = cb[0] 599 args = cb[1:] 600 func(*args) 601 if get_meth_class_inst(func) is self.rich_text: 602 self.switch_to_rich_text() 603 else: 604 self.switch_to_plain_text() 605 606 #------ Public API (related to rich/plain text widgets) -------------------- 607 @property 608 def find_widget(self): 609 if self.plain_text.isVisible(): 610 return self.plain_text.find_widget 611 else: 612 return self.rich_text.find_widget 613 614 def set_rich_text_font(self, font): 615 """Set rich text mode font""" 616 self.rich_text.set_font(font, fixed_font=self.get_plugin_font()) 617 618 def set_plain_text_font(self, font, color_scheme=None): 619 """Set plain text mode font""" 620 self.plain_text.set_font(font, color_scheme=color_scheme) 621 622 def set_plain_text_color_scheme(self, color_scheme): 623 """Set plain text mode color scheme""" 624 self.plain_text.set_color_scheme(color_scheme) 625 626 @Slot(bool) 627 def toggle_wrap_mode(self, checked): 628 """Toggle wrap mode""" 629 self.plain_text.editor.toggle_wrap_mode(checked) 630 self.set_option('wrap', checked) 631 632 def toggle_math_mode(self, checked): 633 """Toggle math mode""" 634 self.set_option('math', checked) 635 636 def is_plain_text_mode(self): 637 """Return True if plain text mode is active""" 638 return self.plain_text.isVisible() 639 640 def is_rich_text_mode(self): 641 """Return True if rich text mode is active""" 642 return self.rich_text.isVisible() 643 644 def switch_to_plain_text(self): 645 """Switch to plain text mode""" 646 self.rich_help = False 647 self.plain_text.show() 648 self.rich_text.hide() 649 self.plain_text_action.setChecked(True) 650 651 def switch_to_rich_text(self): 652 """Switch to rich text mode""" 653 self.rich_help = True 654 self.plain_text.hide() 655 self.rich_text.show() 656 self.rich_text_action.setChecked(True) 657 self.show_source_action.setChecked(False) 658 659 def set_plain_text(self, text, is_code): 660 """Set plain text docs""" 661 662 # text is coming from utils.dochelpers.getdoc 663 if type(text) is dict: 664 name = text['name'] 665 if name: 666 rst_title = ''.join(['='*len(name), '\n', name, '\n', 667 '='*len(name), '\n\n']) 668 else: 669 rst_title = '' 670 671 if text['argspec']: 672 definition = ''.join(['Definition: ', name, text['argspec'], 673 '\n']) 674 else: 675 definition = '' 676 677 if text['note']: 678 note = ''.join(['Type: ', text['note'], '\n\n----\n\n']) 679 else: 680 note = '' 681 682 full_text = ''.join([rst_title, definition, note, 683 text['docstring']]) 684 else: 685 full_text = text 686 687 self.plain_text.set_text(full_text, is_code) 688 self.save_text([self.plain_text.set_text, full_text, is_code]) 689 690 def set_rich_text_html(self, html_text, base_url): 691 """Set rich text""" 692 self.rich_text.set_html(html_text, base_url) 693 self.save_text([self.rich_text.set_html, html_text, base_url]) 694 695 def show_intro_message(self): 696 intro_message = _("Here you can get help of any object by pressing " 697 "%s in front of it, either on the Editor or the " 698 "Console.%s" 699 "Help can also be shown automatically after writing " 700 "a left parenthesis next to an object. You can " 701 "activate this behavior in %s.") 702 prefs = _("Preferences > Help") 703 if sys.platform == 'darwin': 704 shortcut = "Cmd+I" 705 else: 706 shortcut = "Ctrl+I" 707 708 if self.is_rich_text_mode(): 709 title = _("Usage") 710 tutorial_message = _("New to Spyder? Read our") 711 tutorial = _("tutorial") 712 intro_message = intro_message % ("<b>"+shortcut+"</b>", "<br><br>", 713 "<i>"+prefs+"</i>") 714 self.set_rich_text_html(usage(title, intro_message, 715 tutorial_message, tutorial), 716 QUrl.fromLocalFile(CSS_PATH)) 717 else: 718 install_sphinx = "\n\n%s" % _("Please consider installing Sphinx " 719 "to get documentation rendered in " 720 "rich text.") 721 intro_message = intro_message % (shortcut, "\n\n", prefs) 722 intro_message += install_sphinx 723 self.set_plain_text(intro_message, is_code=False) 724 725 def show_rich_text(self, text, collapse=False, img_path=''): 726 """Show text in rich mode""" 727 self.visibility_changed(True) 728 self.raise_() 729 self.switch_to_rich_text() 730 context = generate_context(collapse=collapse, img_path=img_path) 731 self.render_sphinx_doc(text, context) 732 733 def show_plain_text(self, text): 734 """Show text in plain mode""" 735 self.visibility_changed(True) 736 self.raise_() 737 self.switch_to_plain_text() 738 self.set_plain_text(text, is_code=False) 739 740 @Slot() 741 def show_tutorial(self): 742 """Show the Spyder tutorial in the Help plugin, opening it if needed""" 743 if not self.dockwidget.isVisible(): 744 self.dockwidget.show() 745 self.toggle_view_action.setChecked(True) 746 tutorial_path = get_module_source_path('spyder.utils.help') 747 tutorial = osp.join(tutorial_path, 'tutorial.rst') 748 text = open(tutorial).read() 749 self.show_rich_text(text, collapse=True) 750 751 def handle_link_clicks(self, url): 752 url = to_text_string(url.toString()) 753 if url == "spy://tutorial": 754 self.show_tutorial() 755 elif url.startswith('http'): 756 programs.start_file(url) 757 else: 758 self.rich_text.webview.load(QUrl(url)) 759 760 #------ Public API --------------------------------------------------------- 761 def force_refresh(self): 762 if self.source_is_console(): 763 self.set_object_text(None, force_refresh=True) 764 elif self._last_editor_doc is not None: 765 self.set_editor_doc(self._last_editor_doc, force_refresh=True) 766 767 def set_object_text(self, text, force_refresh=False, ignore_unknown=False): 768 """Set object analyzed by Help""" 769 if (self.locked and not force_refresh): 770 return 771 self.switch_to_console_source() 772 773 add_to_combo = True 774 if text is None: 775 text = to_text_string(self.combo.currentText()) 776 add_to_combo = False 777 778 found = self.show_help(text, ignore_unknown=ignore_unknown) 779 if ignore_unknown and not found: 780 return 781 782 if add_to_combo: 783 self.combo.add_text(text) 784 if found: 785 self.save_history() 786 787 if self.dockwidget is not None: 788 self.dockwidget.blockSignals(True) 789 self.__eventually_raise_help(text, force=force_refresh) 790 if self.dockwidget is not None: 791 self.dockwidget.blockSignals(False) 792 793 def set_editor_doc(self, doc, force_refresh=False): 794 """ 795 Use the help plugin to show docstring dictionary computed 796 with introspection plugin from the Editor plugin 797 """ 798 if (self.locked and not force_refresh): 799 return 800 self.switch_to_editor_source() 801 self._last_editor_doc = doc 802 self.object_edit.setText(doc['obj_text']) 803 804 if self.rich_help: 805 self.render_sphinx_doc(doc) 806 else: 807 self.set_plain_text(doc, is_code=False) 808 809 if self.dockwidget is not None: 810 self.dockwidget.blockSignals(True) 811 self.__eventually_raise_help(doc['docstring'], force=force_refresh) 812 if self.dockwidget is not None: 813 self.dockwidget.blockSignals(False) 814 815 def __eventually_raise_help(self, text, force=False): 816 index = self.source_combo.currentIndex() 817 if hasattr(self.main, 'tabifiedDockWidgets'): 818 # 'QMainWindow.tabifiedDockWidgets' was introduced in PyQt 4.5 819 if (self.dockwidget and (force or self.dockwidget.isVisible()) and 820 not self.ismaximized and 821 (force or text != self._last_texts[index])): 822 dockwidgets = self.main.tabifiedDockWidgets(self.dockwidget) 823 if (self.console.dockwidget not in dockwidgets and 824 self.ipyconsole is not None and 825 self.ipyconsole.dockwidget not in dockwidgets): 826 self.dockwidget.show() 827 self.dockwidget.raise_() 828 self._last_texts[index] = text 829 830 def load_history(self, obj=None): 831 """Load history from a text file in user home directory""" 832 if osp.isfile(self.LOG_PATH): 833 history = [line.replace('\n', '') 834 for line in open(self.LOG_PATH, 'r').readlines()] 835 else: 836 history = [] 837 return history 838 839 def save_history(self): 840 """Save history to a text file in user home directory""" 841 open(self.LOG_PATH, 'w').write("\n".join( \ 842 [to_text_string(self.combo.itemText(index)) 843 for index in range(self.combo.count())] )) 844 845 @Slot(bool) 846 def toggle_plain_text(self, checked): 847 """Toggle plain text docstring""" 848 if checked: 849 self.docstring = checked 850 self.switch_to_plain_text() 851 self.force_refresh() 852 self.set_option('rich_mode', not checked) 853 854 @Slot(bool) 855 def toggle_show_source(self, checked): 856 """Toggle show source code""" 857 if checked: 858 self.switch_to_plain_text() 859 self.docstring = not checked 860 self.force_refresh() 861 self.set_option('rich_mode', not checked) 862 863 @Slot(bool) 864 def toggle_rich_text(self, checked): 865 """Toggle between sphinxified docstrings or plain ones""" 866 if checked: 867 self.docstring = not checked 868 self.switch_to_rich_text() 869 self.set_option('rich_mode', checked) 870 871 @Slot(bool) 872 def toggle_auto_import(self, checked): 873 """Toggle automatic import feature""" 874 self.combo.validate_current_text() 875 self.set_option('automatic_import', checked) 876 self.force_refresh() 877 878 @Slot() 879 def toggle_locked(self): 880 """ 881 Toggle locked state 882 locked = disable link with Console 883 """ 884 self.locked = not self.locked 885 self._update_lock_icon() 886 887 def _update_lock_icon(self): 888 """Update locked state icon""" 889 icon = ima.icon('lock') if self.locked else ima.icon('lock_open') 890 self.locked_button.setIcon(icon) 891 tip = _("Unlock") if self.locked else _("Lock") 892 self.locked_button.setToolTip(tip) 893 894 def set_shell(self, shell): 895 """Bind to shell""" 896 self.shell = shell 897 898 def get_shell(self): 899 """ 900 Return shell which is currently bound to Help, 901 or another running shell if it has been terminated 902 """ 903 if (not hasattr(self.shell, 'get_doc') or 904 (hasattr(self.shell, 'is_running') and 905 not self.shell.is_running())): 906 self.shell = None 907 if self.ipyconsole is not None: 908 shell = self.ipyconsole.get_current_shellwidget() 909 if shell is not None and shell.kernel_client is not None: 910 self.shell = shell 911 if self.shell is None: 912 self.shell = self.internal_shell 913 return self.shell 914 915 def render_sphinx_doc(self, doc, context=None): 916 """Transform doc string dictionary to HTML and show it""" 917 # Math rendering option could have changed 918 if self.editor is not None: 919 fname = self.editor.get_current_filename() 920 dname = osp.dirname(fname) 921 else: 922 dname = '' 923 self._sphinx_thread.render(doc, context, self.get_option('math'), 924 dname) 925 926 def _on_sphinx_thread_html_ready(self, html_text): 927 """Set our sphinx documentation based on thread result""" 928 self._sphinx_thread.wait() 929 self.set_rich_text_html(html_text, QUrl.fromLocalFile(CSS_PATH)) 930 931 def _on_sphinx_thread_error_msg(self, error_msg): 932 """ Display error message on Sphinx rich text failure""" 933 self._sphinx_thread.wait() 934 self.plain_text_action.setChecked(True) 935 sphinx_ver = programs.get_module_version('sphinx') 936 QMessageBox.critical(self, 937 _('Help'), 938 _("The following error occured when calling " 939 "<b>Sphinx %s</b>. <br>Incompatible Sphinx " 940 "version or doc string decoding failed." 941 "<br><br>Error message:<br>%s" 942 ) % (sphinx_ver, error_msg)) 943 944 def show_help(self, obj_text, ignore_unknown=False): 945 """Show help""" 946 shell = self.get_shell() 947 if shell is None: 948 return 949 obj_text = to_text_string(obj_text) 950 951 if not shell.is_defined(obj_text): 952 if self.get_option('automatic_import') and \ 953 self.internal_shell.is_defined(obj_text, force_import=True): 954 shell = self.internal_shell 955 else: 956 shell = None 957 doc = None 958 source_text = None 959 960 if shell is not None: 961 doc = shell.get_doc(obj_text) 962 source_text = shell.get_source(obj_text) 963 964 is_code = False 965 966 if self.rich_help: 967 self.render_sphinx_doc(doc) 968 return doc is not None 969 elif self.docstring: 970 hlp_text = doc 971 if hlp_text is None: 972 hlp_text = source_text 973 if hlp_text is None: 974 hlp_text = self.no_doc_string 975 if ignore_unknown: 976 return False 977 else: 978 hlp_text = source_text 979 if hlp_text is None: 980 hlp_text = doc 981 if hlp_text is None: 982 hlp_text = _("No source code available.") 983 if ignore_unknown: 984 return False 985 else: 986 is_code = True 987 self.set_plain_text(hlp_text, is_code=is_code) 988 return True 989