~ubuntu-branches/debian/experimental/spyder/experimental

« back to all changes in this revision

Viewing changes to spyderlib/plugins/ipythonconsole.py

  • Committer: Package Import Robot
  • Author(s): Picca Frédéric-Emmanuel
  • Date: 2013-02-27 09:51:28 UTC
  • mfrom: (1.1.18)
  • Revision ID: package-import@ubuntu.com-20130227095128-wtx1irpvf4vl79lj
Tags: 2.2.0~beta3+dfsg-1
* Imported Upstream version 2.2.0~beta3+dfsg
* debian /patches
  - 0002-feature-forwarded-add-icon-to-desktop-file.patch (deleted)
    this patch was integrated by the upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
#
3
 
# Copyright © 2012 Pierre Raybaut
4
 
# Licensed under the terms of the MIT License
5
 
# (see spyderlib/__init__.py for details)
6
 
 
7
 
"""IPython Console plugin
8
 
 
9
 
Handles IPython clients (and in the future, will handle IPython kernels too
10
 
 
11
 
# pylint: disable=C0103
12
 
# pylint: disable=R0903
13
 
# pylint: disable=R0911
14
 
# pylint: disable=R0201
15
 
 
16
 
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QWidget, QGroupBox,
17
 
                                QLineEdit, QInputDialog, QTabWidget, QMenu,
18
 
                                QFontComboBox, QHBoxLayout, QApplication,
19
 
                                QToolButton, QLabel, QKeySequence)
20
 
from spyderlib.qt.QtCore import SIGNAL, Qt, QUrl
21
 
 
22
 
import sys
23
 
import re
24
 
import os
25
 
import os.path as osp
26
 
import time
27
 
 
28
 
from IPython.config.loader import Config, load_pyconfig_files
29
 
from IPython.core.application import get_ipython_dir
30
 
 
31
 
# Local imports
32
 
from spyderlib.baseconfig import get_conf_path, _
33
 
from spyderlib.guiconfig import get_icon
34
 
from spyderlib.utils import programs
35
 
from spyderlib.utils.misc import get_error_match
36
 
from spyderlib.utils.qthelpers import (create_action, create_toolbutton,
37
 
                                       add_actions, get_std_icon)
38
 
from spyderlib.widgets.tabs import Tabs
39
 
from spyderlib.widgets.ipython import IPythonApp
40
 
from spyderlib.widgets.findreplace import FindReplace
41
 
from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage
42
 
from spyderlib.widgets.sourcecode import mixins
43
 
 
44
 
 
45
 
class IPythonConsoleConfigPage(PluginConfigPage):
46
 
    def __init__(self, plugin, parent):
47
 
        PluginConfigPage.__init__(self, plugin, parent)
48
 
        self.get_name = lambda: _("IPython console")
49
 
 
50
 
    def setup_page(self):
51
 
        newcb = self.create_checkbox
52
 
        mpl_present = programs.is_module_installed("matplotlib")
53
 
        
54
 
        # --- Display ---
55
 
        font_group = self.create_fontgroup(option=None, text=None,
56
 
                                    fontfilters=QFontComboBox.MonospacedFonts)
57
 
 
58
 
        # Interface Group
59
 
        interface_group = QGroupBox(_("Interface"))
60
 
        banner_box = newcb(_("Display initial banner"), 'show_banner',
61
 
                      tip=_("This option lets you hide the message shown at\n"
62
 
                            "the top of the console when it's opened."))
63
 
        gui_comp_box = newcb(_("Use a completion widget"),
64
 
                             'use_gui_completion',
65
 
                             tip=_("Use a widget instead of plain text "
66
 
                                   "output for tab completion"))
67
 
        pager_box = newcb(_("Use a pager to display additional text inside "
68
 
                            "the console"), 'use_pager',
69
 
                            tip=_("Useful if you don't want to fill the "
70
 
                                  "console with long help or completion texts.\n"
71
 
                                  "Note: Use the Q key to get out of the "
72
 
                                  "pager."))
73
 
        calltips_box = newcb(_("Display balloon tips"), 'show_calltips')
74
 
        ask_box = newcb(_("Ask for confirmation before closing"),
75
 
                        'ask_before_closing')
76
 
 
77
 
        interface_layout = QVBoxLayout()
78
 
        interface_layout.addWidget(banner_box)
79
 
        interface_layout.addWidget(gui_comp_box)
80
 
        interface_layout.addWidget(pager_box)
81
 
        interface_layout.addWidget(calltips_box)
82
 
        interface_layout.addWidget(ask_box)
83
 
        interface_group.setLayout(interface_layout)
84
 
        
85
 
        # Background Color Group
86
 
        bg_group = QGroupBox(_("Background color"))
87
 
        light_radio = self.create_radiobutton(_("Light background"),
88
 
                                              'light_color')
89
 
        dark_radio = self.create_radiobutton(_("Dark background"),
90
 
                                             'dark_color')
91
 
        bg_layout = QVBoxLayout()
92
 
        bg_layout.addWidget(light_radio)
93
 
        bg_layout.addWidget(dark_radio)
94
 
        bg_group.setLayout(bg_layout)
95
 
 
96
 
        # Source Code Group
97
 
        source_code_group = QGroupBox(_("Source code"))
98
 
        buffer_spin = self.create_spinbox(
99
 
                _("Buffer:  "), _(" lines"),
100
 
                'buffer_size', min_=-1, max_=1000000, step=100,
101
 
                tip=_("Set the maximum number of lines of text shown in the\n"
102
 
                      "console before truncation. Specifying -1 disables it\n"
103
 
                      "(not recommended!)"))
104
 
        source_code_layout = QVBoxLayout()
105
 
        source_code_layout.addWidget(buffer_spin)
106
 
        source_code_group.setLayout(source_code_layout)
107
 
        
108
 
        # --- Graphics ---
109
 
        # Pylab Group
110
 
        pylab_group = QGroupBox(_("Support for graphics (Pylab)"))
111
 
        pylab_box = newcb(_("Activate support"), 'pylab')
112
 
        autoload_pylab_box = newcb(_("Automatically load Pylab and NumPy"),
113
 
                               'pylab/autoload',
114
 
                               tip=_("This lets you load graphics support "
115
 
                                     "without importing \nthe commands to do "
116
 
                                     "plots. Useful to work with other\n"
117
 
                                     "plotting libraries different to "
118
 
                                     "Matplotlib or to develop \nGUIs with "
119
 
                                     "Spyder."))
120
 
        autoload_pylab_box.setEnabled(self.get_option('pylab') and mpl_present)
121
 
        self.connect(pylab_box, SIGNAL("toggled(bool)"),
122
 
                     autoload_pylab_box.setEnabled)
123
 
        
124
 
        pylab_layout = QVBoxLayout()
125
 
        pylab_layout.addWidget(pylab_box)
126
 
        pylab_layout.addWidget(autoload_pylab_box)
127
 
        pylab_group.setLayout(pylab_layout)
128
 
        
129
 
        if not mpl_present:
130
 
            self.set_option('pylab', False)
131
 
            self.set_option('pylab/autoload', False)
132
 
            pylab_group.setEnabled(False)
133
 
            pylab_tip = _("This feature requires the Matplotlib library.\n"
134
 
                          "It seems you don't have it installed.")
135
 
            pylab_box.setToolTip(pylab_tip)
136
 
        
137
 
        # Pylab backend Group
138
 
        inline = _("Inline")
139
 
        automatic = _("Automatic")
140
 
        backend_group = QGroupBox(_("Graphics backend"))
141
 
        bend_label = QLabel(_("Decide how graphics are going to be displayed "
142
 
                              "in the console. If unsure, please select "
143
 
                              "<b>%s</b> to put graphics inside the "
144
 
                              "console or <b>%s</b> to interact with "
145
 
                              "them (through zooming and panning) in a "
146
 
                              "separate window.") % (inline, automatic))
147
 
        bend_label.setWordWrap(True)
148
 
 
149
 
        backends = [(inline, 0), (automatic, 1), ("Qt", 2)]
150
 
        # TODO: Add gtk3 when 0.13 is released
151
 
        if sys.platform == 'darwin':
152
 
            backends.append( ("Mac OSX", 3) )
153
 
        if programs.is_module_installed('pygtk'):
154
 
            backends.append( ("Gtk", 4) )
155
 
        if programs.is_module_installed('wxPython'):
156
 
            backends.append( ("Wx", 5) )
157
 
        if programs.is_module_installed('_tkinter'):
158
 
            backends.append( ("Tkinter", 6) )
159
 
        backends = tuple(backends)
160
 
        
161
 
        backend_box = self.create_combobox( _("Backend:")+"   ", backends,
162
 
                                       'pylab/backend', default=0,
163
 
                                       tip=_("This option will be applied the "
164
 
                                             "next time a console is opened."))
165
 
        
166
 
        backend_layout = QVBoxLayout()
167
 
        backend_layout.addWidget(bend_label)
168
 
        backend_layout.addWidget(backend_box)
169
 
        backend_group.setLayout(backend_layout)
170
 
        backend_group.setEnabled(self.get_option('pylab') and mpl_present)
171
 
        self.connect(pylab_box, SIGNAL("toggled(bool)"),
172
 
                     backend_group.setEnabled)
173
 
        
174
 
        # Inline backend Group
175
 
        inline_group = QGroupBox(_("Inline backend"))
176
 
        inline_label = QLabel(_("Decide how to render the figures created by "
177
 
                                "this backend"))
178
 
        inline_label.setWordWrap(True)
179
 
        formats = (("PNG", 0), ("SVG", 1))
180
 
        format_box = self.create_combobox(_("Format:")+"   ", formats,
181
 
                                       'pylab/inline/figure_format', default=0)
182
 
        resolution_spin = self.create_spinbox(
183
 
                          _("Resolution:")+"  ", " "+_("dpi"),
184
 
                          'pylab/inline/resolution', min_=56, max_=112, step=1,
185
 
                          tip=_("Only used when the format is PNG. Default is "
186
 
                                "72"))
187
 
        width_spin = self.create_spinbox(
188
 
                          _("Width:")+"  ", " "+_("inches"),
189
 
                          'pylab/inline/width', min_=4, max_=20, step=1,
190
 
                          tip=_("Default is 6"))
191
 
        height_spin = self.create_spinbox(
192
 
                          _("Height:")+"  ", " "+_("inches"),
193
 
                          'pylab/inline/height', min_=4, max_=20, step=1,
194
 
                          tip=_("Default is 4"))
195
 
        
196
 
        inline_layout = QVBoxLayout()
197
 
        inline_layout.addWidget(inline_label)
198
 
        inline_layout.addWidget(format_box)
199
 
        inline_layout.addWidget(resolution_spin)
200
 
        inline_layout.addWidget(width_spin)
201
 
        inline_layout.addWidget(height_spin)
202
 
        inline_group.setLayout(inline_layout)
203
 
        inline_group.setEnabled(self.get_option('pylab') and mpl_present)
204
 
        self.connect(pylab_box, SIGNAL("toggled(bool)"),
205
 
                     inline_group.setEnabled)
206
 
 
207
 
        # --- Startup ---
208
 
        # Run lines Group
209
 
        run_lines_group = QGroupBox(_("Run code"))
210
 
        run_lines_label = QLabel(_("You can run several lines of code when "
211
 
                                   "a console is started. Please introduce "
212
 
                                   "each one separated by commas, for "
213
 
                                   "example:<br>"
214
 
                                   "<i>import os, import sys</i>"))
215
 
        run_lines_label.setWordWrap(True)
216
 
        run_lines_edit = self.create_lineedit(_("Lines:"), 'startup/run_lines',
217
 
                                              '', alignment=Qt.Horizontal)
218
 
        
219
 
        run_lines_layout = QVBoxLayout()
220
 
        run_lines_layout.addWidget(run_lines_label)
221
 
        run_lines_layout.addWidget(run_lines_edit)
222
 
        run_lines_group.setLayout(run_lines_layout)
223
 
        
224
 
        # Run file Group
225
 
        run_file_group = QGroupBox(_("Run a file"))
226
 
        run_file_label = QLabel(_("You can also run a whole file at startup "
227
 
                                  "instead of just some lines (This is "
228
 
                                  "similar to have a PYTHONSTARTUP file)."))
229
 
        run_file_label.setWordWrap(True)
230
 
        file_radio = newcb(_("Use the following file:"),
231
 
                           'startup/use_run_file', False)
232
 
        run_file_browser = self.create_browsefile('', 'startup/run_file', '')
233
 
        run_file_browser.setEnabled(False)
234
 
        self.connect(file_radio, SIGNAL("toggled(bool)"),
235
 
                     run_file_browser.setEnabled)
236
 
        
237
 
        run_file_layout = QVBoxLayout()
238
 
        run_file_layout.addWidget(run_file_label)
239
 
        run_file_layout.addWidget(file_radio)
240
 
        run_file_layout.addWidget(run_file_browser)
241
 
        run_file_group.setLayout(run_file_layout)
242
 
        
243
 
        # Spyder group
244
 
        spyder_group = QGroupBox(_("Spyder startup"))
245
 
        ipystartup_box = newcb(_("Open an IPython console at startup"),
246
 
                                 "open_ipython_at_startup")
247
 
        spyder_layout = QVBoxLayout()
248
 
        spyder_layout.addWidget(ipystartup_box)
249
 
        spyder_group.setLayout(spyder_layout)
250
 
        
251
 
        # ---- Advanced settings ----
252
 
        # Greedy completer group
253
 
        greedy_group = QGroupBox(_("Greedy completion"))
254
 
        greedy_label = QLabel(_("Enable <tt>Tab</tt> completion on elements "
255
 
                                "of lists, results of function calls, etc, "
256
 
                                "<i>without</i> assigning them to a "
257
 
                                "variable.<br>"
258
 
                                "For example, you can get completions on "
259
 
                                "things like <tt>li[0].&lt;Tab&gt;</tt> or "
260
 
                                "<tt>ins.meth().&lt;Tab&gt;</tt>"))
261
 
        greedy_label.setWordWrap(True)
262
 
        greedy_box = newcb(_("Use the greedy completer"), "greedy_completer",
263
 
                           tip="<b>Warning</b>: It can be unsafe because the "
264
 
                                "code is actually evaluated when you press "
265
 
                                "<tt>Tab</tt>.")
266
 
        
267
 
        greedy_layout = QVBoxLayout()
268
 
        greedy_layout.addWidget(greedy_label)
269
 
        greedy_layout.addWidget(greedy_box)
270
 
        greedy_group.setLayout(greedy_layout)
271
 
        
272
 
        # Autocall group
273
 
        autocall_group = QGroupBox(_("Autocall"))
274
 
        autocall_label = QLabel(_("Autocall makes IPython automatically call "
275
 
                                "any callable object even if you didn't type "
276
 
                                "explicit parentheses.<br>"
277
 
                                "For example, if you type <i>str 43</i> it "
278
 
                                "becomes <i>str(43)</i> automatically."))
279
 
        autocall_label.setWordWrap(True)
280
 
        
281
 
        smart = _('Smart')
282
 
        full = _('Full')
283
 
        autocall_opts = ((_('Off'), 0), (smart, 1), (full, 2))
284
 
        autocall_box = self.create_combobox(
285
 
                       _("Autocall:  "), autocall_opts, 'autocall', default=0,
286
 
                       tip=_("On <b>%s</b> mode, Autocall is not applied if "
287
 
                             "there are no arguments after the callable. On "
288
 
                             "<b>%s</b> mode, all callable objects are "
289
 
                             "automatically called (even if no arguments are "
290
 
                             "present).") % (smart, full))
291
 
        
292
 
        autocall_layout = QVBoxLayout()
293
 
        autocall_layout.addWidget(autocall_label)
294
 
        autocall_layout.addWidget(autocall_box)
295
 
        autocall_group.setLayout(autocall_layout)
296
 
        
297
 
        # Sympy group
298
 
        sympy_group = QGroupBox(_("Symbolic Mathematics"))
299
 
        sympy_label = QLabel(_("Perfom symbolic operations in the console "
300
 
                               "(e.g. integrals, derivatives, vector calculus, "
301
 
                               "etc) and get the outputs in a beautifully "
302
 
                               "printed style."))
303
 
        sympy_label.setWordWrap(True)
304
 
        sympy_box = newcb(_("Use symbolic math"), "symbolic_math",
305
 
                          tip=_("This option loads the Sympy library to work "
306
 
                                "with.<br>Please refer to its documentation to "
307
 
                                "learn how to use it."))
308
 
        
309
 
        sympy_layout = QVBoxLayout()
310
 
        sympy_layout.addWidget(sympy_label)
311
 
        sympy_layout.addWidget(sympy_box)
312
 
        sympy_group.setLayout(sympy_layout)
313
 
        
314
 
        sympy_present = programs.is_module_installed("sympy")
315
 
        if not sympy_present:
316
 
            self.set_option("symbolic_math", False)
317
 
            sympy_box.setEnabled(False)
318
 
            sympy_tip = _("This feature requires the Sympy library.\n"
319
 
                          "It seems you don't have it installed.")
320
 
            sympy_box.setToolTip(sympy_tip)
321
 
        
322
 
        # Prompts group
323
 
        prompts_group = QGroupBox(_("Prompts"))
324
 
        prompts_label = QLabel(_("Modify how Input and Output prompts are "
325
 
                                 "shown in the console."))
326
 
        prompts_label.setWordWrap(True)
327
 
        in_prompt_edit = self.create_lineedit(_("Input prompt:"),
328
 
                                    'in_prompt', '',
329
 
                                  _('Default is<br>'
330
 
                                    'In [&lt;span class="in-prompt-number"&gt;'
331
 
                                    '%i&lt;/span&gt;]:'),
332
 
                                    alignment=Qt.Horizontal)
333
 
        out_prompt_edit = self.create_lineedit(_("Output prompt:"),
334
 
                                   'out_prompt', '',
335
 
                                 _('Default is<br>'
336
 
                                   'Out[&lt;span class="out-prompt-number"&gt;'
337
 
                                   '%i&lt;/span&gt;]:'),
338
 
                                   alignment=Qt.Horizontal)
339
 
        
340
 
        prompts_layout = QVBoxLayout()
341
 
        prompts_layout.addWidget(prompts_label)
342
 
        prompts_layout.addWidget(in_prompt_edit)
343
 
        prompts_layout.addWidget(out_prompt_edit)
344
 
        prompts_group.setLayout(prompts_layout)
345
 
 
346
 
        # --- Tabs organization ---
347
 
        tabs = QTabWidget()
348
 
        tabs.addTab(self.create_tab(font_group, interface_group, bg_group,
349
 
                                    source_code_group), _("Display"))
350
 
        tabs.addTab(self.create_tab(pylab_group, backend_group, inline_group),
351
 
                                    _("Graphics"))
352
 
        tabs.addTab(self.create_tab(spyder_group, run_lines_group,
353
 
                                    run_file_group), _("Startup"))
354
 
        tabs.addTab(self.create_tab(greedy_group, autocall_group, sympy_group,
355
 
                                    prompts_group), _("Advanced Settings"))
356
 
 
357
 
        vlayout = QVBoxLayout()
358
 
        vlayout.addWidget(tabs)
359
 
        self.setLayout(vlayout)
360
 
 
361
 
 
362
 
#XXX: For now, we add this layer to the IPython widget (which is the
363
 
#     `ipython_widget` attribute of this `IPythonClient` class) even if this is
364
 
#     quite featureless: the IPythonClient has a vertical layout which contains
365
 
#     only the IPython widget inside it. So we could have directly made the 
366
 
#     IPythonClient class inherit from the IPython widget's class. However,
367
 
#     the latter is not yet clearly defined: IPython API is quite unclear and 
368
 
#     confusing for this matter, so I prefered to add this layer. But that's 
369
 
#     just a start: we should replace it by the correct inheritance logic in 
370
 
#     time.
371
 
class IPythonClient(QWidget, mixins.SaveHistoryMixin):
372
 
    """Spyder IPython client (or frontend)"""
373
 
    
374
 
    CONF_SECTION = 'ipython'
375
 
    SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime())
376
 
    
377
 
    def __init__(self, plugin, connection_file, kernel_widget_id, client_name,
378
 
                 ipython_widget, history_filename, menu_actions=None):
379
 
        super(IPythonClient, self).__init__(plugin)
380
 
        mixins.SaveHistoryMixin.__init__(self)
381
 
        self.options_button = None
382
 
 
383
 
        self.connection_file = connection_file
384
 
        self.kernel_widget_id = kernel_widget_id
385
 
        self.client_name = client_name        
386
 
        self.ipython_widget = ipython_widget
387
 
        self.menu_actions = menu_actions
388
 
        self.history_filename = get_conf_path(history_filename)
389
 
        self.history = []
390
 
        
391
 
        vlayout = QVBoxLayout()
392
 
        toolbar_buttons = self.get_toolbar_buttons()
393
 
        hlayout = QHBoxLayout()
394
 
        for button in toolbar_buttons:
395
 
            hlayout.addWidget(button)
396
 
        vlayout.addLayout(hlayout)
397
 
        vlayout.setContentsMargins(0, 0, 0, 0)
398
 
        vlayout.addWidget(self.ipython_widget)
399
 
        self.setLayout(vlayout)
400
 
        
401
 
        self.exit_callback = lambda: plugin.close_console(widget=self)
402
 
 
403
 
        # Connect the IPython widget to this IPython client:
404
 
        # (see spyderlib/widgets/ipython.py for more details about this)
405
 
        ipython_widget.set_ipython_client(self)
406
 
        
407
 
        # To save history
408
 
        self.ipython_widget.executing.connect(
409
 
                                      lambda c: self.add_to_history(command=c))
410
 
        
411
 
        # To update history after execution
412
 
        self.ipython_widget.executed.connect(self.update_history)
413
 
        
414
 
    #------ Public API --------------------------------------------------------
415
 
    def get_name(self):
416
 
        """Return client name"""
417
 
        return _("Console") + " " + self.client_name
418
 
    
419
 
    def get_control(self):
420
 
        """Return the text widget (or similar) to give focus to"""
421
 
        # page_control is the widget used for paging
422
 
        page_control = self.ipython_widget._page_control
423
 
        if page_control and page_control.isVisible():
424
 
            return page_control
425
 
        else:
426
 
            return self.ipython_widget._control
427
 
 
428
 
    def get_options_menu(self):
429
 
        """Return options menu"""
430
 
        # Kernel
431
 
        self.interrupt_action = create_action(self, _("Interrupt kernel"),
432
 
                                              icon=get_icon('terminate.png'),
433
 
                                              triggered=self.interrupt_kernel)
434
 
        self.restart_action = create_action(self, _("Restart kernel"),
435
 
                                            icon=get_icon('restart.png'),
436
 
                                            triggered=self.restart_kernel)
437
 
        
438
 
        # Help
439
 
        self.intro_action = create_action(self, _("Intro to IPython"),
440
 
                                          triggered=self._show_intro)
441
 
        self.quickref_action = create_action(self, _("Quick Reference"),
442
 
                                             triggered=self._show_quickref)
443
 
        self.guiref_action = create_action(self, _("Console help"),
444
 
                                           triggered=self._show_guiref)                    
445
 
        help_menu = QMenu(_("Help"), self)
446
 
        help_action = create_action(self, _("IPython Help"),
447
 
                                    icon=get_std_icon('DialogHelpButton'))
448
 
        help_action.setMenu(help_menu)
449
 
        add_actions(help_menu, (self.intro_action, self.guiref_action,
450
 
                                self.quickref_action))
451
 
        
452
 
        # Main menu
453
 
        if self.menu_actions is not None:
454
 
            actions = [self.interrupt_action, self.restart_action, None] +\
455
 
                      self.menu_actions + [None, help_menu]
456
 
        else:
457
 
            actions = [self.interrupt_action, self.restart_action, None,
458
 
                       help_menu]
459
 
        return actions
460
 
    
461
 
    def get_toolbar_buttons(self):
462
 
        """Return toolbar buttons list"""
463
 
        #TODO: Eventually add some buttons (Empty for now)
464
 
        # (see for example: spyderlib/widgets/externalshell/baseshell.py)
465
 
        buttons = []
466
 
        if self.options_button is None:
467
 
            options = self.get_options_menu()
468
 
            if options:
469
 
                self.options_button = create_toolbutton(self,
470
 
                        text=_("Options"), icon=get_icon('tooloptions.png'))
471
 
                self.options_button.setPopupMode(QToolButton.InstantPopup)
472
 
                menu = QMenu(self)
473
 
                add_actions(menu, options)
474
 
                self.options_button.setMenu(menu)
475
 
        if self.options_button is not None:
476
 
            buttons.append(self.options_button)
477
 
        return buttons
478
 
    
479
 
    def add_actions_to_context_menu(self, menu):
480
 
        """Add actions to IPython widget context menu"""
481
 
        # See spyderlib/widgets/ipython.py for more details on this method
482
 
        inspect_action = create_action(self, _("Inspect current object"),
483
 
                                    QKeySequence("Ctrl+I"),
484
 
                                    icon=get_std_icon('MessageBoxInformation'),
485
 
                                    triggered=self.inspect_object)
486
 
        clear_line_action = create_action(self, _("Clear line or block"),
487
 
                                          QKeySequence("Shift+Escape"),
488
 
                                          icon=get_icon('eraser.png'),
489
 
                                          triggered=self.clear_line)
490
 
        clear_console_action = create_action(self, _("Clear console"),
491
 
                                             QKeySequence("Ctrl+L"),
492
 
                                             icon=get_icon('clear.png'),
493
 
                                             triggered=self.clear_console)
494
 
        quit_action = create_action(self, _("&Quit"), icon='exit.png',
495
 
                                    triggered=self.exit_callback)
496
 
        add_actions(menu, (None, inspect_action, clear_line_action,
497
 
                           clear_console_action, None, quit_action))
498
 
        return menu
499
 
    
500
 
    def set_font(self, font):
501
 
        """Set IPython widget's font"""
502
 
        self.ipython_widget.font = font
503
 
    
504
 
    def interrupt_kernel(self):
505
 
        """Interrupt the associanted Spyder kernel if it's running"""
506
 
        self.ipython_widget.request_interrupt_kernel()
507
 
    
508
 
    def restart_kernel(self):
509
 
        """Restart the associanted Spyder kernel"""
510
 
        self.ipython_widget.request_restart_kernel()
511
 
    
512
 
    def inspect_object(self):
513
 
        """Show how to inspect an object with our object inspector"""
514
 
        self.ipython_widget._control.inspect_current_object()
515
 
    
516
 
    def clear_line(self):
517
 
        """Clear a console line"""
518
 
        self.ipython_widget._keyboard_quit()
519
 
    
520
 
    def clear_console(self):
521
 
        """Clear the whole console"""
522
 
        self.ipython_widget.execute("%clear")
523
 
    
524
 
    def if_kernel_dies(self, t):
525
 
        """
526
 
        Show a message in the console if the kernel dies.
527
 
        t is the time in seconds between the death and showing the message.
528
 
        """
529
 
        message = _("It seems the kernel died unexpectedly. Use "
530
 
                    "'Restart kernel' to continue using this console.")
531
 
        self.ipython_widget._append_plain_text(message + '\n')
532
 
    
533
 
    def update_history(self):
534
 
        self.history = self.ipython_widget._history
535
 
    
536
 
    def interrupt_message(self):
537
 
        """
538
 
        Print an interrupt message when the client is connected to an external
539
 
        kernel
540
 
        """
541
 
        message = _("Kernel process is either remote or unspecified. "
542
 
                    "Cannot interrupt")
543
 
        QMessageBox.information(self, "IPython", message)
544
 
    
545
 
    def restart_message(self):
546
 
        """
547
 
        Print a restart message when the client is connected to an external
548
 
        kernel
549
 
        """
550
 
        message = _("Kernel process is either remote or unspecified. "
551
 
                    "Cannot restart.")
552
 
        QMessageBox.information(self, "IPython", message)
553
 
    
554
 
    #------ Private API -------------------------------------------------------
555
 
    def _show_rich_help(self, text):
556
 
        """Use our Object Inspector to show IPython help texts in rich mode"""
557
 
        from spyderlib.utils.inspector import sphinxify as spx
558
 
        
559
 
        context = spx.generate_context(title='', argspec='', note='',
560
 
                                       math=False)
561
 
        html_text = spx.sphinxify(text, context)
562
 
        inspector = self.get_control().inspector
563
 
        inspector.switch_to_rich_text()
564
 
        inspector.set_rich_text_html(html_text,
565
 
                                     QUrl.fromLocalFile(spx.CSS_PATH))
566
 
    
567
 
    def _show_plain_help(self, text):
568
 
        """Use our Object Inspector to show IPython help texts in plain mode"""
569
 
        inspector = self.get_control().inspector
570
 
        inspector.switch_to_plain_text()
571
 
        inspector.set_plain_text(text, is_code=False)
572
 
    
573
 
    def _show_intro(self):
574
 
        """Show intro to IPython help"""
575
 
        from IPython.core.usage import interactive_usage
576
 
        self._show_rich_help(interactive_usage)
577
 
    
578
 
    def _show_guiref(self):
579
 
        """Show qtconsole help"""
580
 
        from IPython.core.usage import gui_reference
581
 
        self._show_rich_help(gui_reference)
582
 
    
583
 
    def _show_quickref(self):
584
 
        """Show IPython Cheat Sheet"""
585
 
        from IPython.core.usage import quick_reference
586
 
        self._show_plain_help(quick_reference)
587
 
    
588
 
    #---- Qt methods ----------------------------------------------------------
589
 
    def closeEvent(self, event):
590
 
        """Reimplement Qt method to stop sending the custom_restart_kernel_died
591
 
        signal"""
592
 
        self.ipython_widget.custom_restart = False
593
 
            
594
 
 
595
 
class IPythonConsole(SpyderPluginWidget):
596
 
    """IPython Console plugin"""
597
 
    CONF_SECTION = 'ipython_console'
598
 
    CONFIGWIDGET_CLASS = IPythonConsoleConfigPage
599
 
    def __init__(self, parent):
600
 
        SpyderPluginWidget.__init__(self, parent)
601
 
        
602
 
        self.ipython_app = None
603
 
        self.initialize_application()
604
 
 
605
 
        self.tabwidget = None
606
 
        self.menu_actions = None
607
 
        
608
 
        self.inspector = None # Object inspector plugin
609
 
        self.historylog = None # History log plugin
610
 
        
611
 
        self.shellwidgets = []
612
 
        
613
 
        # Initialize plugin
614
 
        self.initialize_plugin()
615
 
        
616
 
        layout = QVBoxLayout()
617
 
        self.tabwidget = Tabs(self, self.menu_actions)
618
 
        if hasattr(self.tabwidget, 'setDocumentMode')\
619
 
           and not sys.platform == 'darwin':
620
 
            # Don't set document mode to true on OSX because it generates
621
 
            # a crash when the console is detached from the main window
622
 
            # Fixes Issue 561
623
 
            self.tabwidget.setDocumentMode(True)
624
 
        self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
625
 
                     self.refresh_plugin)
626
 
        self.connect(self.tabwidget, SIGNAL('move_data(int,int)'),
627
 
                     self.move_tab)
628
 
                     
629
 
        self.tabwidget.set_close_function(self.close_console)
630
 
 
631
 
        layout.addWidget(self.tabwidget)
632
 
 
633
 
        # Find/replace widget
634
 
        self.find_widget = FindReplace(self)
635
 
        self.find_widget.hide()
636
 
        self.register_widget_shortcuts("Editor", self.find_widget)
637
 
        layout.addWidget(self.find_widget)
638
 
        
639
 
        self.setLayout(layout)
640
 
            
641
 
        # Accepting drops
642
 
        self.setAcceptDrops(True)
643
 
    
644
 
    #------ SpyderPluginWidget API --------------------------------------------
645
 
    def get_plugin_title(self):
646
 
        """Return widget title"""
647
 
        return _('IPython console')
648
 
    
649
 
    def get_plugin_icon(self):
650
 
        """Return widget icon"""
651
 
        return get_icon('ipython_console.png')
652
 
    
653
 
    def get_focus_widget(self):
654
 
        """
655
 
        Return the widget to give focus to when
656
 
        this plugin's dockwidget is raised on top-level
657
 
        """
658
 
        shellwidget = self.tabwidget.currentWidget()
659
 
        if shellwidget is not None:
660
 
            return shellwidget.get_control()
661
 
        
662
 
    def get_plugin_actions(self):
663
 
        """Return a list of actions related to plugin"""
664
 
        client_action = create_action(self, _("Connect to an existing kernel"),
665
 
                None,
666
 
                'ipython_console.png',
667
 
                _("Open a new IPython client connected to an external kernel"),
668
 
                triggered=self.new_client)
669
 
        
670
 
        # Add the action to the 'Interpreters' menu on the main window
671
 
        interact_menu_actions = [None, client_action]
672
 
        self.main.interact_menu_actions += interact_menu_actions
673
 
        
674
 
        # Plugin actions
675
 
        console = self.main.extconsole
676
 
        self.menu_actions = [console.ipython_kernel_action, client_action]
677
 
        
678
 
        return self.menu_actions
679
 
    
680
 
    def register_plugin(self):
681
 
        """Register plugin in Spyder's main window"""
682
 
        self.main.add_dockwidget(self)
683
 
        self.inspector = self.main.inspector
684
 
        self.historylog = self.main.historylog
685
 
        self.connect(self, SIGNAL('focus_changed()'),
686
 
                     self.main.plugin_focus_changed)
687
 
        if self.main.editor:
688
 
            self.connect(self, SIGNAL("edit_goto(QString,int,QString)"),
689
 
                         self.main.editor.load)
690
 
        
691
 
    def closing_plugin(self, cancelable=False):
692
 
        """Perform actions before parent main window is closed"""
693
 
        for shellwidget in self.shellwidgets:
694
 
            shellwidget.close()
695
 
        return True
696
 
    
697
 
    def refresh_plugin(self):
698
 
        """Refresh tabwidget"""
699
 
        clientwidget = None
700
 
        if self.tabwidget.count():
701
 
            # Give focus to the control widget of selected tab 
702
 
            clientwidget = self.tabwidget.currentWidget()
703
 
            control = clientwidget.get_control()
704
 
            control.setFocus()
705
 
            widgets = clientwidget.get_toolbar_buttons()+[5]
706
 
            
707
 
            # Change extconsole tab to the client's kernel widget
708
 
            idx = self.main.extconsole.get_shell_index_from_id(
709
 
                                                 clientwidget.kernel_widget_id)
710
 
            if idx is not None:
711
 
                self.main.extconsole.tabwidget.setCurrentIndex(idx)
712
 
        else:
713
 
            control = None
714
 
            widgets = []
715
 
        self.find_widget.set_editor(control)
716
 
        self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets})
717
 
        self.emit(SIGNAL('update_plugin_title()'))
718
 
    
719
 
    def apply_plugin_settings(self, options):
720
 
        """Apply configuration file's plugin settings"""
721
 
        font = self.get_plugin_font()
722
 
        for shellwidget in self.shellwidgets:
723
 
            shellwidget.set_font(font)
724
 
    
725
 
    #------ Public API --------------------------------------------------------
726
 
    def get_clients(self):
727
 
        """Return IPython client widgets list"""
728
 
        return [sw for sw in self.shellwidgets
729
 
                if isinstance(sw, IPythonClient)]
730
 
        
731
 
#    def get_kernels(self):
732
 
#        """Return IPython kernel widgets list"""
733
 
#        return [sw for sw in self.shellwidgets
734
 
#                if isinstance(sw, IPythonKernel)]
735
 
#        
736
 
 
737
 
    def get_focus_client(self):
738
 
        """Return client shellwidget which has focus, if any"""
739
 
        widget = QApplication.focusWidget()
740
 
        for client in self.get_clients():
741
 
            if widget is client or widget is client.get_control():
742
 
                return client
743
 
 
744
 
    def new_client(self, connection_file=None, kernel_widget_id=None):
745
 
        """Create a new IPython client"""
746
 
        cf = connection_file
747
 
        if cf is None:
748
 
            example = _('(for example: `kernel-3764.json`, or simply `3764`)')
749
 
            while True:
750
 
                cf, valid = QInputDialog.getText(self, _('IPython'),
751
 
                              _('Provide an IPython kernel connection file:')+\
752
 
                              '\n'+example,
753
 
                              QLineEdit.Normal)
754
 
                if valid:
755
 
                    cf = str(cf)
756
 
                    match = re.match('(kernel-|^)([a-fA-F0-9-]+)(.json|$)', cf)
757
 
                    kernel_num = match.groups()[1]
758
 
                    if kernel_num:
759
 
                        cf = 'kernel-%s.json' % kernel_num
760
 
                        break
761
 
                else:
762
 
                    return
763
 
 
764
 
        # Generating the client name and setting kernel_widget_id
765
 
        match = re.match('^kernel-([a-fA-F0-9-]+).json', cf)
766
 
        count = 0
767
 
        while True:
768
 
            client_name = match.groups()[0]+'/'+chr(65+count)
769
 
            for clw in self.get_clients():
770
 
                if clw.client_name == client_name:
771
 
                    kernel_widget_id = clw.kernel_widget_id
772
 
                    break
773
 
            else:
774
 
                break
775
 
            count += 1
776
 
        
777
 
        # Trying to get kernel_widget_id from the currently opened kernels if
778
 
        # the previous procedure fails. This could happen when the first
779
 
        # client connected to a kernel is closed but the kernel is left open
780
 
        # and you try to connect new clients to it
781
 
        if kernel_widget_id is None:
782
 
            console = self.main.extconsole
783
 
            for sw in console.shellwidgets:
784
 
                if sw.connection_file == cf:
785
 
                    kernel_widget_id = id(sw)
786
 
 
787
 
        # Creating the IPython client widget
788
 
        try:
789
 
            self.register_client(cf, kernel_widget_id, client_name)
790
 
        except (IOError, UnboundLocalError):
791
 
            QMessageBox.critical(self, _('IPython'),
792
 
                                 _("Unable to connect to IPython kernel "
793
 
                                   "<b>`%s`") % cf)
794
 
            return
795
 
 
796
 
    def client_config(self):
797
 
        """Generate a Config instance for IPython clients using our config
798
 
        system
799
 
        
800
 
        This let us create each client with its own config (as oppossed to
801
 
        IPythonQtConsoleApp, where all clients have the same config)
802
 
        """
803
 
        # ---- IPython config ----
804
 
        try:
805
 
            profile_path = osp.join(get_ipython_dir(), 'profile_default')
806
 
            full_ip_cfg = load_pyconfig_files(['ipython_qtconsole_config.py'],
807
 
                                              profile_path)
808
 
            
809
 
            # From the full config we only select the IPythonWidget section
810
 
            # because the others have no effect here.
811
 
            ip_cfg = Config({'IPythonWidget': full_ip_cfg.IPythonWidget})
812
 
        except:
813
 
            ip_cfg = Config()
814
 
       
815
 
        # ---- Spyder config ----
816
 
        spy_cfg = Config()
817
 
        
818
 
        # Make the pager widget a rich one (i.e a QTextEdit)
819
 
        spy_cfg.IPythonWidget.kind = 'rich'
820
 
        
821
 
        # Gui completion widget
822
 
        gui_comp_o = self.get_option('use_gui_completion')
823
 
        completions = {True: 'droplist', False: 'ncurses'}
824
 
        spy_cfg.IPythonWidget.gui_completion = completions[gui_comp_o]
825
 
 
826
 
        # Pager
827
 
        pager_o = self.get_option('use_pager')
828
 
        if pager_o:
829
 
            spy_cfg.IPythonWidget.paging = 'inside'
830
 
        else:
831
 
            spy_cfg.IPythonWidget.paging = 'none'
832
 
        
833
 
        # Calltips
834
 
        calltips_o = self.get_option('show_calltips')
835
 
        spy_cfg.IPythonWidget.enable_calltips = calltips_o
836
 
 
837
 
        # Buffer size
838
 
        buffer_size_o = self.get_option('buffer_size')
839
 
        spy_cfg.IPythonWidget.buffer_size = buffer_size_o
840
 
        
841
 
        # Prompts
842
 
        in_prompt_o = self.get_option('in_prompt')
843
 
        out_prompt_o = self.get_option('out_prompt')
844
 
        if in_prompt_o:
845
 
            spy_cfg.IPythonWidget.in_prompt = in_prompt_o
846
 
        if out_prompt_o:
847
 
            spy_cfg.IPythonWidget.out_prompt = out_prompt_o
848
 
        
849
 
        # Merge IPython and Spyder configs. Spyder prefs will have prevalence
850
 
        # over IPython ones
851
 
        ip_cfg._merge(spy_cfg)
852
 
        return ip_cfg
853
 
    
854
 
    def initialize_application(self):
855
 
        """Initialize IPython application"""
856
 
        #======================================================================
857
 
        # For IPython developers review [1]
858
 
        self.ipython_app = IPythonApp()
859
 
        # Is the following line really necessary?
860
 
        #self.ipython_app.initialize_all_except_qt()
861
 
        #======================================================================
862
 
 
863
 
    def register_client(self, connection_file, kernel_widget_id, client_name):
864
 
        """Register new IPython client"""
865
 
        #======================================================================
866
 
        # For IPython developers review [2]
867
 
        ipython_widget = self.ipython_app.create_new_client(connection_file,
868
 
                                                   config=self.client_config())
869
 
        #======================================================================
870
 
 
871
 
        shellwidget = IPythonClient(self, connection_file, kernel_widget_id,
872
 
                                    client_name, ipython_widget,
873
 
                                    history_filename='.history.py',
874
 
                                    menu_actions=self.menu_actions)
875
 
        # QTextEdit Widgets
876
 
        control = shellwidget.ipython_widget._control
877
 
        page_control = shellwidget.ipython_widget._page_control
878
 
        
879
 
        # For tracebacks
880
 
        self.connect(control, SIGNAL("go_to_error(QString)"), self.go_to_error)
881
 
 
882
 
        # Handle kernel interrupt
883
 
        extconsoles = self.main.extconsole.shellwidgets
884
 
        spyder_kernel = None
885
 
        if extconsoles:
886
 
            if extconsoles[-1].connection_file == connection_file:
887
 
                spyder_kernel = extconsoles[-1]
888
 
                ipython_widget.custom_interrupt_requested.connect(
889
 
                                              spyder_kernel.keyboard_interrupt)
890
 
        if spyder_kernel is None:
891
 
            ipython_widget.custom_interrupt_requested.connect(
892
 
                                                 shellwidget.interrupt_message)
893
 
        
894
 
        # Handle kernel restarts asked by the user
895
 
        if spyder_kernel is not None:
896
 
            ipython_widget.custom_restart_requested.connect(
897
 
                                                        self.create_new_kernel)
898
 
        else:
899
 
            ipython_widget.custom_restart_requested.connect(
900
 
                                                   shellwidget.restart_message)
901
 
        
902
 
        # Print a message if kernel dies unexpectedly
903
 
        ipython_widget.custom_restart_kernel_died.connect(
904
 
                                       lambda t: shellwidget.if_kernel_dies(t))
905
 
        
906
 
        # Connect text widget to our inspector
907
 
        if spyder_kernel is not None and self.inspector is not None:
908
 
            control.set_inspector(self.inspector)
909
 
        
910
 
        # Connect client to our history log
911
 
        if self.historylog is not None:
912
 
            self.historylog.add_history(shellwidget.history_filename)
913
 
            self.connect(shellwidget,
914
 
                         SIGNAL('append_to_history(QString,QString)'),
915
 
                         self.historylog.append_to_history)
916
 
        
917
 
        # Apply settings to newly created client widget:
918
 
        shellwidget.set_font( self.get_plugin_font() )
919
 
        
920
 
        # Add tab and change focus to it
921
 
        self.add_tab(shellwidget, name=shellwidget.get_name())
922
 
        self.connect(shellwidget, SIGNAL('focus_changed()'),
923
 
                     lambda: self.emit(SIGNAL('focus_changed()')))
924
 
        
925
 
        # Update the find widget if focus changes between control and
926
 
        # page_control
927
 
        self.find_widget.set_editor(control)
928
 
        if page_control:
929
 
            self.connect(control, SIGNAL('visibility_changed(bool)'),
930
 
                         self.refresh_plugin)
931
 
            self.connect(page_control, SIGNAL('visibility_changed(bool)'),
932
 
                         self.refresh_plugin)
933
 
            self.connect(page_control, SIGNAL('show_find_widget()'),
934
 
                         self.find_widget.show)
935
 
    
936
 
    def close_related_ipython_clients(self, client):
937
 
        """Close all IPython clients related to *client*, except itself"""
938
 
        for clw in self.shellwidgets[:]:
939
 
            if clw is not client and\
940
 
               clw.connection_file == client.connection_file:
941
 
                self.close_console(widget=clw)
942
 
    
943
 
    def get_ipython_widget(self, kernel_widget_id):
944
 
        """Return IPython widget (ipython_plugin.ipython_widget) 
945
 
        associated to kernel_widget_id"""
946
 
        for clw in self.shellwidgets:
947
 
            if clw.kernel_widget_id == kernel_widget_id:
948
 
                return clw.ipython_widget
949
 
        else:
950
 
            raise ValueError, "Unknown kernel widget ID %r" % kernel_widget_id
951
 
        
952
 
    def add_tab(self, widget, name):
953
 
        """Add tab"""
954
 
        self.shellwidgets.append(widget)
955
 
        index = self.tabwidget.addTab(widget, get_icon('ipython_console.png'),
956
 
                                      name)
957
 
        self.tabwidget.setCurrentIndex(index)
958
 
        if self.dockwidget and not self.ismaximized:
959
 
            self.dockwidget.setVisible(True)
960
 
            self.dockwidget.raise_()
961
 
        widget.get_control().setFocus()
962
 
        
963
 
    def move_tab(self, index_from, index_to):
964
 
        """
965
 
        Move tab (tabs themselves have already been moved by the tabwidget)
966
 
        """
967
 
        shell = self.shellwidgets.pop(index_from)
968
 
        self.shellwidgets.insert(index_to, shell)
969
 
        self.emit(SIGNAL('update_plugin_title()'))
970
 
        
971
 
    def close_console(self, index=None, widget=None, force=False):
972
 
        """Close console tab from index or widget (or close current tab)"""
973
 
        if not self.tabwidget.count():
974
 
            return
975
 
        if widget is not None:
976
 
            index = self.tabwidget.indexOf(widget)
977
 
        if index is None and widget is None:
978
 
            index = self.tabwidget.currentIndex()
979
 
        if index is not None:
980
 
            widget = self.tabwidget.widget(index)
981
 
 
982
 
        # Check if related clients or kernels are opened
983
 
        # and eventually ask before closing them
984
 
        if not force and isinstance(widget, IPythonClient):
985
 
            console = self.main.extconsole
986
 
            idx = console.get_shell_index_from_id(widget.kernel_widget_id)
987
 
            if idx is not None:
988
 
                close_all = True
989
 
                if self.get_option('ask_before_closing'):
990
 
                    ans = QMessageBox.question(self, self.get_plugin_title(),
991
 
                           _("%s will be closed.\n"
992
 
                             "Do you want to kill the associated kernel "
993
 
                             "and all of its clients?") % widget.get_name(),
994
 
                           QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
995
 
                    if ans == QMessageBox.Cancel:
996
 
                        return
997
 
                    close_all = ans == QMessageBox.Yes
998
 
                if close_all:
999
 
                    console.close_console(index=idx, from_ipython_client=True)
1000
 
                    self.close_related_ipython_clients(widget)
1001
 
        widget.close()
1002
 
        
1003
 
        # Note: widget index may have changed after closing related widgets
1004
 
        self.tabwidget.removeTab(self.tabwidget.indexOf(widget))
1005
 
        self.shellwidgets.remove(widget)
1006
 
 
1007
 
        self.emit(SIGNAL('update_plugin_title()'))
1008
 
        
1009
 
    def go_to_error(self, text):
1010
 
        """Go to error if relevant"""
1011
 
        match = get_error_match(unicode(text))
1012
 
        if match:
1013
 
            fname, lnb = match.groups()
1014
 
            self.emit(SIGNAL("edit_goto(QString,int,QString)"),
1015
 
                      osp.abspath(fname), int(lnb), '')
1016
 
    
1017
 
    def get_shell_index_from_id(self, shell_id):
1018
 
        """Return shellwidget index from id"""
1019
 
        for index, shell in enumerate(self.shellwidgets):
1020
 
            if id(shell) == shell_id:
1021
 
                return index
1022
 
    
1023
 
    def rename_ipython_client_tab(self, connection_file, client_widget_id):
1024
 
        """Add the pid of the kernel process to an IPython client tab"""
1025
 
        index = self.get_shell_index_from_id(client_widget_id)
1026
 
        match = re.match('^kernel-(\d+).json', connection_file)
1027
 
        if match is not None:  # should not fail, but we never know...
1028
 
            name = _("Console") + " " + match.groups()[0] + '/' + chr(65)
1029
 
            self.tabwidget.setTabText(index, name)
1030
 
    
1031
 
    def create_new_kernel(self):
1032
 
        """Create a new kernel if the user asks for it"""
1033
 
        # Took this bit of code (until if result == ) from the IPython project
1034
 
        # (frontend/qt/frontend_widget.py - restart_kernel).
1035
 
        # Licensed under the BSD license
1036
 
        message = _('Are you sure you want to restart the kernel?')
1037
 
        buttons = QMessageBox.Yes | QMessageBox.No
1038
 
        result = QMessageBox.question(self, _('Restart kernel?'),
1039
 
                                      message, buttons)
1040
 
        if result == QMessageBox.Yes:
1041
 
            console = self.main.extconsole
1042
 
            console.start_ipython_kernel(create_client=False)
1043
 
            kernel_widget = console.shellwidgets[-1]
1044
 
            self.connect(kernel_widget,
1045
 
                      SIGNAL('create_ipython_client(QString)'),
1046
 
                      lambda cf: self.connect_to_new_kernel(cf, kernel_widget))
1047
 
    
1048
 
    def connect_to_new_kernel(self, connection_file, kernel_widget):
1049
 
        """
1050
 
        After a new kernel is created, execute this action to connect the new
1051
 
        kernel to the old client
1052
 
        """
1053
 
        console = self.main.extconsole
1054
 
        shellwidget = self.tabwidget.currentWidget()
1055
 
        
1056
 
        # Close old kernel tab
1057
 
        idx = console.get_shell_index_from_id(shellwidget.kernel_widget_id)
1058
 
        console.close_console(index=idx, from_ipython_client=True)
1059
 
        
1060
 
        # Set attributes for the new kernel
1061
 
        console.set_ipython_kernel_attrs(connection_file, kernel_widget)
1062
 
        
1063
 
        # Connect client to new kernel
1064
 
        kernel_manager = self.ipython_app.create_kernel_manager(connection_file)        
1065
 
        shellwidget.ipython_widget.kernel_manager = kernel_manager
1066
 
        shellwidget.kernel_widget_id = id(kernel_widget)
1067
 
        shellwidget.get_control().setFocus()
1068
 
        
1069
 
        # Rename client tab
1070
 
        client_widget_id = id(shellwidget)
1071
 
        self.rename_ipython_client_tab(connection_file, client_widget_id)
1072
 
            
1073
 
    #----Drag and drop
1074
 
    #TODO: try and reimplement this block
1075
 
    # (this is still the original code block copied from externalconsole.py)
1076
 
#    def dragEnterEvent(self, event):
1077
 
#        """Reimplement Qt method
1078
 
#        Inform Qt about the types of data that the widget accepts"""
1079
 
#        source = event.mimeData()
1080
 
#        if source.hasUrls():
1081
 
#            if mimedata2url(source):
1082
 
#                pathlist = mimedata2url(source)
1083
 
#                shellwidget = self.tabwidget.currentWidget()
1084
 
#                if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
1085
 
#                    event.acceptProposedAction()
1086
 
#                elif shellwidget is None or not shellwidget.is_running():
1087
 
#                    event.ignore()
1088
 
#                else:
1089
 
#                    event.acceptProposedAction()
1090
 
#            else:
1091
 
#                event.ignore()
1092
 
#        elif source.hasText():
1093
 
#            event.acceptProposedAction()            
1094
 
#            
1095
 
#    def dropEvent(self, event):
1096
 
#        """Reimplement Qt method
1097
 
#        Unpack dropped data and handle it"""
1098
 
#        source = event.mimeData()
1099
 
#        shellwidget = self.tabwidget.currentWidget()
1100
 
#        if source.hasText():
1101
 
#            qstr = source.text()
1102
 
#            if is_python_script(unicode(qstr)):
1103
 
#                self.start(qstr)
1104
 
#            elif shellwidget:
1105
 
#                shellwidget.shell.insert_text(qstr)
1106
 
#        elif source.hasUrls():
1107
 
#            pathlist = mimedata2url(source)
1108
 
#            if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
1109
 
#                for fname in pathlist:
1110
 
#                    self.start(fname)
1111
 
#            elif shellwidget:
1112
 
#                shellwidget.shell.drop_pathlist(pathlist)
1113
 
#        event.acceptProposedAction()
1114
 
 
 
1
# -*- coding: utf-8 -*-
 
2
#
 
3
# Copyright © 2012 Pierre Raybaut
 
4
# Licensed under the terms of the MIT License
 
5
# (see spyderlib/__init__.py for details)
 
6
 
 
7
"""IPython Console plugin
 
8
 
 
9
Handles IPython clients (and in the future, will handle IPython kernels too
 
10
-- meanwhile, the external console plugin is handling them)"""
 
11
 
 
12
# pylint: disable=C0103
 
13
# pylint: disable=R0903
 
14
# pylint: disable=R0911
 
15
# pylint: disable=R0201
 
16
 
 
17
from spyderlib.qt.QtGui import (QVBoxLayout, QMessageBox, QWidget, QGroupBox,
 
18
                                QLineEdit, QInputDialog, QTabWidget, QMenu,
 
19
                                QFontComboBox, QHBoxLayout, QApplication,
 
20
                                QToolButton, QLabel, QKeySequence)
 
21
from spyderlib.qt.QtCore import SIGNAL, Qt, QUrl
 
22
 
 
23
import sys
 
24
import re
 
25
import os
 
26
import os.path as osp
 
27
import time
 
28
 
 
29
from IPython.config.loader import Config, load_pyconfig_files
 
30
from IPython.core.application import get_ipython_dir
 
31
 
 
32
# Local imports
 
33
from spyderlib.baseconfig import get_conf_path, _
 
34
from spyderlib.utils import programs
 
35
from spyderlib.utils.misc import (get_error_match,
 
36
                                  remove_trailing_single_backslash)
 
37
from spyderlib.utils.qthelpers import (get_icon, get_std_icon, create_action,
 
38
                                       create_toolbutton, add_actions)
 
39
from spyderlib.widgets.tabs import Tabs
 
40
from spyderlib.widgets.ipython import IPythonApp
 
41
from spyderlib.widgets.findreplace import FindReplace
 
42
from spyderlib.plugins import SpyderPluginWidget, PluginConfigPage
 
43
from spyderlib.widgets.sourcecode import mixins
 
44
 
 
45
 
 
46
class IPythonConsoleConfigPage(PluginConfigPage):
 
47
    def __init__(self, plugin, parent):
 
48
        PluginConfigPage.__init__(self, plugin, parent)
 
49
        self.get_name = lambda: _("IPython console")
 
50
 
 
51
    def setup_page(self):
 
52
        newcb = self.create_checkbox
 
53
        mpl_present = programs.is_module_installed("matplotlib")
 
54
        
 
55
        # --- Display ---
 
56
        font_group = self.create_fontgroup(option=None, text=None,
 
57
                                    fontfilters=QFontComboBox.MonospacedFonts)
 
58
 
 
59
        # Interface Group
 
60
        interface_group = QGroupBox(_("Interface"))
 
61
        banner_box = newcb(_("Display initial banner"), 'show_banner',
 
62
                      tip=_("This option lets you hide the message shown at\n"
 
63
                            "the top of the console when it's opened."))
 
64
        gui_comp_box = newcb(_("Use a completion widget"),
 
65
                             'use_gui_completion',
 
66
                             tip=_("Use a widget instead of plain text "
 
67
                                   "output for tab completion"))
 
68
        pager_box = newcb(_("Use a pager to display additional text inside "
 
69
                            "the console"), 'use_pager',
 
70
                            tip=_("Useful if you don't want to fill the "
 
71
                                  "console with long help or completion texts.\n"
 
72
                                  "Note: Use the Q key to get out of the "
 
73
                                  "pager."))
 
74
        calltips_box = newcb(_("Display balloon tips"), 'show_calltips')
 
75
        ask_box = newcb(_("Ask for confirmation before closing"),
 
76
                        'ask_before_closing')
 
77
 
 
78
        interface_layout = QVBoxLayout()
 
79
        interface_layout.addWidget(banner_box)
 
80
        interface_layout.addWidget(gui_comp_box)
 
81
        interface_layout.addWidget(pager_box)
 
82
        interface_layout.addWidget(calltips_box)
 
83
        interface_layout.addWidget(ask_box)
 
84
        interface_group.setLayout(interface_layout)
 
85
        
 
86
        # Background Color Group
 
87
        bg_group = QGroupBox(_("Background color"))
 
88
        light_radio = self.create_radiobutton(_("Light background"),
 
89
                                              'light_color')
 
90
        dark_radio = self.create_radiobutton(_("Dark background"),
 
91
                                             'dark_color')
 
92
        bg_layout = QVBoxLayout()
 
93
        bg_layout.addWidget(light_radio)
 
94
        bg_layout.addWidget(dark_radio)
 
95
        bg_group.setLayout(bg_layout)
 
96
 
 
97
        # Source Code Group
 
98
        source_code_group = QGroupBox(_("Source code"))
 
99
        buffer_spin = self.create_spinbox(
 
100
                _("Buffer:  "), _(" lines"),
 
101
                'buffer_size', min_=-1, max_=1000000, step=100,
 
102
                tip=_("Set the maximum number of lines of text shown in the\n"
 
103
                      "console before truncation. Specifying -1 disables it\n"
 
104
                      "(not recommended!)"))
 
105
        source_code_layout = QVBoxLayout()
 
106
        source_code_layout.addWidget(buffer_spin)
 
107
        source_code_group.setLayout(source_code_layout)
 
108
        
 
109
        # --- Graphics ---
 
110
        # Pylab Group
 
111
        pylab_group = QGroupBox(_("Support for graphics (Pylab)"))
 
112
        pylab_box = newcb(_("Activate support"), 'pylab')
 
113
        autoload_pylab_box = newcb(_("Automatically load Pylab and NumPy"),
 
114
                               'pylab/autoload',
 
115
                               tip=_("This lets you load graphics support "
 
116
                                     "without importing \nthe commands to do "
 
117
                                     "plots. Useful to work with other\n"
 
118
                                     "plotting libraries different to "
 
119
                                     "Matplotlib or to develop \nGUIs with "
 
120
                                     "Spyder."))
 
121
        autoload_pylab_box.setEnabled(self.get_option('pylab') and mpl_present)
 
122
        self.connect(pylab_box, SIGNAL("toggled(bool)"),
 
123
                     autoload_pylab_box.setEnabled)
 
124
        
 
125
        pylab_layout = QVBoxLayout()
 
126
        pylab_layout.addWidget(pylab_box)
 
127
        pylab_layout.addWidget(autoload_pylab_box)
 
128
        pylab_group.setLayout(pylab_layout)
 
129
        
 
130
        if not mpl_present:
 
131
            self.set_option('pylab', False)
 
132
            self.set_option('pylab/autoload', False)
 
133
            pylab_group.setEnabled(False)
 
134
            pylab_tip = _("This feature requires the Matplotlib library.\n"
 
135
                          "It seems you don't have it installed.")
 
136
            pylab_box.setToolTip(pylab_tip)
 
137
        
 
138
        # Pylab backend Group
 
139
        inline = _("Inline")
 
140
        automatic = _("Automatic")
 
141
        backend_group = QGroupBox(_("Graphics backend"))
 
142
        bend_label = QLabel(_("Decide how graphics are going to be displayed "
 
143
                              "in the console. If unsure, please select "
 
144
                              "<b>%s</b> to put graphics inside the "
 
145
                              "console or <b>%s</b> to interact with "
 
146
                              "them (through zooming and panning) in a "
 
147
                              "separate window.") % (inline, automatic))
 
148
        bend_label.setWordWrap(True)
 
149
 
 
150
        backends = [(inline, 0), (automatic, 1), ("Qt", 2)]
 
151
        # TODO: Add gtk3 when 0.13 is released
 
152
        if sys.platform == 'darwin':
 
153
            backends.append( ("Mac OSX", 3) )
 
154
        if programs.is_module_installed('pygtk'):
 
155
            backends.append( ("Gtk", 4) )
 
156
        if programs.is_module_installed('wxPython'):
 
157
            backends.append( ("Wx", 5) )
 
158
        if programs.is_module_installed('_tkinter'):
 
159
            backends.append( ("Tkinter", 6) )
 
160
        backends = tuple(backends)
 
161
        
 
162
        backend_box = self.create_combobox( _("Backend:")+"   ", backends,
 
163
                                       'pylab/backend', default=0,
 
164
                                       tip=_("This option will be applied the "
 
165
                                             "next time a console is opened."))
 
166
        
 
167
        backend_layout = QVBoxLayout()
 
168
        backend_layout.addWidget(bend_label)
 
169
        backend_layout.addWidget(backend_box)
 
170
        backend_group.setLayout(backend_layout)
 
171
        backend_group.setEnabled(self.get_option('pylab') and mpl_present)
 
172
        self.connect(pylab_box, SIGNAL("toggled(bool)"),
 
173
                     backend_group.setEnabled)
 
174
        
 
175
        # Inline backend Group
 
176
        inline_group = QGroupBox(_("Inline backend"))
 
177
        inline_label = QLabel(_("Decide how to render the figures created by "
 
178
                                "this backend"))
 
179
        inline_label.setWordWrap(True)
 
180
        formats = (("PNG", 0), ("SVG", 1))
 
181
        format_box = self.create_combobox(_("Format:")+"   ", formats,
 
182
                                       'pylab/inline/figure_format', default=0)
 
183
        resolution_spin = self.create_spinbox(
 
184
                          _("Resolution:")+"  ", " "+_("dpi"),
 
185
                          'pylab/inline/resolution', min_=56, max_=112, step=1,
 
186
                          tip=_("Only used when the format is PNG. Default is "
 
187
                                "72"))
 
188
        width_spin = self.create_spinbox(
 
189
                          _("Width:")+"  ", " "+_("inches"),
 
190
                          'pylab/inline/width', min_=4, max_=20, step=1,
 
191
                          tip=_("Default is 6"))
 
192
        height_spin = self.create_spinbox(
 
193
                          _("Height:")+"  ", " "+_("inches"),
 
194
                          'pylab/inline/height', min_=4, max_=20, step=1,
 
195
                          tip=_("Default is 4"))
 
196
        
 
197
        inline_layout = QVBoxLayout()
 
198
        inline_layout.addWidget(inline_label)
 
199
        inline_layout.addWidget(format_box)
 
200
        inline_layout.addWidget(resolution_spin)
 
201
        inline_layout.addWidget(width_spin)
 
202
        inline_layout.addWidget(height_spin)
 
203
        inline_group.setLayout(inline_layout)
 
204
        inline_group.setEnabled(self.get_option('pylab') and mpl_present)
 
205
        self.connect(pylab_box, SIGNAL("toggled(bool)"),
 
206
                     inline_group.setEnabled)
 
207
 
 
208
        # --- Startup ---
 
209
        # Run lines Group
 
210
        run_lines_group = QGroupBox(_("Run code"))
 
211
        run_lines_label = QLabel(_("You can run several lines of code when "
 
212
                                   "a console is started. Please introduce "
 
213
                                   "each one separated by commas, for "
 
214
                                   "example:<br>"
 
215
                                   "<i>import os, import sys</i>"))
 
216
        run_lines_label.setWordWrap(True)
 
217
        run_lines_edit = self.create_lineedit(_("Lines:"), 'startup/run_lines',
 
218
                                              '', alignment=Qt.Horizontal)
 
219
        
 
220
        run_lines_layout = QVBoxLayout()
 
221
        run_lines_layout.addWidget(run_lines_label)
 
222
        run_lines_layout.addWidget(run_lines_edit)
 
223
        run_lines_group.setLayout(run_lines_layout)
 
224
        
 
225
        # Run file Group
 
226
        run_file_group = QGroupBox(_("Run a file"))
 
227
        run_file_label = QLabel(_("You can also run a whole file at startup "
 
228
                                  "instead of just some lines (This is "
 
229
                                  "similar to have a PYTHONSTARTUP file)."))
 
230
        run_file_label.setWordWrap(True)
 
231
        file_radio = newcb(_("Use the following file:"),
 
232
                           'startup/use_run_file', False)
 
233
        run_file_browser = self.create_browsefile('', 'startup/run_file', '')
 
234
        run_file_browser.setEnabled(False)
 
235
        self.connect(file_radio, SIGNAL("toggled(bool)"),
 
236
                     run_file_browser.setEnabled)
 
237
        
 
238
        run_file_layout = QVBoxLayout()
 
239
        run_file_layout.addWidget(run_file_label)
 
240
        run_file_layout.addWidget(file_radio)
 
241
        run_file_layout.addWidget(run_file_browser)
 
242
        run_file_group.setLayout(run_file_layout)
 
243
        
 
244
        # Spyder group
 
245
        spyder_group = QGroupBox(_("Spyder startup"))
 
246
        ipystartup_box = newcb(_("Open an IPython console at startup"),
 
247
                                 "open_ipython_at_startup")
 
248
        spyder_layout = QVBoxLayout()
 
249
        spyder_layout.addWidget(ipystartup_box)
 
250
        spyder_group.setLayout(spyder_layout)
 
251
        
 
252
        # ---- Advanced settings ----
 
253
        # Greedy completer group
 
254
        greedy_group = QGroupBox(_("Greedy completion"))
 
255
        greedy_label = QLabel(_("Enable <tt>Tab</tt> completion on elements "
 
256
                                "of lists, results of function calls, etc, "
 
257
                                "<i>without</i> assigning them to a "
 
258
                                "variable.<br>"
 
259
                                "For example, you can get completions on "
 
260
                                "things like <tt>li[0].&lt;Tab&gt;</tt> or "
 
261
                                "<tt>ins.meth().&lt;Tab&gt;</tt>"))
 
262
        greedy_label.setWordWrap(True)
 
263
        greedy_box = newcb(_("Use the greedy completer"), "greedy_completer",
 
264
                           tip="<b>Warning</b>: It can be unsafe because the "
 
265
                                "code is actually evaluated when you press "
 
266
                                "<tt>Tab</tt>.")
 
267
        
 
268
        greedy_layout = QVBoxLayout()
 
269
        greedy_layout.addWidget(greedy_label)
 
270
        greedy_layout.addWidget(greedy_box)
 
271
        greedy_group.setLayout(greedy_layout)
 
272
        
 
273
        # Autocall group
 
274
        autocall_group = QGroupBox(_("Autocall"))
 
275
        autocall_label = QLabel(_("Autocall makes IPython automatically call "
 
276
                                "any callable object even if you didn't type "
 
277
                                "explicit parentheses.<br>"
 
278
                                "For example, if you type <i>str 43</i> it "
 
279
                                "becomes <i>str(43)</i> automatically."))
 
280
        autocall_label.setWordWrap(True)
 
281
        
 
282
        smart = _('Smart')
 
283
        full = _('Full')
 
284
        autocall_opts = ((_('Off'), 0), (smart, 1), (full, 2))
 
285
        autocall_box = self.create_combobox(
 
286
                       _("Autocall:  "), autocall_opts, 'autocall', default=0,
 
287
                       tip=_("On <b>%s</b> mode, Autocall is not applied if "
 
288
                             "there are no arguments after the callable. On "
 
289
                             "<b>%s</b> mode, all callable objects are "
 
290
                             "automatically called (even if no arguments are "
 
291
                             "present).") % (smart, full))
 
292
        
 
293
        autocall_layout = QVBoxLayout()
 
294
        autocall_layout.addWidget(autocall_label)
 
295
        autocall_layout.addWidget(autocall_box)
 
296
        autocall_group.setLayout(autocall_layout)
 
297
        
 
298
        # Sympy group
 
299
        sympy_group = QGroupBox(_("Symbolic Mathematics"))
 
300
        sympy_label = QLabel(_("Perfom symbolic operations in the console "
 
301
                               "(e.g. integrals, derivatives, vector calculus, "
 
302
                               "etc) and get the outputs in a beautifully "
 
303
                               "printed style."))
 
304
        sympy_label.setWordWrap(True)
 
305
        sympy_box = newcb(_("Use symbolic math"), "symbolic_math",
 
306
                          tip=_("This option loads the Sympy library to work "
 
307
                                "with.<br>Please refer to its documentation to "
 
308
                                "learn how to use it."))
 
309
        
 
310
        sympy_layout = QVBoxLayout()
 
311
        sympy_layout.addWidget(sympy_label)
 
312
        sympy_layout.addWidget(sympy_box)
 
313
        sympy_group.setLayout(sympy_layout)
 
314
        
 
315
        sympy_present = programs.is_module_installed("sympy")
 
316
        if not sympy_present:
 
317
            self.set_option("symbolic_math", False)
 
318
            sympy_box.setEnabled(False)
 
319
            sympy_tip = _("This feature requires the Sympy library.\n"
 
320
                          "It seems you don't have it installed.")
 
321
            sympy_box.setToolTip(sympy_tip)
 
322
        
 
323
        # Prompts group
 
324
        prompts_group = QGroupBox(_("Prompts"))
 
325
        prompts_label = QLabel(_("Modify how Input and Output prompts are "
 
326
                                 "shown in the console."))
 
327
        prompts_label.setWordWrap(True)
 
328
        in_prompt_edit = self.create_lineedit(_("Input prompt:"),
 
329
                                    'in_prompt', '',
 
330
                                  _('Default is<br>'
 
331
                                    'In [&lt;span class="in-prompt-number"&gt;'
 
332
                                    '%i&lt;/span&gt;]:'),
 
333
                                    alignment=Qt.Horizontal)
 
334
        out_prompt_edit = self.create_lineedit(_("Output prompt:"),
 
335
                                   'out_prompt', '',
 
336
                                 _('Default is<br>'
 
337
                                   'Out[&lt;span class="out-prompt-number"&gt;'
 
338
                                   '%i&lt;/span&gt;]:'),
 
339
                                   alignment=Qt.Horizontal)
 
340
        
 
341
        prompts_layout = QVBoxLayout()
 
342
        prompts_layout.addWidget(prompts_label)
 
343
        prompts_layout.addWidget(in_prompt_edit)
 
344
        prompts_layout.addWidget(out_prompt_edit)
 
345
        prompts_group.setLayout(prompts_layout)
 
346
 
 
347
        # --- Tabs organization ---
 
348
        tabs = QTabWidget()
 
349
        tabs.addTab(self.create_tab(font_group, interface_group, bg_group,
 
350
                                    source_code_group), _("Display"))
 
351
        tabs.addTab(self.create_tab(pylab_group, backend_group, inline_group),
 
352
                                    _("Graphics"))
 
353
        tabs.addTab(self.create_tab(spyder_group, run_lines_group,
 
354
                                    run_file_group), _("Startup"))
 
355
        tabs.addTab(self.create_tab(greedy_group, autocall_group, sympy_group,
 
356
                                    prompts_group), _("Advanced Settings"))
 
357
 
 
358
        vlayout = QVBoxLayout()
 
359
        vlayout.addWidget(tabs)
 
360
        self.setLayout(vlayout)
 
361
 
 
362
 
 
363
class IPythonClient(QWidget, mixins.SaveHistoryMixin):
 
364
    """
 
365
    Spyder IPython client or frontend.
 
366
 
 
367
    This is a layer on top of the IPython Qt widget (i.e. RichIPythonWidget +
 
368
    our additions = SpyderIPythonWidget), which becomes the ipywidget attribute
 
369
    of this class. We are doing this for several reasons:
 
370
 
 
371
    1. To add more variables and methods needed to connect the widget to other
 
372
       Spyder plugins and also increase its funcionality.
 
373
    2. To make it clear what has been added by us to IPython widgets.
 
374
    3. To avoid possible name conflicts between our widgets and theirs (e.g.
 
375
       self.history and self._history, respectively)
 
376
    """
 
377
    
 
378
    CONF_SECTION = 'ipython'
 
379
    SEPARATOR = '%s##---(%s)---' % (os.linesep*2, time.ctime())
 
380
    
 
381
    def __init__(self, plugin, connection_file, kernel_widget_id, client_name,
 
382
                 ipywidget, history_filename, menu_actions=None):
 
383
        super(IPythonClient, self).__init__(plugin)
 
384
        mixins.SaveHistoryMixin.__init__(self)
 
385
        self.options_button = None
 
386
 
 
387
        self.connection_file = connection_file
 
388
        self.kernel_widget_id = kernel_widget_id
 
389
        self.client_name = client_name        
 
390
        self.ipywidget = ipywidget
 
391
        self.menu_actions = menu_actions
 
392
        self.history_filename = get_conf_path(history_filename)
 
393
        self.history = []
 
394
        
 
395
        vlayout = QVBoxLayout()
 
396
        toolbar_buttons = self.get_toolbar_buttons()
 
397
        hlayout = QHBoxLayout()
 
398
        for button in toolbar_buttons:
 
399
            hlayout.addWidget(button)
 
400
        vlayout.addLayout(hlayout)
 
401
        vlayout.setContentsMargins(0, 0, 0, 0)
 
402
        vlayout.addWidget(self.ipywidget)
 
403
        self.setLayout(vlayout)
 
404
        
 
405
        self.exit_callback = lambda: plugin.close_console(client=self)
 
406
 
 
407
        # Connect the IPython widget to this IPython client:
 
408
        # (see spyderlib/widgets/ipython.py for more details about this)
 
409
        ipywidget.set_ipyclient(self)
 
410
        
 
411
        # To save history
 
412
        self.ipywidget.executing.connect(
 
413
                                      lambda c: self.add_to_history(command=c))
 
414
        
 
415
        # To update history after execution
 
416
        self.ipywidget.executed.connect(self.update_history)
 
417
        
 
418
    #------ Public API --------------------------------------------------------
 
419
    def get_name(self):
 
420
        """Return client name"""
 
421
        return _("Console") + " " + self.client_name
 
422
    
 
423
    def get_control(self):
 
424
        """Return the text widget (or similar) to give focus to"""
 
425
        # page_control is the widget used for paging
 
426
        page_control = self.ipywidget._page_control
 
427
        if page_control and page_control.isVisible():
 
428
            return page_control
 
429
        else:
 
430
            return self.ipywidget._control
 
431
 
 
432
    def get_options_menu(self):
 
433
        """Return options menu"""
 
434
        # Kernel
 
435
        self.interrupt_action = create_action(self, _("Interrupt kernel"),
 
436
                                              icon=get_icon('terminate.png'),
 
437
                                              triggered=self.interrupt_kernel)
 
438
        self.restart_action = create_action(self, _("Restart kernel"),
 
439
                                            icon=get_icon('restart.png'),
 
440
                                            triggered=self.restart_kernel)
 
441
        
 
442
        # Help
 
443
        self.intro_action = create_action(self, _("Intro to IPython"),
 
444
                                          triggered=self._show_intro)
 
445
        self.quickref_action = create_action(self, _("Quick Reference"),
 
446
                                             triggered=self._show_quickref)
 
447
        self.guiref_action = create_action(self, _("Console help"),
 
448
                                           triggered=self._show_guiref)                    
 
449
        help_menu = QMenu(_("Help"), self)
 
450
        help_action = create_action(self, _("IPython Help"),
 
451
                                    icon=get_std_icon('DialogHelpButton'))
 
452
        help_action.setMenu(help_menu)
 
453
        add_actions(help_menu, (self.intro_action, self.guiref_action,
 
454
                                self.quickref_action))
 
455
        
 
456
        # Main menu
 
457
        if self.menu_actions is not None:
 
458
            actions = [self.interrupt_action, self.restart_action, None] +\
 
459
                      self.menu_actions + [None, help_menu]
 
460
        else:
 
461
            actions = [self.interrupt_action, self.restart_action, None,
 
462
                       help_menu]
 
463
        return actions
 
464
    
 
465
    def get_toolbar_buttons(self):
 
466
        """Return toolbar buttons list"""
 
467
        #TODO: Eventually add some buttons (Empty for now)
 
468
        # (see for example: spyderlib/widgets/externalshell/baseshell.py)
 
469
        buttons = []
 
470
        if self.options_button is None:
 
471
            options = self.get_options_menu()
 
472
            if options:
 
473
                self.options_button = create_toolbutton(self,
 
474
                        text=_("Options"), icon=get_icon('tooloptions.png'))
 
475
                self.options_button.setPopupMode(QToolButton.InstantPopup)
 
476
                menu = QMenu(self)
 
477
                add_actions(menu, options)
 
478
                self.options_button.setMenu(menu)
 
479
        if self.options_button is not None:
 
480
            buttons.append(self.options_button)
 
481
        return buttons
 
482
    
 
483
    def add_actions_to_context_menu(self, menu):
 
484
        """Add actions to IPython widget context menu"""
 
485
        # See spyderlib/widgets/ipython.py for more details on this method
 
486
        inspect_action = create_action(self, _("Inspect current object"),
 
487
                                    QKeySequence("Ctrl+I"),
 
488
                                    icon=get_std_icon('MessageBoxInformation'),
 
489
                                    triggered=self.inspect_object)
 
490
        clear_line_action = create_action(self, _("Clear line or block"),
 
491
                                          QKeySequence("Shift+Escape"),
 
492
                                          icon=get_icon('eraser.png'),
 
493
                                          triggered=self.clear_line)
 
494
        clear_console_action = create_action(self, _("Clear console"),
 
495
                                             QKeySequence("Ctrl+L"),
 
496
                                             icon=get_icon('clear.png'),
 
497
                                             triggered=self.clear_console)
 
498
        quit_action = create_action(self, _("&Quit"), icon='exit.png',
 
499
                                    triggered=self.exit_callback)
 
500
        add_actions(menu, (None, inspect_action, clear_line_action,
 
501
                           clear_console_action, None, quit_action))
 
502
        return menu
 
503
    
 
504
    def set_font(self, font):
 
505
        """Set IPython widget's font"""
 
506
        self.ipywidget.font = font
 
507
    
 
508
    def interrupt_kernel(self):
 
509
        """Interrupt the associanted Spyder kernel if it's running"""
 
510
        self.ipywidget.request_interrupt_kernel()
 
511
    
 
512
    def restart_kernel(self):
 
513
        """Restart the associanted Spyder kernel"""
 
514
        self.ipywidget.request_restart_kernel()
 
515
    
 
516
    def inspect_object(self):
 
517
        """Show how to inspect an object with our object inspector"""
 
518
        self.ipywidget._control.inspect_current_object()
 
519
    
 
520
    def clear_line(self):
 
521
        """Clear a console line"""
 
522
        self.ipywidget._keyboard_quit()
 
523
    
 
524
    def clear_console(self):
 
525
        """Clear the whole console"""
 
526
        self.ipywidget.execute("%clear")
 
527
    
 
528
    def if_kernel_dies(self, t):
 
529
        """
 
530
        Show a message in the console if the kernel dies.
 
531
        t is the time in seconds between the death and showing the message.
 
532
        """
 
533
        message = _("It seems the kernel died unexpectedly. Use "
 
534
                    "'Restart kernel' to continue using this console.")
 
535
        self.ipywidget._append_plain_text(message + '\n')
 
536
    
 
537
    def update_history(self):
 
538
        self.history = self.ipywidget._history
 
539
    
 
540
    def interrupt_message(self):
 
541
        """
 
542
        Print an interrupt message when the client is connected to an external
 
543
        kernel
 
544
        """
 
545
        message = _("Kernel process is either remote or unspecified. "
 
546
                    "Cannot interrupt")
 
547
        QMessageBox.information(self, "IPython", message)
 
548
    
 
549
    def restart_message(self):
 
550
        """
 
551
        Print a restart message when the client is connected to an external
 
552
        kernel
 
553
        """
 
554
        message = _("Kernel process is either remote or unspecified. "
 
555
                    "Cannot restart.")
 
556
        QMessageBox.information(self, "IPython", message)
 
557
    
 
558
    #------ Private API -------------------------------------------------------
 
559
    def _show_rich_help(self, text):
 
560
        """Use our Object Inspector to show IPython help texts in rich mode"""
 
561
        from spyderlib.utils.inspector import sphinxify as spx
 
562
        
 
563
        context = spx.generate_context(title='', argspec='', note='',
 
564
                                       math=False)
 
565
        html_text = spx.sphinxify(text, context)
 
566
        inspector = self.get_control().inspector
 
567
        inspector.switch_to_rich_text()
 
568
        inspector.set_rich_text_html(html_text,
 
569
                                     QUrl.fromLocalFile(spx.CSS_PATH))
 
570
    
 
571
    def _show_plain_help(self, text):
 
572
        """Use our Object Inspector to show IPython help texts in plain mode"""
 
573
        inspector = self.get_control().inspector
 
574
        inspector.switch_to_plain_text()
 
575
        inspector.set_plain_text(text, is_code=False)
 
576
    
 
577
    def _show_intro(self):
 
578
        """Show intro to IPython help"""
 
579
        from IPython.core.usage import interactive_usage
 
580
        self._show_rich_help(interactive_usage)
 
581
    
 
582
    def _show_guiref(self):
 
583
        """Show qtconsole help"""
 
584
        from IPython.core.usage import gui_reference
 
585
        self._show_rich_help(gui_reference)
 
586
    
 
587
    def _show_quickref(self):
 
588
        """Show IPython Cheat Sheet"""
 
589
        from IPython.core.usage import quick_reference
 
590
        self._show_plain_help(quick_reference)
 
591
    
 
592
    #---- Qt methods ----------------------------------------------------------
 
593
    def closeEvent(self, event):
 
594
        """Reimplement Qt method to stop sending the custom_restart_kernel_died
 
595
        signal"""
 
596
        self.ipywidget.custom_restart = False
 
597
            
 
598
 
 
599
class IPythonConsole(SpyderPluginWidget):
 
600
    """
 
601
    IPython Console plugin
 
602
 
 
603
    This is a widget with tabs where each one is an IPythonClient
 
604
    """
 
605
 
 
606
    CONF_SECTION = 'ipython_console'
 
607
    CONFIGWIDGET_CLASS = IPythonConsoleConfigPage
 
608
    def __init__(self, parent):
 
609
        SpyderPluginWidget.__init__(self, parent)
 
610
        
 
611
        self.ipython_app = None
 
612
        self.initialize_application()
 
613
 
 
614
        self.tabwidget = None
 
615
        self.menu_actions = None
 
616
        
 
617
        self.inspector = None # Object inspector plugin
 
618
        self.historylog = None # History log plugin
 
619
        
 
620
        self.clients = []
 
621
        
 
622
        # Initialize plugin
 
623
        self.initialize_plugin()
 
624
        
 
625
        layout = QVBoxLayout()
 
626
        self.tabwidget = Tabs(self, self.menu_actions)
 
627
        if hasattr(self.tabwidget, 'setDocumentMode')\
 
628
           and not sys.platform == 'darwin':
 
629
            # Don't set document mode to true on OSX because it generates
 
630
            # a crash when the console is detached from the main window
 
631
            # Fixes Issue 561
 
632
            self.tabwidget.setDocumentMode(True)
 
633
        self.connect(self.tabwidget, SIGNAL('currentChanged(int)'),
 
634
                     self.refresh_plugin)
 
635
        self.connect(self.tabwidget, SIGNAL('move_data(int,int)'),
 
636
                     self.move_tab)
 
637
                     
 
638
        self.tabwidget.set_close_function(self.close_console)
 
639
 
 
640
        layout.addWidget(self.tabwidget)
 
641
 
 
642
        # Find/replace widget
 
643
        self.find_widget = FindReplace(self)
 
644
        self.find_widget.hide()
 
645
        self.register_widget_shortcuts("Editor", self.find_widget)
 
646
        layout.addWidget(self.find_widget)
 
647
        
 
648
        self.setLayout(layout)
 
649
            
 
650
        # Accepting drops
 
651
        self.setAcceptDrops(True)
 
652
    
 
653
    #------ SpyderPluginWidget API --------------------------------------------
 
654
    def get_plugin_title(self):
 
655
        """Return widget title"""
 
656
        return _('IPython console')
 
657
    
 
658
    def get_plugin_icon(self):
 
659
        """Return widget icon"""
 
660
        return get_icon('ipython_console.png')
 
661
    
 
662
    def get_focus_widget(self):
 
663
        """
 
664
        Return the widget to give focus to when
 
665
        this plugin's dockwidget is raised on top-level
 
666
        """
 
667
        client = self.tabwidget.currentWidget()
 
668
        if client is not None:
 
669
            return client.get_control()
 
670
 
 
671
    def get_current_client(self):
 
672
        """
 
673
        Return the currently selected client
 
674
        """
 
675
        client = self.tabwidget.currentWidget()
 
676
        if client is not None:
 
677
            return client
 
678
 
 
679
    def run_script_in_current_client(self, filename, wdir, args, debug):
 
680
        """Run script in current client, if any"""
 
681
        norm = lambda text: remove_trailing_single_backslash(unicode(text))
 
682
        client = self.get_current_client()
 
683
        if client is not None:
 
684
            # Internal kernels, use runfile
 
685
            if client.kernel_widget_id is not None:
 
686
                line = "%s(r'%s'" % ('debugfile' if debug else 'runfile',
 
687
                                     unicode(filename))
 
688
                if args:
 
689
                    line += ", args=r'%s'" % norm(args)
 
690
                if wdir:
 
691
                    line += ", wdir=r'%s'" % norm(wdir)
 
692
                line += ")"
 
693
            else: # External kernels, use %run
 
694
                line = "%run "
 
695
                if debug:
 
696
                    line += "-d "
 
697
                line += "\"%s\"" % unicode(filename)
 
698
                if args:
 
699
                    line += " %s" % norm(args)
 
700
            self.execute_python_code(line)
 
701
 
 
702
    def execute_python_code(self, lines):
 
703
        client = self.get_current_client()
 
704
        if client is not None:
 
705
            client.ipywidget.execute(unicode(lines))
 
706
            self.activateWindow()
 
707
            client.get_control().setFocus()
 
708
 
 
709
    def write_to_stdin(self, line):
 
710
        client = self.get_current_client()
 
711
        if client is not None:
 
712
            client.ipywidget.write_to_stdin(line)
 
713
    
 
714
    def get_plugin_actions(self):
 
715
        """Return a list of actions related to plugin"""
 
716
        client_action = create_action(self, _("Connect to an existing kernel"),
 
717
                None,
 
718
                'ipython_console.png',
 
719
                _("Open a new IPython client connected to an external kernel"),
 
720
                triggered=self.new_client)
 
721
        
 
722
        # Add the action to the 'Interpreters' menu on the main window
 
723
        interact_menu_actions = [None, client_action]
 
724
        self.main.interact_menu_actions += interact_menu_actions
 
725
        
 
726
        # Plugin actions
 
727
        extconsole = self.main.extconsole
 
728
        self.menu_actions = [extconsole.ipykernel_action, client_action]
 
729
        
 
730
        return self.menu_actions
 
731
    
 
732
    def register_plugin(self):
 
733
        """Register plugin in Spyder's main window"""
 
734
        self.main.add_dockwidget(self)
 
735
        self.inspector = self.main.inspector
 
736
        self.historylog = self.main.historylog
 
737
        self.connect(self, SIGNAL('focus_changed()'),
 
738
                     self.main.plugin_focus_changed)
 
739
        self.connect(self, SIGNAL("edit_goto(QString,int,QString)"),
 
740
                     self.main.editor.load)
 
741
        self.connect(self.main.editor,
 
742
                     SIGNAL('run_in_current_ipyclient(QString,QString,QString,bool)'),
 
743
                     self.run_script_in_current_client)
 
744
        
 
745
    def closing_plugin(self, cancelable=False):
 
746
        """Perform actions before parent main window is closed"""
 
747
        for client in self.clients:
 
748
            client.close()
 
749
        return True
 
750
    
 
751
    def refresh_plugin(self):
 
752
        """Refresh tabwidget"""
 
753
        client = None
 
754
        if self.tabwidget.count():
 
755
            # Give focus to the control widget of the selected tab
 
756
            client = self.tabwidget.currentWidget()
 
757
            control = client.get_control()
 
758
            control.setFocus()
 
759
            widgets = client.get_toolbar_buttons()+[5]
 
760
            
 
761
            # Change extconsole tab to the client's kernel widget
 
762
            idx = self.main.extconsole.get_shell_index_from_id(
 
763
                                                       client.kernel_widget_id)
 
764
            if idx is not None:
 
765
                self.main.extconsole.tabwidget.setCurrentIndex(idx)
 
766
        else:
 
767
            control = None
 
768
            widgets = []
 
769
        self.find_widget.set_editor(control)
 
770
        self.tabwidget.set_corner_widgets({Qt.TopRightCorner: widgets})
 
771
        self.emit(SIGNAL('update_plugin_title()'))
 
772
    
 
773
    def apply_plugin_settings(self, options):
 
774
        """Apply configuration file's plugin settings"""
 
775
        font = self.get_plugin_font()
 
776
        for client in self.clients:
 
777
            client.set_font(font)
 
778
    
 
779
    #------ Public API --------------------------------------------------------
 
780
    def get_clients(self):
 
781
        """Return IPython client widgets list"""
 
782
        return [cl for cl in self.clients if isinstance(cl, IPythonClient)]
 
783
        
 
784
#    def get_kernels(self):
 
785
#        """Return IPython kernel widgets list"""
 
786
#        return [sw for sw in self.shellwidgets
 
787
#                if isinstance(sw, IPythonKernel)]
 
788
#        
 
789
 
 
790
    def get_focus_client(self):
 
791
        """Return current client with focus, if any"""
 
792
        widget = QApplication.focusWidget()
 
793
        for client in self.get_clients():
 
794
            if widget is client or widget is client.get_control():
 
795
                return client
 
796
 
 
797
    def new_client(self, connection_file=None, kernel_widget_id=None):
 
798
        """Create a new IPython client"""
 
799
        cf = connection_file
 
800
        if cf is None:
 
801
            example = _('(for example: `kernel-3764.json`, or simply `3764`)')
 
802
            while True:
 
803
                cf, valid = QInputDialog.getText(self, _('IPython'),
 
804
                              _('Provide an IPython kernel connection file:')+\
 
805
                              '\n'+example,
 
806
                              QLineEdit.Normal)
 
807
                if valid:
 
808
                    cf = str(cf)
 
809
                    match = re.match('(kernel-|^)([a-fA-F0-9-]+)(.json|$)', cf)
 
810
                    kernel_num = match.groups()[1]
 
811
                    if kernel_num:
 
812
                        cf = 'kernel-%s.json' % kernel_num
 
813
                        break
 
814
                else:
 
815
                    return
 
816
 
 
817
        # Generating the client name and setting kernel_widget_id
 
818
        match = re.match('^kernel-([a-fA-F0-9-]+).json', cf)
 
819
        count = 0
 
820
        while True:
 
821
            client_name = match.groups()[0]+'/'+chr(65+count)
 
822
            for cl in self.get_clients():
 
823
                if cl.client_name == client_name:
 
824
                    kernel_widget_id = cl.kernel_widget_id
 
825
                    break
 
826
            else:
 
827
                break
 
828
            count += 1
 
829
        
 
830
        # Trying to get kernel_widget_id from the currently opened kernels if
 
831
        # the previous procedure fails. This could happen when the first
 
832
        # client connected to a kernel is closed but the kernel is left open
 
833
        # and you try to connect new clients to it
 
834
        if kernel_widget_id is None:
 
835
            extconsole = self.main.extconsole
 
836
            for sw in extconsole.shellwidgets:
 
837
                if sw.connection_file == cf:
 
838
                    kernel_widget_id = id(sw)
 
839
 
 
840
        # Creating the IPython client widget
 
841
        try:
 
842
            self.register_client(cf, kernel_widget_id, client_name)
 
843
        except (IOError, UnboundLocalError):
 
844
            QMessageBox.critical(self, _('IPython'),
 
845
                                 _("Unable to connect to IPython kernel "
 
846
                                   "<b>`%s`") % cf)
 
847
            return
 
848
 
 
849
    def ipywidget_config(self):
 
850
        """Generate a Config instance for IPython widgets using our config
 
851
        system
 
852
        
 
853
        This let us create each widget with its own config (as oppossed to
 
854
        IPythonQtConsoleApp, where all widgets have the same config)
 
855
        """
 
856
        # ---- IPython config ----
 
857
        try:
 
858
            profile_path = osp.join(get_ipython_dir(), 'profile_default')
 
859
            full_ip_cfg = load_pyconfig_files(['ipython_qtconsole_config.py'],
 
860
                                              profile_path)
 
861
            
 
862
            # From the full config we only select the IPythonWidget section
 
863
            # because the others have no effect here.
 
864
            ip_cfg = Config({'IPythonWidget': full_ip_cfg.IPythonWidget})
 
865
        except:
 
866
            ip_cfg = Config()
 
867
       
 
868
        # ---- Spyder config ----
 
869
        spy_cfg = Config()
 
870
        
 
871
        # Make the pager widget a rich one (i.e a QTextEdit)
 
872
        spy_cfg.IPythonWidget.kind = 'rich'
 
873
        
 
874
        # Gui completion widget
 
875
        gui_comp_o = self.get_option('use_gui_completion')
 
876
        completions = {True: 'droplist', False: 'ncurses'}
 
877
        spy_cfg.IPythonWidget.gui_completion = completions[gui_comp_o]
 
878
 
 
879
        # Pager
 
880
        pager_o = self.get_option('use_pager')
 
881
        if pager_o:
 
882
            spy_cfg.IPythonWidget.paging = 'inside'
 
883
        else:
 
884
            spy_cfg.IPythonWidget.paging = 'none'
 
885
        
 
886
        # Calltips
 
887
        calltips_o = self.get_option('show_calltips')
 
888
        spy_cfg.IPythonWidget.enable_calltips = calltips_o
 
889
 
 
890
        # Buffer size
 
891
        buffer_size_o = self.get_option('buffer_size')
 
892
        spy_cfg.IPythonWidget.buffer_size = buffer_size_o
 
893
        
 
894
        # Prompts
 
895
        in_prompt_o = self.get_option('in_prompt')
 
896
        out_prompt_o = self.get_option('out_prompt')
 
897
        if in_prompt_o:
 
898
            spy_cfg.IPythonWidget.in_prompt = in_prompt_o
 
899
        if out_prompt_o:
 
900
            spy_cfg.IPythonWidget.out_prompt = out_prompt_o
 
901
        
 
902
        # Merge IPython and Spyder configs. Spyder prefs will have prevalence
 
903
        # over IPython ones
 
904
        ip_cfg._merge(spy_cfg)
 
905
        return ip_cfg
 
906
    
 
907
    def initialize_application(self):
 
908
        """Initialize IPython application"""
 
909
        #======================================================================
 
910
        # For IPython developers review [1]
 
911
        self.ipython_app = IPythonApp()
 
912
        # Is the following line really necessary?
 
913
        #self.ipython_app.initialize_all_except_qt()
 
914
        #======================================================================
 
915
 
 
916
    def register_client(self, connection_file, kernel_widget_id, client_name):
 
917
        """Register new IPython client"""
 
918
        #======================================================================
 
919
        # For IPython developers review [2]
 
920
        ipywidget = self.ipython_app.new_ipywidget(connection_file,
 
921
                                                config=self.ipywidget_config())
 
922
        #======================================================================
 
923
 
 
924
        client = IPythonClient(self, connection_file, kernel_widget_id,
 
925
                               client_name, ipywidget,
 
926
                               history_filename='.history.py',
 
927
                               menu_actions=self.menu_actions)
 
928
        # QTextEdit Widgets
 
929
        control = client.ipywidget._control
 
930
        page_control = client.ipywidget._page_control
 
931
        
 
932
        # For tracebacks
 
933
        self.connect(control, SIGNAL("go_to_error(QString)"), self.go_to_error)
 
934
 
 
935
        # Handle kernel interrupts
 
936
        extconsoles = self.main.extconsole.shellwidgets
 
937
        kernel_widget = None
 
938
        if extconsoles:
 
939
            if extconsoles[-1].connection_file == connection_file:
 
940
                kernel_widget = extconsoles[-1]
 
941
                ipywidget.custom_interrupt_requested.connect(
 
942
                                              kernel_widget.keyboard_interrupt)
 
943
        if kernel_widget is None:
 
944
            ipywidget.custom_interrupt_requested.connect(
 
945
                                                      client.interrupt_message)
 
946
        
 
947
        # Handle kernel restarts asked by the user
 
948
        if kernel_widget is not None:
 
949
            ipywidget.custom_restart_requested.connect(self.create_new_kernel)
 
950
        else:
 
951
            ipywidget.custom_restart_requested.connect(client.restart_message)
 
952
        
 
953
        # Print a message if kernel dies unexpectedly
 
954
        ipywidget.custom_restart_kernel_died.connect(
 
955
                                            lambda t: client.if_kernel_dies(t))
 
956
        
 
957
        # Connect text widget to our inspector
 
958
        if kernel_widget is not None and self.inspector is not None:
 
959
            control.set_inspector(self.inspector)
 
960
        
 
961
        # Connect client to our history log
 
962
        if self.historylog is not None:
 
963
            self.historylog.add_history(client.history_filename)
 
964
            self.connect(client, SIGNAL('append_to_history(QString,QString)'),
 
965
                         self.historylog.append_to_history)
 
966
        
 
967
        # Apply settings to newly created client widget:
 
968
        client.set_font( self.get_plugin_font() )
 
969
        
 
970
        # Add tab and connect focus signals to client's control widgets
 
971
        self.add_tab(client, name=client.get_name())
 
972
        self.connect(control, SIGNAL('focus_changed()'),
 
973
                     lambda: self.emit(SIGNAL('focus_changed()')))
 
974
        self.connect(page_control, SIGNAL('focus_changed()'),
 
975
                     lambda: self.emit(SIGNAL('focus_changed()')))
 
976
        
 
977
        # Update the find widget if focus changes between control and
 
978
        # page_control
 
979
        self.find_widget.set_editor(control)
 
980
        if page_control:
 
981
            self.connect(control, SIGNAL('visibility_changed(bool)'),
 
982
                         self.refresh_plugin)
 
983
            self.connect(page_control, SIGNAL('visibility_changed(bool)'),
 
984
                         self.refresh_plugin)
 
985
            self.connect(page_control, SIGNAL('show_find_widget()'),
 
986
                         self.find_widget.show)
 
987
    
 
988
    def close_related_ipyclients(self, client):
 
989
        """Close all IPython clients related to *client*, except itself"""
 
990
        for cl in self.clients[:]:
 
991
            if cl is not client and \
 
992
              cl.connection_file == client.connection_file:
 
993
                self.close_console(client=cl)
 
994
    
 
995
    def get_ipywidget_by_kernelwidget_id(self, kernel_id):
 
996
        """Return the IPython widget associated to a kernel widget id"""
 
997
        for cl in self.clients:
 
998
            if cl.kernel_widget_id == kernel_id:
 
999
                return cl.ipywidget
 
1000
        else:
 
1001
            raise ValueError, "Unknown kernel widget ID %r" % kernel_id
 
1002
        
 
1003
    def add_tab(self, widget, name):
 
1004
        """Add tab"""
 
1005
        self.clients.append(widget)
 
1006
        index = self.tabwidget.addTab(widget, get_icon('ipython_console.png'),
 
1007
                                      name)
 
1008
        self.tabwidget.setCurrentIndex(index)
 
1009
        if self.dockwidget and not self.ismaximized:
 
1010
            self.dockwidget.setVisible(True)
 
1011
            self.dockwidget.raise_()
 
1012
        self.activateWindow()
 
1013
        widget.get_control().setFocus()
 
1014
        
 
1015
    def move_tab(self, index_from, index_to):
 
1016
        """
 
1017
        Move tab (tabs themselves have already been moved by the tabwidget)
 
1018
        """
 
1019
        client = self.clients.pop(index_from)
 
1020
        self.clients.insert(index_to, client)
 
1021
        self.emit(SIGNAL('update_plugin_title()'))
 
1022
        
 
1023
    def close_console(self, index=None, client=None, force=False):
 
1024
        """Close console tab from index or widget (or close current tab)"""
 
1025
        if not self.tabwidget.count():
 
1026
            return
 
1027
        if client is not None:
 
1028
            index = self.tabwidget.indexOf(client)
 
1029
        if index is None and client is None:
 
1030
            index = self.tabwidget.currentIndex()
 
1031
        if index is not None:
 
1032
            client = self.tabwidget.widget(index)
 
1033
 
 
1034
        # Check if related clients or kernels are opened
 
1035
        # and eventually ask before closing them
 
1036
        if not force and isinstance(client, IPythonClient):
 
1037
            extconsole = self.main.extconsole
 
1038
            idx = extconsole.get_shell_index_from_id(client.kernel_widget_id)
 
1039
            if idx is not None:
 
1040
                close_all = True
 
1041
                if self.get_option('ask_before_closing'):
 
1042
                    ans = QMessageBox.question(self, self.get_plugin_title(),
 
1043
                           _("%s will be closed.\n"
 
1044
                             "Do you want to kill the associated kernel "
 
1045
                             "and all of its clients?") % client.get_name(),
 
1046
                           QMessageBox.Yes|QMessageBox.No|QMessageBox.Cancel)
 
1047
                    if ans == QMessageBox.Cancel:
 
1048
                        return
 
1049
                    close_all = ans == QMessageBox.Yes
 
1050
                if close_all:
 
1051
                    extconsole.close_console(index=idx, from_ipyclient=True)
 
1052
                    self.close_related_ipyclients(client)
 
1053
        client.close()
 
1054
        
 
1055
        # Note: client index may have changed after closing related widgets
 
1056
        self.tabwidget.removeTab(self.tabwidget.indexOf(client))
 
1057
        self.clients.remove(client)
 
1058
 
 
1059
        self.emit(SIGNAL('update_plugin_title()'))
 
1060
        
 
1061
    def go_to_error(self, text):
 
1062
        """Go to error if relevant"""
 
1063
        match = get_error_match(unicode(text))
 
1064
        if match:
 
1065
            fname, lnb = match.groups()
 
1066
            self.emit(SIGNAL("edit_goto(QString,int,QString)"),
 
1067
                      osp.abspath(fname), int(lnb), '')
 
1068
    
 
1069
    def get_client_index_from_id(self, client_id):
 
1070
        """Return client index from id"""
 
1071
        for index, client in enumerate(self.clients):
 
1072
            if id(client) == client_id:
 
1073
                return index
 
1074
    
 
1075
    def rename_ipyclient_tab(self, connection_file, client_widget_id):
 
1076
        """Add the pid of the kernel process to an IPython client tab"""
 
1077
        index = self.get_client_index_from_id(client_widget_id)
 
1078
        match = re.match('^kernel-(\d+).json', connection_file)
 
1079
        if match is not None:  # should not fail, but we never know...
 
1080
            name = _("Console") + " " + match.groups()[0] + '/' + chr(65)
 
1081
            self.tabwidget.setTabText(index, name)
 
1082
    
 
1083
    def create_new_kernel(self):
 
1084
        """Create a new kernel if the user asks for it"""
 
1085
        # Took this bit of code (until if result == ) from the IPython project
 
1086
        # (frontend/qt/frontend_widget.py - restart_kernel).
 
1087
        # Licensed under the BSD license
 
1088
        message = _('Are you sure you want to restart the kernel?')
 
1089
        buttons = QMessageBox.Yes | QMessageBox.No
 
1090
        result = QMessageBox.question(self, _('Restart kernel?'),
 
1091
                                      message, buttons)
 
1092
        if result == QMessageBox.Yes:
 
1093
            extconsole = self.main.extconsole
 
1094
            extconsole.start_ipykernel(create_client=False)
 
1095
            kernel_widget = extconsole.shellwidgets[-1]
 
1096
            self.connect(kernel_widget,
 
1097
                      SIGNAL('create_ipython_client(QString)'),
 
1098
                      lambda cf: self.connect_to_new_kernel(cf, kernel_widget))
 
1099
    
 
1100
    def connect_to_new_kernel(self, connection_file, kernel_widget):
 
1101
        """
 
1102
        After a new kernel is created, execute this action to connect the new
 
1103
        kernel to the old client
 
1104
        """
 
1105
        extconsole = self.main.extconsole
 
1106
        client = self.tabwidget.currentWidget()
 
1107
        
 
1108
        # Close old kernel tab
 
1109
        idx = extconsole.get_shell_index_from_id(client.kernel_widget_id)
 
1110
        extconsole.close_console(index=idx, from_ipyclient=True)
 
1111
        
 
1112
        # Set attributes for the new kernel
 
1113
        extconsole.set_ipykernel_attrs(connection_file, kernel_widget)
 
1114
        
 
1115
        # Connect client to new kernel
 
1116
        kernel_manager = self.ipython_app.create_kernel_manager(connection_file)        
 
1117
        client.ipywidget.kernel_manager = kernel_manager
 
1118
        client.kernel_widget_id = id(kernel_widget)
 
1119
        client.get_control().setFocus()
 
1120
        
 
1121
        # Rename client tab
 
1122
        client_widget_id = id(client)
 
1123
        self.rename_ipyclient_tab(connection_file, client_widget_id)
 
1124
            
 
1125
    #----Drag and drop
 
1126
    #TODO: try and reimplement this block
 
1127
    # (this is still the original code block copied from externalconsole.py)
 
1128
#    def dragEnterEvent(self, event):
 
1129
#        """Reimplement Qt method
 
1130
#        Inform Qt about the types of data that the widget accepts"""
 
1131
#        source = event.mimeData()
 
1132
#        if source.hasUrls():
 
1133
#            if mimedata2url(source):
 
1134
#                pathlist = mimedata2url(source)
 
1135
#                shellwidget = self.tabwidget.currentWidget()
 
1136
#                if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
 
1137
#                    event.acceptProposedAction()
 
1138
#                elif shellwidget is None or not shellwidget.is_running():
 
1139
#                    event.ignore()
 
1140
#                else:
 
1141
#                    event.acceptProposedAction()
 
1142
#            else:
 
1143
#                event.ignore()
 
1144
#        elif source.hasText():
 
1145
#            event.acceptProposedAction()            
 
1146
#            
 
1147
#    def dropEvent(self, event):
 
1148
#        """Reimplement Qt method
 
1149
#        Unpack dropped data and handle it"""
 
1150
#        source = event.mimeData()
 
1151
#        shellwidget = self.tabwidget.currentWidget()
 
1152
#        if source.hasText():
 
1153
#            qstr = source.text()
 
1154
#            if is_python_script(unicode(qstr)):
 
1155
#                self.start(qstr)
 
1156
#            elif shellwidget:
 
1157
#                shellwidget.shell.insert_text(qstr)
 
1158
#        elif source.hasUrls():
 
1159
#            pathlist = mimedata2url(source)
 
1160
#            if all([is_python_script(unicode(qstr)) for qstr in pathlist]):
 
1161
#                for fname in pathlist:
 
1162
#                    self.start(fname)
 
1163
#            elif shellwidget:
 
1164
#                shellwidget.shell.drop_pathlist(pathlist)
 
1165
#        event.acceptProposedAction()
 
1166