##// END OF EJS Templates
Merge pull request #1065 from Carreau/qtconsole-racecondition...
Fernando Perez -
r5527:a5fd0a3d merge
parent child Browse files
Show More
@@ -1,728 +1,730 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7 import uuid
8 8
9 9 # System library imports
10 10 from pygments.lexers import PythonLexer
11 11 from IPython.external import qt
12 12 from IPython.external.qt import QtCore, QtGui
13 13
14 14 # Local imports
15 15 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
16 16 from IPython.core.oinspect import call_tip
17 17 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 18 from IPython.utils.traitlets import Bool, Instance, Unicode
19 19 from bracket_matcher import BracketMatcher
20 20 from call_tip_widget import CallTipWidget
21 21 from completion_lexer import CompletionLexer
22 22 from history_console_widget import HistoryConsoleWidget
23 23 from pygments_highlighter import PygmentsHighlighter
24 24
25 25
26 26 class FrontendHighlighter(PygmentsHighlighter):
27 27 """ A PygmentsHighlighter that understands and ignores prompts.
28 28 """
29 29
30 30 def __init__(self, frontend):
31 31 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 32 self._current_offset = 0
33 33 self._frontend = frontend
34 34 self.highlighting_on = False
35 35
36 36 def highlightBlock(self, string):
37 37 """ Highlight a block of text. Reimplemented to highlight selectively.
38 38 """
39 39 if not self.highlighting_on:
40 40 return
41 41
42 42 # The input to this function is a unicode string that may contain
43 43 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 44 # the string as plain text so we can compare it.
45 45 current_block = self.currentBlock()
46 46 string = self._frontend._get_block_plain_text(current_block)
47 47
48 48 # Decide whether to check for the regular or continuation prompt.
49 49 if current_block.contains(self._frontend._prompt_pos):
50 50 prompt = self._frontend._prompt
51 51 else:
52 52 prompt = self._frontend._continuation_prompt
53 53
54 54 # Only highlight if we can identify a prompt, but make sure not to
55 55 # highlight the prompt.
56 56 if string.startswith(prompt):
57 57 self._current_offset = len(prompt)
58 58 string = string[len(prompt):]
59 59 super(FrontendHighlighter, self).highlightBlock(string)
60 60
61 61 def rehighlightBlock(self, block):
62 62 """ Reimplemented to temporarily enable highlighting if disabled.
63 63 """
64 64 old = self.highlighting_on
65 65 self.highlighting_on = True
66 66 super(FrontendHighlighter, self).rehighlightBlock(block)
67 67 self.highlighting_on = old
68 68
69 69 def setFormat(self, start, count, format):
70 70 """ Reimplemented to highlight selectively.
71 71 """
72 72 start += self._current_offset
73 73 super(FrontendHighlighter, self).setFormat(start, count, format)
74 74
75 75
76 76 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 77 """ A Qt frontend for a generic Python kernel.
78 78 """
79 79
80 80 # The text to show when the kernel is (re)started.
81 81 banner = Unicode()
82 82
83 83 # An option and corresponding signal for overriding the default kernel
84 84 # interrupt behavior.
85 85 custom_interrupt = Bool(False)
86 86 custom_interrupt_requested = QtCore.Signal()
87 87
88 88 # An option and corresponding signals for overriding the default kernel
89 89 # restart behavior.
90 90 custom_restart = Bool(False)
91 91 custom_restart_kernel_died = QtCore.Signal(float)
92 92 custom_restart_requested = QtCore.Signal()
93 93
94 94 # Whether to automatically show calltips on open-parentheses.
95 95 enable_calltips = Bool(True, config=True,
96 96 help="Whether to draw information calltips on open-parentheses.")
97 97
98 98 # Emitted when a user visible 'execute_request' has been submitted to the
99 99 # kernel from the FrontendWidget. Contains the code to be executed.
100 100 executing = QtCore.Signal(object)
101 101
102 102 # Emitted when a user-visible 'execute_reply' has been received from the
103 103 # kernel and processed by the FrontendWidget. Contains the response message.
104 104 executed = QtCore.Signal(object)
105 105
106 106 # Emitted when an exit request has been received from the kernel.
107 107 exit_requested = QtCore.Signal(object)
108 108
109 109 # Protected class variables.
110 110 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 111 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 112 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 113 _input_splitter_class = InputSplitter
114 114 _local_kernel = False
115 115 _highlighter = Instance(FrontendHighlighter)
116 116
117 117 #---------------------------------------------------------------------------
118 118 # 'object' interface
119 119 #---------------------------------------------------------------------------
120 120
121 121 def __init__(self, *args, **kw):
122 122 super(FrontendWidget, self).__init__(*args, **kw)
123 123 # FIXME: remove this when PySide min version is updated past 1.0.7
124 124 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 125 if qt.QT_API == qt.QT_API_PYSIDE:
126 126 import PySide
127 127 if PySide.__version_info__ < (1,0,7):
128 128 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 129 self.enable_calltips = False
130 130
131 131 # FrontendWidget protected variables.
132 132 self._bracket_matcher = BracketMatcher(self._control)
133 133 self._call_tip_widget = CallTipWidget(self._control)
134 134 self._completion_lexer = CompletionLexer(PythonLexer())
135 135 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 136 self._hidden = False
137 137 self._highlighter = FrontendHighlighter(self)
138 138 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 139 self._kernel_manager = None
140 140 self._request_info = {}
141 self._request_info['execute'] = {};
141 142 self._callback_dict = {}
142 143
143 144 # Configure the ConsoleWidget.
144 145 self.tab_width = 4
145 146 self._set_continuation_prompt('... ')
146 147
147 148 # Configure the CallTipWidget.
148 149 self._call_tip_widget.setFont(self.font)
149 150 self.font_changed.connect(self._call_tip_widget.setFont)
150 151
151 152 # Configure actions.
152 153 action = self._copy_raw_action
153 154 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
154 155 action.setEnabled(False)
155 156 action.setShortcut(QtGui.QKeySequence(key))
156 157 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
157 158 action.triggered.connect(self.copy_raw)
158 159 self.copy_available.connect(action.setEnabled)
159 160 self.addAction(action)
160 161
161 162 # Connect signal handlers.
162 163 document = self._control.document()
163 164 document.contentsChange.connect(self._document_contents_change)
164 165
165 166 # Set flag for whether we are connected via localhost.
166 167 self._local_kernel = kw.get('local_kernel',
167 168 FrontendWidget._local_kernel)
168 169
169 170 #---------------------------------------------------------------------------
170 171 # 'ConsoleWidget' public interface
171 172 #---------------------------------------------------------------------------
172 173
173 174 def copy(self):
174 175 """ Copy the currently selected text to the clipboard, removing prompts.
175 176 """
176 177 text = self._control.textCursor().selection().toPlainText()
177 178 if text:
178 179 lines = map(transform_classic_prompt, text.splitlines())
179 180 text = '\n'.join(lines)
180 181 QtGui.QApplication.clipboard().setText(text)
181 182
182 183 #---------------------------------------------------------------------------
183 184 # 'ConsoleWidget' abstract interface
184 185 #---------------------------------------------------------------------------
185 186
186 187 def _is_complete(self, source, interactive):
187 188 """ Returns whether 'source' can be completely processed and a new
188 189 prompt created. When triggered by an Enter/Return key press,
189 190 'interactive' is True; otherwise, it is False.
190 191 """
191 192 complete = self._input_splitter.push(source)
192 193 if interactive:
193 194 complete = not self._input_splitter.push_accepts_more()
194 195 return complete
195 196
196 197 def _execute(self, source, hidden):
197 198 """ Execute 'source'. If 'hidden', do not show any output.
198 199
199 200 See parent class :meth:`execute` docstring for full details.
200 201 """
201 202 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
202 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
203 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
203 204 self._hidden = hidden
204 205 if not hidden:
205 206 self.executing.emit(source)
206 207
207 208 def _prompt_started_hook(self):
208 209 """ Called immediately after a new prompt is displayed.
209 210 """
210 211 if not self._reading:
211 212 self._highlighter.highlighting_on = True
212 213
213 214 def _prompt_finished_hook(self):
214 215 """ Called immediately after a prompt is finished, i.e. when some input
215 216 will be processed and a new prompt displayed.
216 217 """
217 218 # Flush all state from the input splitter so the next round of
218 219 # reading input starts with a clean buffer.
219 220 self._input_splitter.reset()
220 221
221 222 if not self._reading:
222 223 self._highlighter.highlighting_on = False
223 224
224 225 def _tab_pressed(self):
225 226 """ Called when the tab key is pressed. Returns whether to continue
226 227 processing the event.
227 228 """
228 229 # Perform tab completion if:
229 230 # 1) The cursor is in the input buffer.
230 231 # 2) There is a non-whitespace character before the cursor.
231 232 text = self._get_input_buffer_cursor_line()
232 233 if text is None:
233 234 return False
234 235 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
235 236 if complete:
236 237 self._complete()
237 238 return not complete
238 239
239 240 #---------------------------------------------------------------------------
240 241 # 'ConsoleWidget' protected interface
241 242 #---------------------------------------------------------------------------
242 243
243 244 def _context_menu_make(self, pos):
244 245 """ Reimplemented to add an action for raw copy.
245 246 """
246 247 menu = super(FrontendWidget, self)._context_menu_make(pos)
247 248 for before_action in menu.actions():
248 249 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
249 250 QtGui.QKeySequence.ExactMatch:
250 251 menu.insertAction(before_action, self._copy_raw_action)
251 252 break
252 253 return menu
253 254
254 255 def request_interrupt_kernel(self):
255 256 if self._executing:
256 257 self.interrupt_kernel()
257 258
258 259 def request_restart_kernel(self):
259 260 message = 'Are you sure you want to restart the kernel?'
260 261 self.restart_kernel(message, now=False)
261 262
262 263 def _event_filter_console_keypress(self, event):
263 264 """ Reimplemented for execution interruption and smart backspace.
264 265 """
265 266 key = event.key()
266 267 if self._control_key_down(event.modifiers(), include_command=False):
267 268
268 269 if key == QtCore.Qt.Key_C and self._executing:
269 270 self.request_interrupt_kernel()
270 271 return True
271 272
272 273 elif key == QtCore.Qt.Key_Period:
273 274 self.request_restart_kernel()
274 275 return True
275 276
276 277 elif not event.modifiers() & QtCore.Qt.AltModifier:
277 278
278 279 # Smart backspace: remove four characters in one backspace if:
279 280 # 1) everything left of the cursor is whitespace
280 281 # 2) the four characters immediately left of the cursor are spaces
281 282 if key == QtCore.Qt.Key_Backspace:
282 283 col = self._get_input_buffer_cursor_column()
283 284 cursor = self._control.textCursor()
284 285 if col > 3 and not cursor.hasSelection():
285 286 text = self._get_input_buffer_cursor_line()[:col]
286 287 if text.endswith(' ') and not text.strip():
287 288 cursor.movePosition(QtGui.QTextCursor.Left,
288 289 QtGui.QTextCursor.KeepAnchor, 4)
289 290 cursor.removeSelectedText()
290 291 return True
291 292
292 293 return super(FrontendWidget, self)._event_filter_console_keypress(event)
293 294
294 295 def _insert_continuation_prompt(self, cursor):
295 296 """ Reimplemented for auto-indentation.
296 297 """
297 298 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
298 299 cursor.insertText(' ' * self._input_splitter.indent_spaces)
299 300
300 301 #---------------------------------------------------------------------------
301 302 # 'BaseFrontendMixin' abstract interface
302 303 #---------------------------------------------------------------------------
303 304
304 305 def _handle_complete_reply(self, rep):
305 306 """ Handle replies for tab completion.
306 307 """
307 308 self.log.debug("complete: %s", rep.get('content', ''))
308 309 cursor = self._get_cursor()
309 310 info = self._request_info.get('complete')
310 311 if info and info.id == rep['parent_header']['msg_id'] and \
311 312 info.pos == cursor.position():
312 313 text = '.'.join(self._get_context())
313 314 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
314 315 self._complete_with_items(cursor, rep['content']['matches'])
315 316
316 317 def _silent_exec_callback(self, expr, callback):
317 318 """Silently execute `expr` in the kernel and call `callback` with reply
318 319
319 320 the `expr` is evaluated silently in the kernel (without) output in
320 321 the frontend. Call `callback` with the
321 322 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
322 323
323 324 Parameters
324 325 ----------
325 326 expr : string
326 327 valid string to be executed by the kernel.
327 328 callback : function
328 329 function accepting one arguement, as a string. The string will be
329 330 the `repr` of the result of evaluating `expr`
330 331
331 332 The `callback` is called with the 'repr()' of the result of `expr` as
332 333 first argument. To get the object, do 'eval()' onthe passed value.
333 334
334 335 See Also
335 336 --------
336 337 _handle_exec_callback : private method, deal with calling callback with reply
337 338
338 339 """
339 340
340 341 # generate uuid, which would be used as a indication of wether or not
341 342 # the unique request originate from here (can use msg id ?)
342 343 local_uuid = str(uuid.uuid1())
343 344 msg_id = self.kernel_manager.shell_channel.execute('',
344 345 silent=True, user_expressions={ local_uuid:expr })
345 346 self._callback_dict[local_uuid] = callback
346 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
347 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
347 348
348 349 def _handle_exec_callback(self, msg):
349 350 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
350 351
351 352 Parameters
352 353 ----------
353 354 msg : raw message send by the kernel containing an `user_expressions`
354 355 and having a 'silent_exec_callback' kind.
355 356
356 357 Notes
357 358 -----
358 359 This fonction will look for a `callback` associated with the
359 360 corresponding message id. Association has been made by
360 361 `_silent_exec_callback`. `callback` is then called with the `repr()`
361 362 of the value of corresponding `user_expressions` as argument.
362 363 `callback` is then removed from the known list so that any message
363 364 coming again with the same id won't trigger it.
364 365
365 366 """
366 367
367 368 user_exp = msg['content']['user_expressions']
368 369 for expression in user_exp:
369 370 if expression in self._callback_dict:
370 371 self._callback_dict.pop(expression)(user_exp[expression])
371 372
372 373 def _handle_execute_reply(self, msg):
373 374 """ Handles replies for code execution.
374 375 """
375 376 self.log.debug("execute: %s", msg.get('content', ''))
376 info = self._request_info.get('execute')
377 msg_id = msg['parent_header']['msg_id']
378 info = self._request_info['execute'].get(msg_id)
377 379 # unset reading flag, because if execute finished, raw_input can't
378 380 # still be pending.
379 381 self._reading = False
380 if info and info.id == msg['parent_header']['msg_id'] and \
381 info.kind == 'user' and not self._hidden:
382 if info and info.kind == 'user' and not self._hidden:
382 383 # Make sure that all output from the SUB channel has been processed
383 384 # before writing a new prompt.
384 385 self.kernel_manager.sub_channel.flush()
385 386
386 387 # Reset the ANSI style information to prevent bad text in stdout
387 388 # from messing up our colors. We're not a true terminal so we're
388 389 # allowed to do this.
389 390 if self.ansi_codes:
390 391 self._ansi_processor.reset_sgr()
391 392
392 393 content = msg['content']
393 394 status = content['status']
394 395 if status == 'ok':
395 396 self._process_execute_ok(msg)
396 397 elif status == 'error':
397 398 self._process_execute_error(msg)
398 399 elif status == 'aborted':
399 400 self._process_execute_abort(msg)
400 401
401 402 self._show_interpreter_prompt_for_reply(msg)
402 403 self.executed.emit(msg)
403 elif info and info.id == msg['parent_header']['msg_id'] and \
404 info.kind == 'silent_exec_callback' and not self._hidden:
404 self._request_info['execute'].pop(msg_id)
405 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
405 406 self._handle_exec_callback(msg)
407 self._request_info['execute'].pop(msg_id)
406 408 else:
407 409 super(FrontendWidget, self)._handle_execute_reply(msg)
408 410
409 411 def _handle_input_request(self, msg):
410 412 """ Handle requests for raw_input.
411 413 """
412 414 self.log.debug("input: %s", msg.get('content', ''))
413 415 if self._hidden:
414 416 raise RuntimeError('Request for raw input during hidden execution.')
415 417
416 418 # Make sure that all output from the SUB channel has been processed
417 419 # before entering readline mode.
418 420 self.kernel_manager.sub_channel.flush()
419 421
420 422 def callback(line):
421 423 self.kernel_manager.stdin_channel.input(line)
422 424 if self._reading:
423 425 self.log.debug("Got second input request, assuming first was interrupted.")
424 426 self._reading = False
425 427 self._readline(msg['content']['prompt'], callback=callback)
426 428
427 429 def _handle_kernel_died(self, since_last_heartbeat):
428 430 """ Handle the kernel's death by asking if the user wants to restart.
429 431 """
430 432 self.log.debug("kernel died: %s", since_last_heartbeat)
431 433 if self.custom_restart:
432 434 self.custom_restart_kernel_died.emit(since_last_heartbeat)
433 435 else:
434 436 message = 'The kernel heartbeat has been inactive for %.2f ' \
435 437 'seconds. Do you want to restart the kernel? You may ' \
436 438 'first want to check the network connection.' % \
437 439 since_last_heartbeat
438 440 self.restart_kernel(message, now=True)
439 441
440 442 def _handle_object_info_reply(self, rep):
441 443 """ Handle replies for call tips.
442 444 """
443 445 self.log.debug("oinfo: %s", rep.get('content', ''))
444 446 cursor = self._get_cursor()
445 447 info = self._request_info.get('call_tip')
446 448 if info and info.id == rep['parent_header']['msg_id'] and \
447 449 info.pos == cursor.position():
448 450 # Get the information for a call tip. For now we format the call
449 451 # line as string, later we can pass False to format_call and
450 452 # syntax-highlight it ourselves for nicer formatting in the
451 453 # calltip.
452 454 content = rep['content']
453 455 # if this is from pykernel, 'docstring' will be the only key
454 456 if content.get('ismagic', False):
455 457 # Don't generate a call-tip for magics. Ideally, we should
456 458 # generate a tooltip, but not on ( like we do for actual
457 459 # callables.
458 460 call_info, doc = None, None
459 461 else:
460 462 call_info, doc = call_tip(content, format_call=True)
461 463 if call_info or doc:
462 464 self._call_tip_widget.show_call_info(call_info, doc)
463 465
464 466 def _handle_pyout(self, msg):
465 467 """ Handle display hook output.
466 468 """
467 469 self.log.debug("pyout: %s", msg.get('content', ''))
468 470 if not self._hidden and self._is_from_this_session(msg):
469 471 text = msg['content']['data']
470 472 self._append_plain_text(text + '\n', before_prompt=True)
471 473
472 474 def _handle_stream(self, msg):
473 475 """ Handle stdout, stderr, and stdin.
474 476 """
475 477 self.log.debug("stream: %s", msg.get('content', ''))
476 478 if not self._hidden and self._is_from_this_session(msg):
477 479 # Most consoles treat tabs as being 8 space characters. Convert tabs
478 480 # to spaces so that output looks as expected regardless of this
479 481 # widget's tab width.
480 482 text = msg['content']['data'].expandtabs(8)
481 483
482 484 self._append_plain_text(text, before_prompt=True)
483 485 self._control.moveCursor(QtGui.QTextCursor.End)
484 486
485 487 def _handle_shutdown_reply(self, msg):
486 488 """ Handle shutdown signal, only if from other console.
487 489 """
488 490 self.log.debug("shutdown: %s", msg.get('content', ''))
489 491 if not self._hidden and not self._is_from_this_session(msg):
490 492 if self._local_kernel:
491 493 if not msg['content']['restart']:
492 494 self.exit_requested.emit(self)
493 495 else:
494 496 # we just got notified of a restart!
495 497 time.sleep(0.25) # wait 1/4 sec to reset
496 498 # lest the request for a new prompt
497 499 # goes to the old kernel
498 500 self.reset()
499 501 else: # remote kernel, prompt on Kernel shutdown/reset
500 502 title = self.window().windowTitle()
501 503 if not msg['content']['restart']:
502 504 reply = QtGui.QMessageBox.question(self, title,
503 505 "Kernel has been shutdown permanently. "
504 506 "Close the Console?",
505 507 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
506 508 if reply == QtGui.QMessageBox.Yes:
507 509 self.exit_requested.emit(self)
508 510 else:
509 511 reply = QtGui.QMessageBox.question(self, title,
510 512 "Kernel has been reset. Clear the Console?",
511 513 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
512 514 if reply == QtGui.QMessageBox.Yes:
513 515 time.sleep(0.25) # wait 1/4 sec to reset
514 516 # lest the request for a new prompt
515 517 # goes to the old kernel
516 518 self.reset()
517 519
518 520 def _started_channels(self):
519 521 """ Called when the KernelManager channels have started listening or
520 522 when the frontend is assigned an already listening KernelManager.
521 523 """
522 524 self.reset()
523 525
524 526 #---------------------------------------------------------------------------
525 527 # 'FrontendWidget' public interface
526 528 #---------------------------------------------------------------------------
527 529
528 530 def copy_raw(self):
529 531 """ Copy the currently selected text to the clipboard without attempting
530 532 to remove prompts or otherwise alter the text.
531 533 """
532 534 self._control.copy()
533 535
534 536 def execute_file(self, path, hidden=False):
535 537 """ Attempts to execute file with 'path'. If 'hidden', no output is
536 538 shown.
537 539 """
538 540 self.execute('execfile(%r)' % path, hidden=hidden)
539 541
540 542 def interrupt_kernel(self):
541 543 """ Attempts to interrupt the running kernel.
542 544
543 545 Also unsets _reading flag, to avoid runtime errors
544 546 if raw_input is called again.
545 547 """
546 548 if self.custom_interrupt:
547 549 self._reading = False
548 550 self.custom_interrupt_requested.emit()
549 551 elif self.kernel_manager.has_kernel:
550 552 self._reading = False
551 553 self.kernel_manager.interrupt_kernel()
552 554 else:
553 555 self._append_plain_text('Kernel process is either remote or '
554 556 'unspecified. Cannot interrupt.\n')
555 557
556 558 def reset(self):
557 559 """ Resets the widget to its initial state. Similar to ``clear``, but
558 560 also re-writes the banner and aborts execution if necessary.
559 561 """
560 562 if self._executing:
561 563 self._executing = False
562 self._request_info['execute'] = None
564 self._request_info['execute'] = {}
563 565 self._reading = False
564 566 self._highlighter.highlighting_on = False
565 567
566 568 self._control.clear()
567 569 self._append_plain_text(self.banner)
568 570 # update output marker for stdout/stderr, so that startup
569 571 # messages appear after banner:
570 572 self._append_before_prompt_pos = self._get_cursor().position()
571 573 self._show_interpreter_prompt()
572 574
573 575 def restart_kernel(self, message, now=False):
574 576 """ Attempts to restart the running kernel.
575 577 """
576 578 # FIXME: now should be configurable via a checkbox in the dialog. Right
577 579 # now at least the heartbeat path sets it to True and the manual restart
578 580 # to False. But those should just be the pre-selected states of a
579 581 # checkbox that the user could override if so desired. But I don't know
580 582 # enough Qt to go implementing the checkbox now.
581 583
582 584 if self.custom_restart:
583 585 self.custom_restart_requested.emit()
584 586
585 587 elif self.kernel_manager.has_kernel:
586 588 # Pause the heart beat channel to prevent further warnings.
587 589 self.kernel_manager.hb_channel.pause()
588 590
589 591 # Prompt the user to restart the kernel. Un-pause the heartbeat if
590 592 # they decline. (If they accept, the heartbeat will be un-paused
591 593 # automatically when the kernel is restarted.)
592 594 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
593 595 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
594 596 message, buttons)
595 597 if result == QtGui.QMessageBox.Yes:
596 598 try:
597 599 self.kernel_manager.restart_kernel(now=now)
598 600 except RuntimeError:
599 601 self._append_plain_text('Kernel started externally. '
600 602 'Cannot restart.\n')
601 603 else:
602 604 self.reset()
603 605 else:
604 606 self.kernel_manager.hb_channel.unpause()
605 607
606 608 else:
607 609 self._append_plain_text('Kernel process is either remote or '
608 610 'unspecified. Cannot restart.\n')
609 611
610 612 #---------------------------------------------------------------------------
611 613 # 'FrontendWidget' protected interface
612 614 #---------------------------------------------------------------------------
613 615
614 616 def _call_tip(self):
615 617 """ Shows a call tip, if appropriate, at the current cursor location.
616 618 """
617 619 # Decide if it makes sense to show a call tip
618 620 if not self.enable_calltips:
619 621 return False
620 622 cursor = self._get_cursor()
621 623 cursor.movePosition(QtGui.QTextCursor.Left)
622 624 if cursor.document().characterAt(cursor.position()) != '(':
623 625 return False
624 626 context = self._get_context(cursor)
625 627 if not context:
626 628 return False
627 629
628 630 # Send the metadata request to the kernel
629 631 name = '.'.join(context)
630 632 msg_id = self.kernel_manager.shell_channel.object_info(name)
631 633 pos = self._get_cursor().position()
632 634 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
633 635 return True
634 636
635 637 def _complete(self):
636 638 """ Performs completion at the current cursor location.
637 639 """
638 640 context = self._get_context()
639 641 if context:
640 642 # Send the completion request to the kernel
641 643 msg_id = self.kernel_manager.shell_channel.complete(
642 644 '.'.join(context), # text
643 645 self._get_input_buffer_cursor_line(), # line
644 646 self._get_input_buffer_cursor_column(), # cursor_pos
645 647 self.input_buffer) # block
646 648 pos = self._get_cursor().position()
647 649 info = self._CompletionRequest(msg_id, pos)
648 650 self._request_info['complete'] = info
649 651
650 652 def _get_context(self, cursor=None):
651 653 """ Gets the context for the specified cursor (or the current cursor
652 654 if none is specified).
653 655 """
654 656 if cursor is None:
655 657 cursor = self._get_cursor()
656 658 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
657 659 QtGui.QTextCursor.KeepAnchor)
658 660 text = cursor.selection().toPlainText()
659 661 return self._completion_lexer.get_context(text)
660 662
661 663 def _process_execute_abort(self, msg):
662 664 """ Process a reply for an aborted execution request.
663 665 """
664 666 self._append_plain_text("ERROR: execution aborted\n")
665 667
666 668 def _process_execute_error(self, msg):
667 669 """ Process a reply for an execution request that resulted in an error.
668 670 """
669 671 content = msg['content']
670 672 # If a SystemExit is passed along, this means exit() was called - also
671 673 # all the ipython %exit magic syntax of '-k' to be used to keep
672 674 # the kernel running
673 675 if content['ename']=='SystemExit':
674 676 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
675 677 self._keep_kernel_on_exit = keepkernel
676 678 self.exit_requested.emit(self)
677 679 else:
678 680 traceback = ''.join(content['traceback'])
679 681 self._append_plain_text(traceback)
680 682
681 683 def _process_execute_ok(self, msg):
682 684 """ Process a reply for a successful execution equest.
683 685 """
684 686 payload = msg['content']['payload']
685 687 for item in payload:
686 688 if not self._process_execute_payload(item):
687 689 warning = 'Warning: received unknown payload of type %s'
688 690 print(warning % repr(item['source']))
689 691
690 692 def _process_execute_payload(self, item):
691 693 """ Process a single payload item from the list of payload items in an
692 694 execution reply. Returns whether the payload was handled.
693 695 """
694 696 # The basic FrontendWidget doesn't handle payloads, as they are a
695 697 # mechanism for going beyond the standard Python interpreter model.
696 698 return False
697 699
698 700 def _show_interpreter_prompt(self):
699 701 """ Shows a prompt for the interpreter.
700 702 """
701 703 self._show_prompt('>>> ')
702 704
703 705 def _show_interpreter_prompt_for_reply(self, msg):
704 706 """ Shows a prompt for the interpreter given an 'execute_reply' message.
705 707 """
706 708 self._show_interpreter_prompt()
707 709
708 710 #------ Signal handlers ----------------------------------------------------
709 711
710 712 def _document_contents_change(self, position, removed, added):
711 713 """ Called whenever the document's content changes. Display a call tip
712 714 if appropriate.
713 715 """
714 716 # Calculate where the cursor should be *after* the change:
715 717 position += added
716 718
717 719 document = self._control.document()
718 720 if position == self._get_cursor().position():
719 721 self._call_tip()
720 722
721 723 #------ Trait default initializers -----------------------------------------
722 724
723 725 def _banner_default(self):
724 726 """ Returns the standard Python banner.
725 727 """
726 728 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
727 729 '"license" for more information.'
728 730 return banner % (sys.version, sys.platform)
@@ -1,283 +1,283 b''
1 1 # System library imports
2 2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from IPython.utils.traitlets import Bool
6 6 from console_widget import ConsoleWidget
7 7
8 8
9 9 class HistoryConsoleWidget(ConsoleWidget):
10 10 """ A ConsoleWidget that keeps a history of the commands that have been
11 11 executed and provides a readline-esque interface to this history.
12 12 """
13 13
14 14 #------ Configuration ------------------------------------------------------
15 15
16 16 # If enabled, the input buffer will become "locked" to history movement when
17 17 # an edit is made to a multi-line input buffer. To override the lock, use
18 18 # Shift in conjunction with the standard history cycling keys.
19 19 history_lock = Bool(False, config=True)
20 20
21 21 #---------------------------------------------------------------------------
22 22 # 'object' interface
23 23 #---------------------------------------------------------------------------
24 24
25 25 def __init__(self, *args, **kw):
26 26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27 27
28 28 # HistoryConsoleWidget protected variables.
29 29 self._history = []
30 30 self._history_edits = {}
31 31 self._history_index = 0
32 32 self._history_prefix = ''
33 33
34 34 #---------------------------------------------------------------------------
35 35 # 'ConsoleWidget' public interface
36 36 #---------------------------------------------------------------------------
37 37
38 38 def execute(self, source=None, hidden=False, interactive=False):
39 39 """ Reimplemented to the store history.
40 40 """
41 41 if not hidden:
42 42 history = self.input_buffer if source is None else source
43 43
44 44 executed = super(HistoryConsoleWidget, self).execute(
45 45 source, hidden, interactive)
46 46
47 47 if executed and not hidden:
48 48 # Save the command unless it was an empty string or was identical
49 49 # to the previous command.
50 50 history = history.rstrip()
51 51 if history and (not self._history or self._history[-1] != history):
52 52 self._history.append(history)
53 53
54 54 # Emulate readline: reset all history edits.
55 55 self._history_edits = {}
56 56
57 57 # Move the history index to the most recent item.
58 58 self._history_index = len(self._history)
59 59
60 60 return executed
61 61
62 62 #---------------------------------------------------------------------------
63 63 # 'ConsoleWidget' abstract interface
64 64 #---------------------------------------------------------------------------
65 65
66 66 def _up_pressed(self, shift_modifier):
67 67 """ Called when the up key is pressed. Returns whether to continue
68 68 processing the event.
69 69 """
70 70 prompt_cursor = self._get_prompt_cursor()
71 71 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
72 72 # Bail out if we're locked.
73 73 if self._history_locked() and not shift_modifier:
74 74 return False
75 75
76 76 # Set a search prefix based on the cursor position.
77 77 col = self._get_input_buffer_cursor_column()
78 78 input_buffer = self.input_buffer
79 79 if self._history_index == len(self._history) or \
80 80 (self._history_prefix and col != len(self._history_prefix)):
81 81 self._history_index = len(self._history)
82 82 self._history_prefix = input_buffer[:col]
83 83
84 84 # Perform the search.
85 85 self.history_previous(self._history_prefix,
86 86 as_prefix=not shift_modifier)
87 87
88 88 # Go to the first line of the prompt for seemless history scrolling.
89 89 # Emulate readline: keep the cursor position fixed for a prefix
90 90 # search.
91 91 cursor = self._get_prompt_cursor()
92 92 if self._history_prefix:
93 93 cursor.movePosition(QtGui.QTextCursor.Right,
94 94 n=len(self._history_prefix))
95 95 else:
96 96 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
97 97 self._set_cursor(cursor)
98 98
99 99 return False
100 100
101 101 return True
102 102
103 103 def _down_pressed(self, shift_modifier):
104 104 """ Called when the down key is pressed. Returns whether to continue
105 105 processing the event.
106 106 """
107 107 end_cursor = self._get_end_cursor()
108 108 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
109 109 # Bail out if we're locked.
110 110 if self._history_locked() and not shift_modifier:
111 111 return False
112 112
113 113 # Perform the search.
114 114 replaced = self.history_next(self._history_prefix,
115 115 as_prefix=not shift_modifier)
116 116
117 117 # Emulate readline: keep the cursor position fixed for a prefix
118 118 # search. (We don't need to move the cursor to the end of the buffer
119 119 # in the other case because this happens automatically when the
120 120 # input buffer is set.)
121 121 if self._history_prefix and replaced:
122 122 cursor = self._get_prompt_cursor()
123 123 cursor.movePosition(QtGui.QTextCursor.Right,
124 124 n=len(self._history_prefix))
125 125 self._set_cursor(cursor)
126 126
127 127 return False
128 128
129 129 return True
130 130
131 131 #---------------------------------------------------------------------------
132 132 # 'HistoryConsoleWidget' public interface
133 133 #---------------------------------------------------------------------------
134 134
135 135 def history_previous(self, substring='', as_prefix=True):
136 136 """ If possible, set the input buffer to a previous history item.
137 137
138 138 Parameters:
139 139 -----------
140 140 substring : str, optional
141 141 If specified, search for an item with this substring.
142 142 as_prefix : bool, optional
143 143 If True, the substring must match at the beginning (default).
144 144
145 145 Returns:
146 146 --------
147 147 Whether the input buffer was changed.
148 148 """
149 149 index = self._history_index
150 150 replace = False
151 151 while index > 0:
152 152 index -= 1
153 153 history = self._get_edited_history(index)
154 154 if (as_prefix and history.startswith(substring)) \
155 155 or (not as_prefix and substring in history):
156 156 replace = True
157 157 break
158 158
159 159 if replace:
160 160 self._store_edits()
161 161 self._history_index = index
162 162 self.input_buffer = history
163 163
164 164 return replace
165 165
166 166 def history_next(self, substring='', as_prefix=True):
167 167 """ If possible, set the input buffer to a subsequent history item.
168 168
169 169 Parameters:
170 170 -----------
171 171 substring : str, optional
172 172 If specified, search for an item with this substring.
173 173 as_prefix : bool, optional
174 174 If True, the substring must match at the beginning (default).
175 175
176 176 Returns:
177 177 --------
178 178 Whether the input buffer was changed.
179 179 """
180 180 index = self._history_index
181 181 replace = False
182 182 while self._history_index < len(self._history):
183 183 index += 1
184 184 history = self._get_edited_history(index)
185 185 if (as_prefix and history.startswith(substring)) \
186 186 or (not as_prefix and substring in history):
187 187 replace = True
188 188 break
189 189
190 190 if replace:
191 191 self._store_edits()
192 192 self._history_index = index
193 193 self.input_buffer = history
194 194
195 195 return replace
196 196
197 197 def history_tail(self, n=10):
198 198 """ Get the local history list.
199 199
200 200 Parameters:
201 201 -----------
202 202 n : int
203 203 The (maximum) number of history items to get.
204 204 """
205 205 return self._history[-n:]
206 206
207 207 def _request_update_session_history_length(self):
208 208 msg_id = self.kernel_manager.shell_channel.execute('',
209 209 silent=True,
210 210 user_expressions={
211 211 'hlen':'len(get_ipython().history_manager.input_hist_raw)',
212 212 }
213 213 )
214 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'save_magic')
214 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'save_magic')
215 215
216 216 def _handle_execute_reply(self, msg):
217 217 """ Handles replies for code execution, here only session history length
218 218 """
219 info = self._request_info.get('execute')
220 if info and info.id == msg['parent_header']['msg_id'] and \
221 info.kind == 'save_magic' and not self._hidden:
219 msg_id = msg['parent_header']['msg_id']
220 info = self._request_info.get['execute'].pop(msg_id,None)
221 if info and info.kind == 'save_magic' and not self._hidden:
222 222 content = msg['content']
223 223 status = content['status']
224 224 if status == 'ok':
225 225 self._max_session_history=(int(content['user_expressions']['hlen']))
226 226
227 227 def save_magic(self):
228 228 # update the session history length
229 229 self._request_update_session_history_length()
230 230
231 231 file_name,extFilter = QtGui.QFileDialog.getSaveFileName(self,
232 232 "Enter A filename",
233 233 filter='Python File (*.py);; All files (*.*)'
234 234 )
235 235
236 236 # let's the user search/type for a file name, while the history length
237 237 # is fetched
238 238
239 239 if file_name:
240 240 hist_range, ok = QtGui.QInputDialog.getText(self,
241 241 'Please enter an interval of command to save',
242 242 'Saving commands:',
243 243 text=str('1-'+str(self._max_session_history))
244 244 )
245 245 if ok:
246 246 self.execute("%save"+" "+file_name+" "+str(hist_range))
247 247
248 248 #---------------------------------------------------------------------------
249 249 # 'HistoryConsoleWidget' protected interface
250 250 #---------------------------------------------------------------------------
251 251
252 252 def _history_locked(self):
253 253 """ Returns whether history movement is locked.
254 254 """
255 255 return (self.history_lock and
256 256 (self._get_edited_history(self._history_index) !=
257 257 self.input_buffer) and
258 258 (self._get_prompt_cursor().blockNumber() !=
259 259 self._get_end_cursor().blockNumber()))
260 260
261 261 def _get_edited_history(self, index):
262 262 """ Retrieves a history item, possibly with temporary edits.
263 263 """
264 264 if index in self._history_edits:
265 265 return self._history_edits[index]
266 266 elif index == len(self._history):
267 267 return unicode()
268 268 return self._history[index]
269 269
270 270 def _set_history(self, history):
271 271 """ Replace the current history with a sequence of history items.
272 272 """
273 273 self._history = list(history)
274 274 self._history_edits = {}
275 275 self._history_index = len(self._history)
276 276
277 277 def _store_edits(self):
278 278 """ If there are edits to the current input buffer, store them.
279 279 """
280 280 current = self.input_buffer
281 281 if self._history_index == len(self._history) or \
282 282 self._history[self._history_index] != current:
283 283 self._history_edits[self._history_index] = current
@@ -1,562 +1,563 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 23 transform_ipy_prompt
24 24 from IPython.utils.traitlets import Bool, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101
102 102 # IPythonWidget protected class variables.
103 103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 107 _payload_source_page = 'IPython.zmq.page.page'
108 108 _retrying_history_request = False
109 109
110 110 #---------------------------------------------------------------------------
111 111 # 'object' interface
112 112 #---------------------------------------------------------------------------
113 113
114 114 def __init__(self, *args, **kw):
115 115 super(IPythonWidget, self).__init__(*args, **kw)
116 116
117 117 # IPythonWidget protected variables.
118 118 self._payload_handlers = {
119 119 self._payload_source_edit : self._handle_payload_edit,
120 120 self._payload_source_exit : self._handle_payload_exit,
121 121 self._payload_source_page : self._handle_payload_page,
122 122 self._payload_source_next_input : self._handle_payload_next_input }
123 123 self._previous_prompt_obj = None
124 124 self._keep_kernel_on_exit = None
125 125
126 126 # Initialize widget styling.
127 127 if self.style_sheet:
128 128 self._style_sheet_changed()
129 129 self._syntax_style_changed()
130 130 else:
131 131 self.set_default_style()
132 132
133 133 #---------------------------------------------------------------------------
134 134 # 'BaseFrontendMixin' abstract interface
135 135 #---------------------------------------------------------------------------
136 136
137 137 def _handle_complete_reply(self, rep):
138 138 """ Reimplemented to support IPython's improved completion machinery.
139 139 """
140 140 self.log.debug("complete: %s", rep.get('content', ''))
141 141 cursor = self._get_cursor()
142 142 info = self._request_info.get('complete')
143 143 if info and info.id == rep['parent_header']['msg_id'] and \
144 144 info.pos == cursor.position():
145 145 matches = rep['content']['matches']
146 146 text = rep['content']['matched_text']
147 147 offset = len(text)
148 148
149 149 # Clean up matches with period and path separators if the matched
150 150 # text has not been transformed. This is done by truncating all
151 151 # but the last component and then suitably decreasing the offset
152 152 # between the current cursor position and the start of completion.
153 153 if len(matches) > 1 and matches[0][:offset] == text:
154 154 parts = re.split(r'[./\\]', text)
155 155 sep_count = len(parts) - 1
156 156 if sep_count:
157 157 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 158 matches = [ match[chop_length:] for match in matches ]
159 159 offset -= chop_length
160 160
161 161 # Move the cursor to the start of the match and complete.
162 162 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 163 self._complete_with_items(cursor, matches)
164 164
165 165 def _handle_execute_reply(self, msg):
166 166 """ Reimplemented to support prompt requests.
167 167 """
168 info = self._request_info.get('execute')
169 if info and info.id == msg['parent_header']['msg_id']:
170 if info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
173 else:
174 super(IPythonWidget, self)._handle_execute_reply(msg)
168 msg_id = msg['parent_header'].get('msg_id')
169 info = self._request_info['execute'].get(msg_id)
170 if info and info.kind == 'prompt':
171 number = msg['content']['execution_count'] + 1
172 self._show_interpreter_prompt(number)
173 self._request_info['execute'].pop(msg_id)
174 else:
175 super(IPythonWidget, self)._handle_execute_reply(msg)
175 176
176 177 def _handle_history_reply(self, msg):
177 178 """ Implemented to handle history tail replies, which are only supported
178 179 by the IPython kernel.
179 180 """
180 181 self.log.debug("history: %s", msg.get('content', ''))
181 182 content = msg['content']
182 183 if 'history' not in content:
183 184 self.log.error("History request failed: %r"%content)
184 185 if content.get('status', '') == 'aborted' and \
185 186 not self._retrying_history_request:
186 187 # a *different* action caused this request to be aborted, so
187 188 # we should try again.
188 189 self.log.error("Retrying aborted history request")
189 190 # prevent multiple retries of aborted requests:
190 191 self._retrying_history_request = True
191 192 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 193 time.sleep(0.25)
193 194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 195 else:
195 196 self._retrying_history_request = False
196 197 return
197 198 # reset retry flag
198 199 self._retrying_history_request = False
199 200 history_items = content['history']
200 201 items = []
201 202 last_cell = u""
202 203 for _, _, cell in history_items:
203 204 cell = cell.rstrip()
204 205 if cell != last_cell:
205 206 items.append(cell)
206 207 last_cell = cell
207 208 self._set_history(items)
208 209
209 210 def _handle_pyout(self, msg):
210 211 """ Reimplemented for IPython-style "display hook".
211 212 """
212 213 self.log.debug("pyout: %s", msg.get('content', ''))
213 214 if not self._hidden and self._is_from_this_session(msg):
214 215 content = msg['content']
215 216 prompt_number = content['execution_count']
216 217 data = content['data']
217 218 if data.has_key('text/html'):
218 219 self._append_plain_text(self.output_sep, True)
219 220 self._append_html(self._make_out_prompt(prompt_number), True)
220 221 html = data['text/html']
221 222 self._append_plain_text('\n', True)
222 223 self._append_html(html + self.output_sep2, True)
223 224 elif data.has_key('text/plain'):
224 225 self._append_plain_text(self.output_sep, True)
225 226 self._append_html(self._make_out_prompt(prompt_number), True)
226 227 text = data['text/plain']
227 228 # If the repr is multiline, make sure we start on a new line,
228 229 # so that its lines are aligned.
229 230 if "\n" in text and not self.output_sep.endswith("\n"):
230 231 self._append_plain_text('\n', True)
231 232 self._append_plain_text(text + self.output_sep2, True)
232 233
233 234 def _handle_display_data(self, msg):
234 235 """ The base handler for the ``display_data`` message.
235 236 """
236 237 self.log.debug("display: %s", msg.get('content', ''))
237 238 # For now, we don't display data from other frontends, but we
238 239 # eventually will as this allows all frontends to monitor the display
239 240 # data. But we need to figure out how to handle this in the GUI.
240 241 if not self._hidden and self._is_from_this_session(msg):
241 242 source = msg['content']['source']
242 243 data = msg['content']['data']
243 244 metadata = msg['content']['metadata']
244 245 # In the regular IPythonWidget, we simply print the plain text
245 246 # representation.
246 247 if data.has_key('text/html'):
247 248 html = data['text/html']
248 249 self._append_html(html, True)
249 250 elif data.has_key('text/plain'):
250 251 text = data['text/plain']
251 252 self._append_plain_text(text, True)
252 253 # This newline seems to be needed for text and html output.
253 254 self._append_plain_text(u'\n', True)
254 255
255 256 def _started_channels(self):
256 257 """ Reimplemented to make a history request.
257 258 """
258 259 super(IPythonWidget, self)._started_channels()
259 260 self.kernel_manager.shell_channel.history(hist_access_type='tail',
260 261 n=1000)
261 262 #---------------------------------------------------------------------------
262 263 # 'ConsoleWidget' public interface
263 264 #---------------------------------------------------------------------------
264 265
265 266 def copy(self):
266 267 """ Copy the currently selected text to the clipboard, removing prompts
267 268 if possible.
268 269 """
269 270 text = self._control.textCursor().selection().toPlainText()
270 271 if text:
271 272 lines = map(transform_ipy_prompt, text.splitlines())
272 273 text = '\n'.join(lines)
273 274 QtGui.QApplication.clipboard().setText(text)
274 275
275 276 #---------------------------------------------------------------------------
276 277 # 'FrontendWidget' public interface
277 278 #---------------------------------------------------------------------------
278 279
279 280 def execute_file(self, path, hidden=False):
280 281 """ Reimplemented to use the 'run' magic.
281 282 """
282 283 # Use forward slashes on Windows to avoid escaping each separator.
283 284 if sys.platform == 'win32':
284 285 path = os.path.normpath(path).replace('\\', '/')
285 286
286 287 # Perhaps we should not be using %run directly, but while we
287 288 # are, it is necessary to quote filenames containing spaces or quotes.
288 289 # Escaping quotes in filename in %run seems tricky and inconsistent,
289 290 # so not trying it at present.
290 291 if '"' in path:
291 292 if "'" in path:
292 293 raise ValueError("Can't run filename containing both single "
293 294 "and double quotes: %s" % path)
294 295 path = "'%s'" % path
295 296 elif ' ' in path or "'" in path:
296 297 path = '"%s"' % path
297 298
298 299 self.execute('%%run %s' % path, hidden=hidden)
299 300
300 301 #---------------------------------------------------------------------------
301 302 # 'FrontendWidget' protected interface
302 303 #---------------------------------------------------------------------------
303 304
304 305 def _complete(self):
305 306 """ Reimplemented to support IPython's improved completion machinery.
306 307 """
307 308 # We let the kernel split the input line, so we *always* send an empty
308 309 # text field. Readline-based frontends do get a real text field which
309 310 # they can use.
310 311 text = ''
311 312
312 313 # Send the completion request to the kernel
313 314 msg_id = self.kernel_manager.shell_channel.complete(
314 315 text, # text
315 316 self._get_input_buffer_cursor_line(), # line
316 317 self._get_input_buffer_cursor_column(), # cursor_pos
317 318 self.input_buffer) # block
318 319 pos = self._get_cursor().position()
319 320 info = self._CompletionRequest(msg_id, pos)
320 321 self._request_info['complete'] = info
321 322
322 323 def _process_execute_error(self, msg):
323 324 """ Reimplemented for IPython-style traceback formatting.
324 325 """
325 326 content = msg['content']
326 327 traceback = '\n'.join(content['traceback']) + '\n'
327 328 if False:
328 329 # FIXME: For now, tracebacks come as plain text, so we can't use
329 330 # the html renderer yet. Once we refactor ultratb to produce
330 331 # properly styled tracebacks, this branch should be the default
331 332 traceback = traceback.replace(' ', '&nbsp;')
332 333 traceback = traceback.replace('\n', '<br/>')
333 334
334 335 ename = content['ename']
335 336 ename_styled = '<span class="error">%s</span>' % ename
336 337 traceback = traceback.replace(ename, ename_styled)
337 338
338 339 self._append_html(traceback)
339 340 else:
340 341 # This is the fallback for now, using plain text with ansi escapes
341 342 self._append_plain_text(traceback)
342 343
343 344 def _process_execute_payload(self, item):
344 345 """ Reimplemented to dispatch payloads to handler methods.
345 346 """
346 347 handler = self._payload_handlers.get(item['source'])
347 348 if handler is None:
348 349 # We have no handler for this type of payload, simply ignore it
349 350 return False
350 351 else:
351 352 handler(item)
352 353 return True
353 354
354 355 def _show_interpreter_prompt(self, number=None):
355 356 """ Reimplemented for IPython-style prompts.
356 357 """
357 358 # If a number was not specified, make a prompt number request.
358 359 if number is None:
359 360 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
360 361 info = self._ExecutionRequest(msg_id, 'prompt')
361 self._request_info['execute'] = info
362 self._request_info['execute'][msg_id] = info
362 363 return
363 364
364 365 # Show a new prompt and save information about it so that it can be
365 366 # updated later if the prompt number turns out to be wrong.
366 367 self._prompt_sep = self.input_sep
367 368 self._show_prompt(self._make_in_prompt(number), html=True)
368 369 block = self._control.document().lastBlock()
369 370 length = len(self._prompt)
370 371 self._previous_prompt_obj = self._PromptBlock(block, length, number)
371 372
372 373 # Update continuation prompt to reflect (possibly) new prompt length.
373 374 self._set_continuation_prompt(
374 375 self._make_continuation_prompt(self._prompt), html=True)
375 376
376 377 def _show_interpreter_prompt_for_reply(self, msg):
377 378 """ Reimplemented for IPython-style prompts.
378 379 """
379 380 # Update the old prompt number if necessary.
380 381 content = msg['content']
381 382 # abort replies do not have any keys:
382 383 if content['status'] == 'aborted':
383 384 if self._previous_prompt_obj:
384 385 previous_prompt_number = self._previous_prompt_obj.number
385 386 else:
386 387 previous_prompt_number = 0
387 388 else:
388 389 previous_prompt_number = content['execution_count']
389 390 if self._previous_prompt_obj and \
390 391 self._previous_prompt_obj.number != previous_prompt_number:
391 392 block = self._previous_prompt_obj.block
392 393
393 394 # Make sure the prompt block has not been erased.
394 395 if block.isValid() and block.text():
395 396
396 397 # Remove the old prompt and insert a new prompt.
397 398 cursor = QtGui.QTextCursor(block)
398 399 cursor.movePosition(QtGui.QTextCursor.Right,
399 400 QtGui.QTextCursor.KeepAnchor,
400 401 self._previous_prompt_obj.length)
401 402 prompt = self._make_in_prompt(previous_prompt_number)
402 403 self._prompt = self._insert_html_fetching_plain_text(
403 404 cursor, prompt)
404 405
405 406 # When the HTML is inserted, Qt blows away the syntax
406 407 # highlighting for the line, so we need to rehighlight it.
407 408 self._highlighter.rehighlightBlock(cursor.block())
408 409
409 410 self._previous_prompt_obj = None
410 411
411 412 # Show a new prompt with the kernel's estimated prompt number.
412 413 self._show_interpreter_prompt(previous_prompt_number + 1)
413 414
414 415 #---------------------------------------------------------------------------
415 416 # 'IPythonWidget' interface
416 417 #---------------------------------------------------------------------------
417 418
418 419 def set_default_style(self, colors='lightbg'):
419 420 """ Sets the widget style to the class defaults.
420 421
421 422 Parameters:
422 423 -----------
423 424 colors : str, optional (default lightbg)
424 425 Whether to use the default IPython light background or dark
425 426 background or B&W style.
426 427 """
427 428 colors = colors.lower()
428 429 if colors=='lightbg':
429 430 self.style_sheet = styles.default_light_style_sheet
430 431 self.syntax_style = styles.default_light_syntax_style
431 432 elif colors=='linux':
432 433 self.style_sheet = styles.default_dark_style_sheet
433 434 self.syntax_style = styles.default_dark_syntax_style
434 435 elif colors=='nocolor':
435 436 self.style_sheet = styles.default_bw_style_sheet
436 437 self.syntax_style = styles.default_bw_syntax_style
437 438 else:
438 439 raise KeyError("No such color scheme: %s"%colors)
439 440
440 441 #---------------------------------------------------------------------------
441 442 # 'IPythonWidget' protected interface
442 443 #---------------------------------------------------------------------------
443 444
444 445 def _edit(self, filename, line=None):
445 446 """ Opens a Python script for editing.
446 447
447 448 Parameters:
448 449 -----------
449 450 filename : str
450 451 A path to a local system file.
451 452
452 453 line : int, optional
453 454 A line of interest in the file.
454 455 """
455 456 if self.custom_edit:
456 457 self.custom_edit_requested.emit(filename, line)
457 458 elif not self.editor:
458 459 self._append_plain_text('No default editor available.\n'
459 460 'Specify a GUI text editor in the `IPythonWidget.editor` '
460 461 'configurable to enable the %edit magic')
461 462 else:
462 463 try:
463 464 filename = '"%s"' % filename
464 465 if line and self.editor_line:
465 466 command = self.editor_line.format(filename=filename,
466 467 line=line)
467 468 else:
468 469 try:
469 470 command = self.editor.format()
470 471 except KeyError:
471 472 command = self.editor.format(filename=filename)
472 473 else:
473 474 command += ' ' + filename
474 475 except KeyError:
475 476 self._append_plain_text('Invalid editor command.\n')
476 477 else:
477 478 try:
478 479 Popen(command, shell=True)
479 480 except OSError:
480 481 msg = 'Opening editor with command "%s" failed.\n'
481 482 self._append_plain_text(msg % command)
482 483
483 484 def _make_in_prompt(self, number):
484 485 """ Given a prompt number, returns an HTML In prompt.
485 486 """
486 487 try:
487 488 body = self.in_prompt % number
488 489 except TypeError:
489 490 # allow in_prompt to leave out number, e.g. '>>> '
490 491 body = self.in_prompt
491 492 return '<span class="in-prompt">%s</span>' % body
492 493
493 494 def _make_continuation_prompt(self, prompt):
494 495 """ Given a plain text version of an In prompt, returns an HTML
495 496 continuation prompt.
496 497 """
497 498 end_chars = '...: '
498 499 space_count = len(prompt.lstrip('\n')) - len(end_chars)
499 500 body = '&nbsp;' * space_count + end_chars
500 501 return '<span class="in-prompt">%s</span>' % body
501 502
502 503 def _make_out_prompt(self, number):
503 504 """ Given a prompt number, returns an HTML Out prompt.
504 505 """
505 506 body = self.out_prompt % number
506 507 return '<span class="out-prompt">%s</span>' % body
507 508
508 509 #------ Payload handlers --------------------------------------------------
509 510
510 511 # Payload handlers with a generic interface: each takes the opaque payload
511 512 # dict, unpacks it and calls the underlying functions with the necessary
512 513 # arguments.
513 514
514 515 def _handle_payload_edit(self, item):
515 516 self._edit(item['filename'], item['line_number'])
516 517
517 518 def _handle_payload_exit(self, item):
518 519 self._keep_kernel_on_exit = item['keepkernel']
519 520 self.exit_requested.emit(self)
520 521
521 522 def _handle_payload_next_input(self, item):
522 523 self.input_buffer = dedent(item['text'].rstrip())
523 524
524 525 def _handle_payload_page(self, item):
525 526 # Since the plain text widget supports only a very small subset of HTML
526 527 # and we have no control over the HTML source, we only page HTML
527 528 # payloads in the rich text widget.
528 529 if item['html'] and self.kind == 'rich':
529 530 self._page(item['html'], html=True)
530 531 else:
531 532 self._page(item['text'], html=False)
532 533
533 534 #------ Trait change handlers --------------------------------------------
534 535
535 536 def _style_sheet_changed(self):
536 537 """ Set the style sheets of the underlying widgets.
537 538 """
538 539 self.setStyleSheet(self.style_sheet)
539 540 self._control.document().setDefaultStyleSheet(self.style_sheet)
540 541 if self._page_control:
541 542 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
542 543
543 544 bg_color = self._control.palette().window().color()
544 545 self._ansi_processor.set_background_color(bg_color)
545 546
546 547
547 548 def _syntax_style_changed(self):
548 549 """ Set the style for the syntax highlighter.
549 550 """
550 551 if self._highlighter is None:
551 552 # ignore premature calls
552 553 return
553 554 if self.syntax_style:
554 555 self._highlighter.set_style(self.syntax_style)
555 556 else:
556 557 self._highlighter.set_style_sheet(self.style_sheet)
557 558
558 559 #------ Trait default initializers -----------------------------------------
559 560
560 561 def _banner_default(self):
561 562 from IPython.core.usage import default_gui_banner
562 563 return default_gui_banner
@@ -1,957 +1,909 b''
1 1 """The Qt MainWindow for the QtConsole
2 2
3 3 This is a tabbed pseudo-terminal of IPython sessions, with a menu bar for
4 4 common actions.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # stdlib imports
22 22 import sys
23 23 import re
24 24 import webbrowser
25 25 from threading import Thread
26 26
27 27 # System library imports
28 28 from IPython.external.qt import QtGui,QtCore
29 29
30 30 def background(f):
31 31 """call a function in a simple thread, to prevent blocking"""
32 32 t = Thread(target=f)
33 33 t.start()
34 34 return t
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Classes
38 38 #-----------------------------------------------------------------------------
39 39
40 40 class MainWindow(QtGui.QMainWindow):
41 41
42 42 #---------------------------------------------------------------------------
43 43 # 'object' interface
44 44 #---------------------------------------------------------------------------
45 45
46 46 def __init__(self, app,
47 47 confirm_exit=True,
48 48 new_frontend_factory=None, slave_frontend_factory=None,
49 49 ):
50 50 """ Create a tabbed MainWindow for managing IPython FrontendWidgets
51 51
52 52 Parameters
53 53 ----------
54 54
55 55 app : reference to QApplication parent
56 56 confirm_exit : bool, optional
57 57 Whether we should prompt on close of tabs
58 58 new_frontend_factory : callable
59 59 A callable that returns a new IPythonWidget instance, attached to
60 60 its own running kernel.
61 61 slave_frontend_factory : callable
62 62 A callable that takes an existing IPythonWidget, and returns a new
63 63 IPythonWidget instance, attached to the same kernel.
64 64 """
65 65
66 66 super(MainWindow, self).__init__()
67 67 self._kernel_counter = 0
68 68 self._app = app
69 69 self.confirm_exit = confirm_exit
70 70 self.new_frontend_factory = new_frontend_factory
71 71 self.slave_frontend_factory = slave_frontend_factory
72 72
73 73 self.tab_widget = QtGui.QTabWidget(self)
74 74 self.tab_widget.setDocumentMode(True)
75 75 self.tab_widget.setTabsClosable(True)
76 76 self.tab_widget.tabCloseRequested[int].connect(self.close_tab)
77 77
78 78 self.setCentralWidget(self.tab_widget)
79 79 # hide tab bar at first, since we have no tabs:
80 80 self.tab_widget.tabBar().setVisible(False)
81 81 # prevent focus in tab bar
82 82 self.tab_widget.setFocusPolicy(QtCore.Qt.NoFocus)
83 83
84 84 def update_tab_bar_visibility(self):
85 85 """ update visibility of the tabBar depending of the number of tab
86 86
87 87 0 or 1 tab, tabBar hidden
88 88 2+ tabs, tabBar visible
89 89
90 90 send a self.close if number of tab ==0
91 91
92 92 need to be called explicitely, or be connected to tabInserted/tabRemoved
93 93 """
94 94 if self.tab_widget.count() <= 1:
95 95 self.tab_widget.tabBar().setVisible(False)
96 96 else:
97 97 self.tab_widget.tabBar().setVisible(True)
98 98 if self.tab_widget.count()==0 :
99 99 self.close()
100 100
101 101 @property
102 102 def next_kernel_id(self):
103 103 """constantly increasing counter for kernel IDs"""
104 104 c = self._kernel_counter
105 105 self._kernel_counter += 1
106 106 return c
107 107
108 108 @property
109 109 def active_frontend(self):
110 110 return self.tab_widget.currentWidget()
111 111
112 112 def create_tab_with_new_frontend(self):
113 113 """create a new frontend and attach it to a new tab"""
114 114 widget = self.new_frontend_factory()
115 115 self.add_tab_with_frontend(widget)
116 116
117 117 def create_tab_with_current_kernel(self):
118 118 """create a new frontend attached to the same kernel as the current tab"""
119 119 current_widget = self.tab_widget.currentWidget()
120 120 current_widget_index = self.tab_widget.indexOf(current_widget)
121 121 current_widget_name = self.tab_widget.tabText(current_widget_index)
122 122 widget = self.slave_frontend_factory(current_widget)
123 123 if 'slave' in current_widget_name:
124 124 # don't keep stacking slaves
125 125 name = current_widget_name
126 126 else:
127 127 name = '(%s) slave' % current_widget_name
128 128 self.add_tab_with_frontend(widget,name=name)
129 129
130 130 def close_tab(self,current_tab):
131 131 """ Called when you need to try to close a tab.
132 132
133 133 It takes the number of the tab to be closed as argument, or a referece
134 134 to the wiget insite this tab
135 135 """
136 136
137 137 # let's be sure "tab" and "closing widget are respectivey the index of the tab to close
138 138 # and a reference to the trontend to close
139 139 if type(current_tab) is not int :
140 140 current_tab = self.tab_widget.indexOf(current_tab)
141 141 closing_widget=self.tab_widget.widget(current_tab)
142 142
143 143
144 144 # when trying to be closed, widget might re-send a request to be closed again, but will
145 145 # be deleted when event will be processed. So need to check that widget still exist and
146 146 # skip if not. One example of this is when 'exit' is send in a slave tab. 'exit' will be
147 147 # re-send by this fonction on the master widget, which ask all slaves widget to exit
148 148 if closing_widget==None:
149 149 return
150 150
151 151 #get a list of all slave widgets on the same kernel.
152 152 slave_tabs = self.find_slave_widgets(closing_widget)
153 153
154 154 keepkernel = None #Use the prompt by default
155 155 if hasattr(closing_widget,'_keep_kernel_on_exit'): #set by exit magic
156 156 keepkernel = closing_widget._keep_kernel_on_exit
157 157 # If signal sent by exit magic (_keep_kernel_on_exit, exist and not None)
158 158 # we set local slave tabs._hidden to True to avoid prompting for kernel
159 159 # restart when they get the signal. and then "forward" the 'exit'
160 160 # to the main window
161 161 if keepkernel is not None:
162 162 for tab in slave_tabs:
163 163 tab._hidden = True
164 164 if closing_widget in slave_tabs:
165 165 try :
166 166 self.find_master_tab(closing_widget).execute('exit')
167 167 except AttributeError:
168 168 self.log.info("Master already closed or not local, closing only current tab")
169 169 self.tab_widget.removeTab(current_tab)
170 170 self.update_tab_bar_visibility()
171 171 return
172 172
173 173 kernel_manager = closing_widget.kernel_manager
174 174
175 175 if keepkernel is None and not closing_widget._confirm_exit:
176 176 # don't prompt, just terminate the kernel if we own it
177 177 # or leave it alone if we don't
178 178 keepkernel = closing_widget._existing
179 179 if keepkernel is None: #show prompt
180 180 if kernel_manager and kernel_manager.channels_running:
181 181 title = self.window().windowTitle()
182 182 cancel = QtGui.QMessageBox.Cancel
183 183 okay = QtGui.QMessageBox.Ok
184 184 if closing_widget._may_close:
185 185 msg = "You are closing the tab : "+'"'+self.tab_widget.tabText(current_tab)+'"'
186 186 info = "Would you like to quit the Kernel and close all attached Consoles as well?"
187 187 justthis = QtGui.QPushButton("&No, just this Tab", self)
188 188 justthis.setShortcut('N')
189 189 closeall = QtGui.QPushButton("&Yes, close all", self)
190 190 closeall.setShortcut('Y')
191 191 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
192 192 title, msg)
193 193 box.setInformativeText(info)
194 194 box.addButton(cancel)
195 195 box.addButton(justthis, QtGui.QMessageBox.NoRole)
196 196 box.addButton(closeall, QtGui.QMessageBox.YesRole)
197 197 box.setDefaultButton(closeall)
198 198 box.setEscapeButton(cancel)
199 199 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
200 200 box.setIconPixmap(pixmap)
201 201 reply = box.exec_()
202 202 if reply == 1: # close All
203 203 for slave in slave_tabs:
204 204 background(slave.kernel_manager.stop_channels)
205 205 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
206 206 closing_widget.execute("exit")
207 207 self.tab_widget.removeTab(current_tab)
208 208 background(kernel_manager.stop_channels)
209 209 elif reply == 0: # close Console
210 210 if not closing_widget._existing:
211 211 # Have kernel: don't quit, just close the tab
212 212 closing_widget.execute("exit True")
213 213 self.tab_widget.removeTab(current_tab)
214 214 background(kernel_manager.stop_channels)
215 215 else:
216 216 reply = QtGui.QMessageBox.question(self, title,
217 217 "Are you sure you want to close this Console?"+
218 218 "\nThe Kernel and other Consoles will remain active.",
219 219 okay|cancel,
220 220 defaultButton=okay
221 221 )
222 222 if reply == okay:
223 223 self.tab_widget.removeTab(current_tab)
224 224 elif keepkernel: #close console but leave kernel running (no prompt)
225 225 self.tab_widget.removeTab(current_tab)
226 226 background(kernel_manager.stop_channels)
227 227 else: #close console and kernel (no prompt)
228 228 self.tab_widget.removeTab(current_tab)
229 229 if kernel_manager and kernel_manager.channels_running:
230 230 for slave in slave_tabs:
231 231 background(slave.kernel_manager.stop_channels)
232 232 self.tab_widget.removeTab(self.tab_widget.indexOf(slave))
233 233 kernel_manager.shutdown_kernel()
234 234 background(kernel_manager.stop_channels)
235 235
236 236 self.update_tab_bar_visibility()
237 237
238 238 def add_tab_with_frontend(self,frontend,name=None):
239 239 """ insert a tab with a given frontend in the tab bar, and give it a name
240 240
241 241 """
242 242 if not name:
243 243 name = 'kernel %i' % self.next_kernel_id
244 244 self.tab_widget.addTab(frontend,name)
245 245 self.update_tab_bar_visibility()
246 246 self.make_frontend_visible(frontend)
247 247 frontend.exit_requested.connect(self.close_tab)
248 248
249 249 def next_tab(self):
250 250 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()+1))
251 251
252 252 def prev_tab(self):
253 253 self.tab_widget.setCurrentIndex((self.tab_widget.currentIndex()-1))
254 254
255 255 def make_frontend_visible(self,frontend):
256 256 widget_index=self.tab_widget.indexOf(frontend)
257 257 if widget_index > 0 :
258 258 self.tab_widget.setCurrentIndex(widget_index)
259 259
260 260 def find_master_tab(self,tab,as_list=False):
261 261 """
262 262 Try to return the frontend that own the kernel attached to the given widget/tab.
263 263
264 264 Only find frontend owed by the current application. Selection
265 265 based on port of the kernel, might be inacurate if several kernel
266 266 on different ip use same port number.
267 267
268 268 This fonction does the conversion tabNumber/widget if needed.
269 269 Might return None if no master widget (non local kernel)
270 270 Will crash IPython if more than 1 masterWidget
271 271
272 272 When asList set to True, always return a list of widget(s) owning
273 273 the kernel. The list might be empty or containing several Widget.
274 274 """
275 275
276 276 #convert from/to int/richIpythonWidget if needed
277 277 if isinstance(tab, int):
278 278 tab = self.tab_widget.widget(tab)
279 279 km=tab.kernel_manager
280 280
281 281 #build list of all widgets
282 282 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
283 283
284 284 # widget that are candidate to be the owner of the kernel does have all the same port of the curent widget
285 285 # And should have a _may_close attribute
286 286 filtered_widget_list = [ widget for widget in widget_list if
287 287 widget.kernel_manager.connection_file == km.connection_file and
288 288 hasattr(widget,'_may_close') ]
289 289 # the master widget is the one that may close the kernel
290 290 master_widget= [ widget for widget in filtered_widget_list if widget._may_close]
291 291 if as_list:
292 292 return master_widget
293 293 assert(len(master_widget)<=1 )
294 294 if len(master_widget)==0:
295 295 return None
296 296
297 297 return master_widget[0]
298 298
299 299 def find_slave_widgets(self,tab):
300 300 """return all the frontends that do not own the kernel attached to the given widget/tab.
301 301
302 302 Only find frontends owned by the current application. Selection
303 303 based on connection file of the kernel.
304 304
305 305 This function does the conversion tabNumber/widget if needed.
306 306 """
307 307 #convert from/to int/richIpythonWidget if needed
308 308 if isinstance(tab, int):
309 309 tab = self.tab_widget.widget(tab)
310 310 km=tab.kernel_manager
311 311
312 312 #build list of all widgets
313 313 widget_list = [self.tab_widget.widget(i) for i in range(self.tab_widget.count())]
314 314
315 315 # widget that are candidate not to be the owner of the kernel does have all the same port of the curent widget
316 316 filtered_widget_list = ( widget for widget in widget_list if
317 317 widget.kernel_manager.connection_file == km.connection_file)
318 318 # Get a list of all widget owning the same kernel and removed it from
319 319 # the previous cadidate. (better using sets ?)
320 320 master_widget_list = self.find_master_tab(tab, as_list=True)
321 321 slave_list = [widget for widget in filtered_widget_list if widget not in master_widget_list]
322 322
323 323 return slave_list
324 324
325 325 # Populate the menu bar with common actions and shortcuts
326 326 def add_menu_action(self, menu, action, defer_shortcut=False):
327 327 """Add action to menu as well as self
328 328
329 329 So that when the menu bar is invisible, its actions are still available.
330 330
331 331 If defer_shortcut is True, set the shortcut context to widget-only,
332 332 where it will avoid conflict with shortcuts already bound to the
333 333 widgets themselves.
334 334 """
335 335 menu.addAction(action)
336 336 self.addAction(action)
337 337
338 338 if defer_shortcut:
339 339 action.setShortcutContext(QtCore.Qt.WidgetShortcut)
340 340
341 341 def init_menu_bar(self):
342 342 #create menu in the order they should appear in the menu bar
343 343 self.init_file_menu()
344 344 self.init_edit_menu()
345 345 self.init_view_menu()
346 346 self.init_kernel_menu()
347 347 self.init_magic_menu()
348 348 self.init_window_menu()
349 349 self.init_help_menu()
350 350
351 351 def init_file_menu(self):
352 352 self.file_menu = self.menuBar().addMenu("&File")
353 353
354 354 self.new_kernel_tab_act = QtGui.QAction("New Tab with &New kernel",
355 355 self,
356 356 shortcut="Ctrl+T",
357 357 triggered=self.create_tab_with_new_frontend)
358 358 self.add_menu_action(self.file_menu, self.new_kernel_tab_act)
359 359
360 360 self.slave_kernel_tab_act = QtGui.QAction("New Tab with Sa&me kernel",
361 361 self,
362 362 shortcut="Ctrl+Shift+T",
363 363 triggered=self.create_tab_with_current_kernel)
364 364 self.add_menu_action(self.file_menu, self.slave_kernel_tab_act)
365 365
366 366 self.file_menu.addSeparator()
367 367
368 368 self.close_action=QtGui.QAction("&Close Tab",
369 369 self,
370 370 shortcut=QtGui.QKeySequence.Close,
371 371 triggered=self.close_active_frontend
372 372 )
373 373 self.add_menu_action(self.file_menu, self.close_action)
374 374
375 375 self.export_action=QtGui.QAction("&Save to HTML/XHTML",
376 376 self,
377 377 shortcut=QtGui.QKeySequence.Save,
378 378 triggered=self.export_action_active_frontend
379 379 )
380 380 self.add_menu_action(self.file_menu, self.export_action, True)
381 381
382 382 self.file_menu.addSeparator()
383 383
384 384 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
385 385 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
386 386 # Only override the default if there is a collision.
387 387 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
388 388 printkey = "Ctrl+Shift+P"
389 389 self.print_action = QtGui.QAction("&Print",
390 390 self,
391 391 shortcut=printkey,
392 392 triggered=self.print_action_active_frontend)
393 393 self.add_menu_action(self.file_menu, self.print_action, True)
394 394
395 395 if sys.platform != 'darwin':
396 396 # OSX always has Quit in the Application menu, only add it
397 397 # to the File menu elsewhere.
398 398
399 399 self.file_menu.addSeparator()
400 400
401 401 self.quit_action = QtGui.QAction("&Quit",
402 402 self,
403 403 shortcut=QtGui.QKeySequence.Quit,
404 404 triggered=self.close,
405 405 )
406 406 self.add_menu_action(self.file_menu, self.quit_action)
407 407
408 408
409 409 def init_edit_menu(self):
410 410 self.edit_menu = self.menuBar().addMenu("&Edit")
411 411
412 412 self.undo_action = QtGui.QAction("&Undo",
413 413 self,
414 414 shortcut=QtGui.QKeySequence.Undo,
415 415 statusTip="Undo last action if possible",
416 416 triggered=self.undo_active_frontend
417 417 )
418 418 self.add_menu_action(self.edit_menu, self.undo_action)
419 419
420 420 self.redo_action = QtGui.QAction("&Redo",
421 421 self,
422 422 shortcut=QtGui.QKeySequence.Redo,
423 423 statusTip="Redo last action if possible",
424 424 triggered=self.redo_active_frontend)
425 425 self.add_menu_action(self.edit_menu, self.redo_action)
426 426
427 427 self.edit_menu.addSeparator()
428 428
429 429 self.cut_action = QtGui.QAction("&Cut",
430 430 self,
431 431 shortcut=QtGui.QKeySequence.Cut,
432 432 triggered=self.cut_active_frontend
433 433 )
434 434 self.add_menu_action(self.edit_menu, self.cut_action, True)
435 435
436 436 self.copy_action = QtGui.QAction("&Copy",
437 437 self,
438 438 shortcut=QtGui.QKeySequence.Copy,
439 439 triggered=self.copy_active_frontend
440 440 )
441 441 self.add_menu_action(self.edit_menu, self.copy_action, True)
442 442
443 443 self.copy_raw_action = QtGui.QAction("Copy (&Raw Text)",
444 444 self,
445 445 shortcut="Ctrl+Shift+C",
446 446 triggered=self.copy_raw_active_frontend
447 447 )
448 448 self.add_menu_action(self.edit_menu, self.copy_raw_action, True)
449 449
450 450 self.paste_action = QtGui.QAction("&Paste",
451 451 self,
452 452 shortcut=QtGui.QKeySequence.Paste,
453 453 triggered=self.paste_active_frontend
454 454 )
455 455 self.add_menu_action(self.edit_menu, self.paste_action, True)
456 456
457 457 self.edit_menu.addSeparator()
458 458
459 459 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
460 460 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
461 461 # Only override the default if there is a collision.
462 462 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
463 463 selectall = "Ctrl+Shift+A"
464 464 self.select_all_action = QtGui.QAction("Select &All",
465 465 self,
466 466 shortcut=selectall,
467 467 triggered=self.select_all_active_frontend
468 468 )
469 469 self.add_menu_action(self.edit_menu, self.select_all_action, True)
470 470
471 471
472 472 def init_view_menu(self):
473 473 self.view_menu = self.menuBar().addMenu("&View")
474 474
475 475 if sys.platform != 'darwin':
476 476 # disable on OSX, where there is always a menu bar
477 477 self.toggle_menu_bar_act = QtGui.QAction("Toggle &Menu Bar",
478 478 self,
479 479 shortcut="Ctrl+Shift+M",
480 480 statusTip="Toggle visibility of menubar",
481 481 triggered=self.toggle_menu_bar)
482 482 self.add_menu_action(self.view_menu, self.toggle_menu_bar_act)
483 483
484 484 fs_key = "Ctrl+Meta+F" if sys.platform == 'darwin' else "F11"
485 485 self.full_screen_act = QtGui.QAction("&Full Screen",
486 486 self,
487 487 shortcut=fs_key,
488 488 statusTip="Toggle between Fullscreen and Normal Size",
489 489 triggered=self.toggleFullScreen)
490 490 self.add_menu_action(self.view_menu, self.full_screen_act)
491 491
492 492 self.view_menu.addSeparator()
493 493
494 494 self.increase_font_size = QtGui.QAction("Zoom &In",
495 495 self,
496 496 shortcut=QtGui.QKeySequence.ZoomIn,
497 497 triggered=self.increase_font_size_active_frontend
498 498 )
499 499 self.add_menu_action(self.view_menu, self.increase_font_size, True)
500 500
501 501 self.decrease_font_size = QtGui.QAction("Zoom &Out",
502 502 self,
503 503 shortcut=QtGui.QKeySequence.ZoomOut,
504 504 triggered=self.decrease_font_size_active_frontend
505 505 )
506 506 self.add_menu_action(self.view_menu, self.decrease_font_size, True)
507 507
508 508 self.reset_font_size = QtGui.QAction("Zoom &Reset",
509 509 self,
510 510 shortcut="Ctrl+0",
511 511 triggered=self.reset_font_size_active_frontend
512 512 )
513 513 self.add_menu_action(self.view_menu, self.reset_font_size, True)
514 514
515 515 self.view_menu.addSeparator()
516 516
517 517 self.clear_action = QtGui.QAction("&Clear Screen",
518 518 self,
519 519 shortcut='Ctrl+L',
520 520 statusTip="Clear the console",
521 521 triggered=self.clear_magic_active_frontend)
522 522 self.add_menu_action(self.view_menu, self.clear_action)
523 523
524 524 def init_kernel_menu(self):
525 525 self.kernel_menu = self.menuBar().addMenu("&Kernel")
526 526 # Qt on OSX maps Ctrl to Cmd, and Meta to Ctrl
527 527 # keep the signal shortcuts to ctrl, rather than
528 528 # platform-default like we do elsewhere.
529 529
530 530 ctrl = "Meta" if sys.platform == 'darwin' else "Ctrl"
531 531
532 532 self.interrupt_kernel_action = QtGui.QAction("Interrupt current Kernel",
533 533 self,
534 534 triggered=self.interrupt_kernel_active_frontend,
535 535 shortcut=ctrl+"+C",
536 536 )
537 537 self.add_menu_action(self.kernel_menu, self.interrupt_kernel_action)
538 538
539 539 self.restart_kernel_action = QtGui.QAction("Restart current Kernel",
540 540 self,
541 541 triggered=self.restart_kernel_active_frontend,
542 542 shortcut=ctrl+"+.",
543 543 )
544 544 self.add_menu_action(self.kernel_menu, self.restart_kernel_action)
545 545
546 546 self.kernel_menu.addSeparator()
547 547
548 548 def _make_dynamic_magic(self,magic):
549 549 """Return a function `fun` that will execute `magic` on active frontend.
550 550
551 551 Parameters
552 552 ----------
553 553 magic : string
554 554 string that will be executed as is when the returned function is called
555 555
556 556 Returns
557 557 -------
558 558 fun : function
559 559 function with no parameters, when called will execute `magic` on the
560 560 current active frontend at call time
561 561
562 562 See Also
563 563 --------
564 564 populate_all_magic_menu : generate the "All Magics..." menu
565 565
566 566 Notes
567 567 -----
568 568 `fun` execute `magic` an active frontend at the moment it is triggerd,
569 569 not the active frontend at the moment it has been created.
570 570
571 571 This function is mostly used to create the "All Magics..." Menu at run time.
572 572 """
573 573 # need to level nested function to be sure to past magic
574 574 # on active frontend **at run time**.
575 575 def inner_dynamic_magic():
576 576 self.active_frontend.execute(magic)
577 577 inner_dynamic_magic.__name__ = "dynamics_magic_s"
578 578 return inner_dynamic_magic
579 579
580 580 def populate_all_magic_menu(self, listofmagic=None):
581 581 """Clean "All Magics..." menu and repopulate it with `listofmagic`
582 582
583 583 Parameters
584 584 ----------
585 585 listofmagic : string,
586 586 repr() of a list of strings, send back by the kernel
587 587
588 588 Notes
589 589 -----
590 590 `listofmagic`is a repr() of list because it is fed with the result of
591 591 a 'user_expression'
592 592 """
593 593 alm_magic_menu = self.all_magic_menu
594 594 alm_magic_menu.clear()
595 595
596 596 # list of protected magic that don't like to be called without argument
597 597 # append '?' to the end to print the docstring when called from the menu
598 598 protected_magic = set(["more","less","load_ext","pycat","loadpy","save"])
599 599 magics=re.findall('\w+', listofmagic)
600 600 for magic in magics:
601 601 if magic in protected_magic:
602 602 pmagic = '%s%s%s'%('%',magic,'?')
603 603 else:
604 604 pmagic = '%s%s'%('%',magic)
605 605 xaction = QtGui.QAction(pmagic,
606 606 self,
607 607 triggered=self._make_dynamic_magic(pmagic)
608 608 )
609 609 alm_magic_menu.addAction(xaction)
610 610
611 611 def update_all_magic_menu(self):
612 612 """ Update the list on magic in the "All Magics..." Menu
613 613
614 614 Request the kernel with the list of availlable magic and populate the
615 615 menu with the list received back
616 616
617 617 """
618 618 # first define a callback which will get the list of all magic and put it in the menu.
619 619 self.active_frontend._silent_exec_callback('get_ipython().lsmagic()', self.populate_all_magic_menu)
620 620
621 621 def init_magic_menu(self):
622 622 self.magic_menu = self.menuBar().addMenu("&Magic")
623 623 self.all_magic_menu = self.magic_menu.addMenu("&All Magics")
624 624
625 625 # This action should usually not appear as it will be cleared when menu
626 626 # is updated at first kernel response. Though, it is necessary when
627 627 # connecting through X-forwarding, as in this case, the menu is not
628 628 # auto updated, SO DO NOT DELETE.
629
630 ########################################################################
631 ## TEMPORARILY DISABLED - see #1057 for details. Uncomment this
632 ## section when a proper fix is found
633
634 ## self.pop = QtGui.QAction("&Update All Magic Menu ",
635 ## self, triggered=self.update_all_magic_menu)
636 ## self.add_menu_action(self.all_magic_menu, self.pop)
637
638 ## END TEMPORARY FIX
639 ########################################################################
629 self.pop = QtGui.QAction("&Update All Magic Menu ",
630 self, triggered=self.update_all_magic_menu)
631 self.add_menu_action(self.all_magic_menu, self.pop)
632 # we need to populate the 'Magic Menu' once the kernel has answer at
633 # least once let's do it immedialy, but it's assured to works
634 self.pop.trigger()
640 635
641 636 self.reset_action = QtGui.QAction("&Reset",
642 637 self,
643 638 statusTip="Clear all varible from workspace",
644 639 triggered=self.reset_magic_active_frontend)
645 640 self.add_menu_action(self.magic_menu, self.reset_action)
646 641
647 642 self.history_action = QtGui.QAction("&History",
648 643 self,
649 644 statusTip="show command history",
650 645 triggered=self.history_magic_active_frontend)
651 646 self.add_menu_action(self.magic_menu, self.history_action)
652 647
653 648 self.save_action = QtGui.QAction("E&xport History ",
654 649 self,
655 650 statusTip="Export History as Python File",
656 651 triggered=self.save_magic_active_frontend)
657 652 self.add_menu_action(self.magic_menu, self.save_action)
658 653
659 654 self.who_action = QtGui.QAction("&Who",
660 655 self,
661 656 statusTip="List interactive variable",
662 657 triggered=self.who_magic_active_frontend)
663 658 self.add_menu_action(self.magic_menu, self.who_action)
664 659
665 660 self.who_ls_action = QtGui.QAction("Wh&o ls",
666 661 self,
667 662 statusTip="Return a list of interactive variable",
668 663 triggered=self.who_ls_magic_active_frontend)
669 664 self.add_menu_action(self.magic_menu, self.who_ls_action)
670 665
671 666 self.whos_action = QtGui.QAction("Who&s",
672 667 self,
673 668 statusTip="List interactive variable with detail",
674 669 triggered=self.whos_magic_active_frontend)
675 670 self.add_menu_action(self.magic_menu, self.whos_action)
676 671
677
678 ########################################################################
679 ## TEMPORARILY ADDED BACK - see #1057 for details. The magic menu is
680 ## supposed to be dynamic, but the mechanism merged in #1057 has a race
681 ## condition that locks up the console very often. We're putting back
682 ## the static list temporarily/ Remove this code when a proper fix is
683 ## found.
684
685 # allmagics submenu.
686
687 # for now this is just a copy and paste, but we should get this
688 # dynamically
689 magiclist = ["%alias", "%autocall", "%automagic", "%bookmark", "%cd",
690 "%clear", "%colors", "%debug", "%dhist", "%dirs", "%doctest_mode",
691 "%ed", "%edit", "%env", "%gui", "%guiref", "%hist", "%history",
692 "%install_default_config", "%install_profiles", "%less", "%load_ext",
693 "%loadpy", "%logoff", "%logon", "%logstart", "%logstate", "%logstop",
694 "%lsmagic", "%macro", "%magic", "%man", "%more", "%notebook", "%page",
695 "%pastebin", "%pdb", "%pdef", "%pdoc", "%pfile", "%pinfo", "%pinfo2",
696 "%popd", "%pprint", "%precision", "%profile", "%prun", "%psearch",
697 "%psource", "%pushd", "%pwd", "%pycat", "%pylab", "%quickref",
698 "%recall", "%rehashx", "%reload_ext", "%rep", "%rerun", "%reset",
699 "%reset_selective", "%run", "%save", "%sc", "%sx", "%tb", "%time",
700 "%timeit", "%unalias", "%unload_ext", "%who", "%who_ls", "%whos",
701 "%xdel", "%xmode"]
702
703 def make_dynamic_magic(i):
704 def inner_dynamic_magic():
705 self.active_frontend.execute(i)
706 inner_dynamic_magic.__name__ = "dynamics_magic_%s" % i
707 return inner_dynamic_magic
708
709 for magic in magiclist:
710 xaction = QtGui.QAction(magic,
711 self,
712 triggered=make_dynamic_magic(magic)
713 )
714 self.all_magic_menu.addAction(xaction)
715
716 ## END TEMPORARY FIX
717 ########################################################################
718
719
720 672 def init_window_menu(self):
721 673 self.window_menu = self.menuBar().addMenu("&Window")
722 674 if sys.platform == 'darwin':
723 675 # add min/maximize actions to OSX, which lacks default bindings.
724 676 self.minimizeAct = QtGui.QAction("Mini&mize",
725 677 self,
726 678 shortcut="Ctrl+m",
727 679 statusTip="Minimize the window/Restore Normal Size",
728 680 triggered=self.toggleMinimized)
729 681 # maximize is called 'Zoom' on OSX for some reason
730 682 self.maximizeAct = QtGui.QAction("&Zoom",
731 683 self,
732 684 shortcut="Ctrl+Shift+M",
733 685 statusTip="Maximize the window/Restore Normal Size",
734 686 triggered=self.toggleMaximized)
735 687
736 688 self.add_menu_action(self.window_menu, self.minimizeAct)
737 689 self.add_menu_action(self.window_menu, self.maximizeAct)
738 690 self.window_menu.addSeparator()
739 691
740 692 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
741 693 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
742 694 self,
743 695 shortcut=prev_key,
744 696 statusTip="Select previous tab",
745 697 triggered=self.prev_tab)
746 698 self.add_menu_action(self.window_menu, self.prev_tab_act)
747 699
748 700 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
749 701 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
750 702 self,
751 703 shortcut=next_key,
752 704 statusTip="Select next tab",
753 705 triggered=self.next_tab)
754 706 self.add_menu_action(self.window_menu, self.next_tab_act)
755 707
756 708 def init_help_menu(self):
757 709 # please keep the Help menu in Mac Os even if empty. It will
758 710 # automatically contain a search field to search inside menus and
759 711 # please keep it spelled in English, as long as Qt Doesn't support
760 712 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
761 713 # this search field fonctionality
762 714
763 715 self.help_menu = self.menuBar().addMenu("&Help")
764 716
765 717
766 718 # Help Menu
767 719
768 720 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
769 721 self,
770 722 triggered=self.intro_active_frontend
771 723 )
772 724 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
773 725
774 726 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
775 727 self,
776 728 triggered=self.quickref_active_frontend
777 729 )
778 730 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
779 731
780 732 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
781 733 self,
782 734 triggered=self.guiref_active_frontend
783 735 )
784 736 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
785 737
786 738 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
787 739 self,
788 740 triggered=self._open_online_help)
789 741 self.add_menu_action(self.help_menu, self.onlineHelpAct)
790 742
791 743 # minimize/maximize/fullscreen actions:
792 744
793 745 def toggle_menu_bar(self):
794 746 menu_bar = self.menuBar()
795 747 if menu_bar.isVisible():
796 748 menu_bar.setVisible(False)
797 749 else:
798 750 menu_bar.setVisible(True)
799 751
800 752 def toggleMinimized(self):
801 753 if not self.isMinimized():
802 754 self.showMinimized()
803 755 else:
804 756 self.showNormal()
805 757
806 758 def _open_online_help(self):
807 759 filename="http://ipython.org/ipython-doc/stable/index.html"
808 760 webbrowser.open(filename, new=1, autoraise=True)
809 761
810 762 def toggleMaximized(self):
811 763 if not self.isMaximized():
812 764 self.showMaximized()
813 765 else:
814 766 self.showNormal()
815 767
816 768 # Min/Max imizing while in full screen give a bug
817 769 # when going out of full screen, at least on OSX
818 770 def toggleFullScreen(self):
819 771 if not self.isFullScreen():
820 772 self.showFullScreen()
821 773 if sys.platform == 'darwin':
822 774 self.maximizeAct.setEnabled(False)
823 775 self.minimizeAct.setEnabled(False)
824 776 else:
825 777 self.showNormal()
826 778 if sys.platform == 'darwin':
827 779 self.maximizeAct.setEnabled(True)
828 780 self.minimizeAct.setEnabled(True)
829 781
830 782 def close_active_frontend(self):
831 783 self.close_tab(self.active_frontend)
832 784
833 785 def restart_kernel_active_frontend(self):
834 786 self.active_frontend.request_restart_kernel()
835 787
836 788 def interrupt_kernel_active_frontend(self):
837 789 self.active_frontend.request_interrupt_kernel()
838 790
839 791 def cut_active_frontend(self):
840 792 widget = self.active_frontend
841 793 if widget.can_cut():
842 794 widget.cut()
843 795
844 796 def copy_active_frontend(self):
845 797 widget = self.active_frontend
846 798 if widget.can_copy():
847 799 widget.copy()
848 800
849 801 def copy_raw_active_frontend(self):
850 802 self.active_frontend._copy_raw_action.trigger()
851 803
852 804 def paste_active_frontend(self):
853 805 widget = self.active_frontend
854 806 if widget.can_paste():
855 807 widget.paste()
856 808
857 809 def undo_active_frontend(self):
858 810 self.active_frontend.undo()
859 811
860 812 def redo_active_frontend(self):
861 813 self.active_frontend.redo()
862 814
863 815 def reset_magic_active_frontend(self):
864 816 self.active_frontend.execute("%reset")
865 817
866 818 def history_magic_active_frontend(self):
867 819 self.active_frontend.execute("%history")
868 820
869 821 def save_magic_active_frontend(self):
870 822 self.active_frontend.save_magic()
871 823
872 824 def clear_magic_active_frontend(self):
873 825 self.active_frontend.execute("%clear")
874 826
875 827 def who_magic_active_frontend(self):
876 828 self.active_frontend.execute("%who")
877 829
878 830 def who_ls_magic_active_frontend(self):
879 831 self.active_frontend.execute("%who_ls")
880 832
881 833 def whos_magic_active_frontend(self):
882 834 self.active_frontend.execute("%whos")
883 835
884 836 def print_action_active_frontend(self):
885 837 self.active_frontend.print_action.trigger()
886 838
887 839 def export_action_active_frontend(self):
888 840 self.active_frontend.export_action.trigger()
889 841
890 842 def select_all_active_frontend(self):
891 843 self.active_frontend.select_all_action.trigger()
892 844
893 845 def increase_font_size_active_frontend(self):
894 846 self.active_frontend.increase_font_size.trigger()
895 847
896 848 def decrease_font_size_active_frontend(self):
897 849 self.active_frontend.decrease_font_size.trigger()
898 850
899 851 def reset_font_size_active_frontend(self):
900 852 self.active_frontend.reset_font_size.trigger()
901 853
902 854 def guiref_active_frontend(self):
903 855 self.active_frontend.execute("%guiref")
904 856
905 857 def intro_active_frontend(self):
906 858 self.active_frontend.execute("?")
907 859
908 860 def quickref_active_frontend(self):
909 861 self.active_frontend.execute("%quickref")
910 862 #---------------------------------------------------------------------------
911 863 # QWidget interface
912 864 #---------------------------------------------------------------------------
913 865
914 866 def closeEvent(self, event):
915 867 """ Forward the close event to every tabs contained by the windows
916 868 """
917 869 if self.tab_widget.count() == 0:
918 870 # no tabs, just close
919 871 event.accept()
920 872 return
921 873 # Do Not loop on the widget count as it change while closing
922 874 title = self.window().windowTitle()
923 875 cancel = QtGui.QMessageBox.Cancel
924 876 okay = QtGui.QMessageBox.Ok
925 877
926 878 if self.confirm_exit:
927 879 if self.tab_widget.count() > 1:
928 880 msg = "Close all tabs, stop all kernels, and Quit?"
929 881 else:
930 882 msg = "Close console, stop kernel, and Quit?"
931 883 info = "Kernels not started here (e.g. notebooks) will be left alone."
932 884 closeall = QtGui.QPushButton("&Yes, quit everything", self)
933 885 closeall.setShortcut('Y')
934 886 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
935 887 title, msg)
936 888 box.setInformativeText(info)
937 889 box.addButton(cancel)
938 890 box.addButton(closeall, QtGui.QMessageBox.YesRole)
939 891 box.setDefaultButton(closeall)
940 892 box.setEscapeButton(cancel)
941 893 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
942 894 box.setIconPixmap(pixmap)
943 895 reply = box.exec_()
944 896 else:
945 897 reply = okay
946 898
947 899 if reply == cancel:
948 900 event.ignore()
949 901 return
950 902 if reply == okay:
951 903 while self.tab_widget.count() >= 1:
952 904 # prevent further confirmations:
953 905 widget = self.active_frontend
954 906 widget._confirm_exit = False
955 907 self.close_tab(widget)
956 908 event.accept()
957 909
@@ -1,559 +1,549 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14
15 15 """
16 16
17 17 #-----------------------------------------------------------------------------
18 18 # Imports
19 19 #-----------------------------------------------------------------------------
20 20
21 21 # stdlib imports
22 22 import json
23 23 import os
24 24 import signal
25 25 import sys
26 26 import uuid
27 27
28 28 # System library imports
29 29 from IPython.external.qt import QtGui
30 30
31 31 # Local imports
32 32 from IPython.config.application import boolean_flag, catch_config_error
33 33 from IPython.core.application import BaseIPythonApplication
34 34 from IPython.core.profiledir import ProfileDir
35 35 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 36 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 37 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 38 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 39 from IPython.frontend.qt.console import styles
40 40 from IPython.frontend.qt.console.mainwindow import MainWindow
41 41 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 42 from IPython.utils.path import filefind
43 43 from IPython.utils.py3compat import str_to_bytes
44 44 from IPython.utils.traitlets import (
45 45 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 46 )
47 47 from IPython.zmq.ipkernel import (
48 48 flags as ipkernel_flags,
49 49 aliases as ipkernel_aliases,
50 50 IPKernelApp
51 51 )
52 52 from IPython.zmq.session import Session, default_secure
53 53 from IPython.zmq.zmqshell import ZMQInteractiveShell
54 54
55 55 #-----------------------------------------------------------------------------
56 56 # Network Constants
57 57 #-----------------------------------------------------------------------------
58 58
59 59 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # Globals
63 63 #-----------------------------------------------------------------------------
64 64
65 65 _examples = """
66 66 ipython qtconsole # start the qtconsole
67 67 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 68 """
69 69
70 70 #-----------------------------------------------------------------------------
71 71 # Aliases and Flags
72 72 #-----------------------------------------------------------------------------
73 73
74 74 flags = dict(ipkernel_flags)
75 75 qt_flags = {
76 76 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
77 77 "Connect to an existing kernel. If no argument specified, guess most recent"),
78 78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 79 "Use a pure Python kernel instead of an IPython kernel."),
80 80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 81 "Disable rich text support."),
82 82 }
83 83 qt_flags.update(boolean_flag(
84 84 'gui-completion', 'ConsoleWidget.gui_completion',
85 85 "use a GUI widget for tab completion",
86 86 "use plaintext output for completion"
87 87 ))
88 88 qt_flags.update(boolean_flag(
89 89 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
90 90 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
91 91 to force a direct exit without any confirmation.
92 92 """,
93 93 """Don't prompt the user when exiting. This will terminate the kernel
94 94 if it is owned by the frontend, and leave it alive if it is external.
95 95 """
96 96 ))
97 97 flags.update(qt_flags)
98 98
99 99 aliases = dict(ipkernel_aliases)
100 100
101 101 qt_aliases = dict(
102 102 hb = 'IPythonQtConsoleApp.hb_port',
103 103 shell = 'IPythonQtConsoleApp.shell_port',
104 104 iopub = 'IPythonQtConsoleApp.iopub_port',
105 105 stdin = 'IPythonQtConsoleApp.stdin_port',
106 106 ip = 'IPythonQtConsoleApp.ip',
107 107 existing = 'IPythonQtConsoleApp.existing',
108 108 f = 'IPythonQtConsoleApp.connection_file',
109 109
110 110 style = 'IPythonWidget.syntax_style',
111 111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 112 colors = 'ZMQInteractiveShell.colors',
113 113
114 114 editor = 'IPythonWidget.editor',
115 115 paging = 'ConsoleWidget.paging',
116 116 ssh = 'IPythonQtConsoleApp.sshserver',
117 117 )
118 118 aliases.update(qt_aliases)
119 119
120 120 #-----------------------------------------------------------------------------
121 121 # Classes
122 122 #-----------------------------------------------------------------------------
123 123
124 124 #-----------------------------------------------------------------------------
125 125 # IPythonQtConsole
126 126 #-----------------------------------------------------------------------------
127 127
128 128
129 129 class IPythonQtConsoleApp(BaseIPythonApplication):
130 130 name = 'ipython-qtconsole'
131 131 default_config_file_name='ipython_config.py'
132 132
133 133 description = """
134 134 The IPython QtConsole.
135 135
136 136 This launches a Console-style application using Qt. It is not a full
137 137 console, in that launched terminal subprocesses will not be able to accept
138 138 input.
139 139
140 140 The QtConsole supports various extra features beyond the Terminal IPython
141 141 shell, such as inline plotting with matplotlib, via:
142 142
143 143 ipython qtconsole --pylab=inline
144 144
145 145 as well as saving your session as HTML, and printing the output.
146 146
147 147 """
148 148 examples = _examples
149 149
150 150 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
151 151 flags = Dict(flags)
152 152 aliases = Dict(aliases)
153 153
154 154 kernel_argv = List(Unicode)
155 155
156 156 # create requested profiles by default, if they don't exist:
157 157 auto_create = CBool(True)
158 158 # connection info:
159 159 ip = Unicode(LOCALHOST, config=True,
160 160 help="""Set the kernel\'s IP address [default localhost].
161 161 If the IP address is something other than localhost, then
162 162 Consoles on other machines will be able to connect
163 163 to the Kernel, so be careful!"""
164 164 )
165 165
166 166 sshserver = Unicode('', config=True,
167 167 help="""The SSH server to use to connect to the kernel.""")
168 168 sshkey = Unicode('', config=True,
169 169 help="""Path to the ssh key to use for logging in to the ssh server.""")
170 170
171 171 hb_port = Integer(0, config=True,
172 172 help="set the heartbeat port [default: random]")
173 173 shell_port = Integer(0, config=True,
174 174 help="set the shell (XREP) port [default: random]")
175 175 iopub_port = Integer(0, config=True,
176 176 help="set the iopub (PUB) port [default: random]")
177 177 stdin_port = Integer(0, config=True,
178 178 help="set the stdin (XREQ) port [default: random]")
179 179 connection_file = Unicode('', config=True,
180 180 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
181 181
182 182 This file will contain the IP, ports, and authentication key needed to connect
183 183 clients to this kernel. By default, this file will be created in the security-dir
184 184 of the current profile, but can be specified by absolute path.
185 185 """)
186 186 def _connection_file_default(self):
187 187 return 'kernel-%i.json' % os.getpid()
188 188
189 189 existing = Unicode('', config=True,
190 190 help="""Connect to an already running kernel""")
191 191
192 192 stylesheet = Unicode('', config=True,
193 193 help="path to a custom CSS stylesheet")
194 194
195 195 pure = CBool(False, config=True,
196 196 help="Use a pure Python kernel instead of an IPython kernel.")
197 197 plain = CBool(False, config=True,
198 198 help="Use a plaintext widget instead of rich text (plain can't print/save).")
199 199
200 200 def _pure_changed(self, name, old, new):
201 201 kind = 'plain' if self.plain else 'rich'
202 202 self.config.ConsoleWidget.kind = kind
203 203 if self.pure:
204 204 self.widget_factory = FrontendWidget
205 205 elif self.plain:
206 206 self.widget_factory = IPythonWidget
207 207 else:
208 208 self.widget_factory = RichIPythonWidget
209 209
210 210 _plain_changed = _pure_changed
211 211
212 212 confirm_exit = CBool(True, config=True,
213 213 help="""
214 214 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
215 215 to force a direct exit without any confirmation.""",
216 216 )
217 217
218 218 # the factory for creating a widget
219 219 widget_factory = Any(RichIPythonWidget)
220 220
221 221 def parse_command_line(self, argv=None):
222 222 super(IPythonQtConsoleApp, self).parse_command_line(argv)
223 223 if argv is None:
224 224 argv = sys.argv[1:]
225 225 self.kernel_argv = list(argv) # copy
226 226 # kernel should inherit default config file from frontend
227 227 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
228 228 # Scrub frontend-specific flags
229 229 swallow_next = False
230 230 was_flag = False
231 231 # copy again, in case some aliases have the same name as a flag
232 232 # argv = list(self.kernel_argv)
233 233 for a in argv:
234 234 if swallow_next:
235 235 swallow_next = False
236 236 # last arg was an alias, remove the next one
237 237 # *unless* the last alias has a no-arg flag version, in which
238 238 # case, don't swallow the next arg if it's also a flag:
239 239 if not (was_flag and a.startswith('-')):
240 240 self.kernel_argv.remove(a)
241 241 continue
242 242 if a.startswith('-'):
243 243 split = a.lstrip('-').split('=')
244 244 alias = split[0]
245 245 if alias in qt_aliases:
246 246 self.kernel_argv.remove(a)
247 247 if len(split) == 1:
248 248 # alias passed with arg via space
249 249 swallow_next = True
250 250 # could have been a flag that matches an alias, e.g. `existing`
251 251 # in which case, we might not swallow the next arg
252 252 was_flag = alias in qt_flags
253 253 elif alias in qt_flags:
254 254 # strip flag, but don't swallow next, as flags don't take args
255 255 self.kernel_argv.remove(a)
256 256
257 257 def init_connection_file(self):
258 258 """find the connection file, and load the info if found.
259 259
260 260 The current working directory and the current profile's security
261 261 directory will be searched for the file if it is not given by
262 262 absolute path.
263 263
264 264 When attempting to connect to an existing kernel and the `--existing`
265 265 argument does not match an existing file, it will be interpreted as a
266 266 fileglob, and the matching file in the current profile's security dir
267 267 with the latest access time will be used.
268 268 """
269 269 if self.existing:
270 270 try:
271 271 cf = find_connection_file(self.existing)
272 272 except Exception:
273 273 self.log.critical("Could not find existing kernel connection file %s", self.existing)
274 274 self.exit(1)
275 275 self.log.info("Connecting to existing kernel: %s" % cf)
276 276 self.connection_file = cf
277 277 # should load_connection_file only be used for existing?
278 278 # as it is now, this allows reusing ports if an existing
279 279 # file is requested
280 280 try:
281 281 self.load_connection_file()
282 282 except Exception:
283 283 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
284 284 self.exit(1)
285 285
286 286 def load_connection_file(self):
287 287 """load ip/port/hmac config from JSON connection file"""
288 288 # this is identical to KernelApp.load_connection_file
289 289 # perhaps it can be centralized somewhere?
290 290 try:
291 291 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
292 292 except IOError:
293 293 self.log.debug("Connection File not found: %s", self.connection_file)
294 294 return
295 295 self.log.debug(u"Loading connection file %s", fname)
296 296 with open(fname) as f:
297 297 s = f.read()
298 298 cfg = json.loads(s)
299 299 if self.ip == LOCALHOST and 'ip' in cfg:
300 300 # not overridden by config or cl_args
301 301 self.ip = cfg['ip']
302 302 for channel in ('hb', 'shell', 'iopub', 'stdin'):
303 303 name = channel + '_port'
304 304 if getattr(self, name) == 0 and name in cfg:
305 305 # not overridden by config or cl_args
306 306 setattr(self, name, cfg[name])
307 307 if 'key' in cfg:
308 308 self.config.Session.key = str_to_bytes(cfg['key'])
309 309
310 310 def init_ssh(self):
311 311 """set up ssh tunnels, if needed."""
312 312 if not self.sshserver and not self.sshkey:
313 313 return
314 314
315 315 if self.sshkey and not self.sshserver:
316 316 # specifying just the key implies that we are connecting directly
317 317 self.sshserver = self.ip
318 318 self.ip = LOCALHOST
319 319
320 320 # build connection dict for tunnels:
321 321 info = dict(ip=self.ip,
322 322 shell_port=self.shell_port,
323 323 iopub_port=self.iopub_port,
324 324 stdin_port=self.stdin_port,
325 325 hb_port=self.hb_port
326 326 )
327 327
328 328 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
329 329
330 330 # tunnels return a new set of ports, which will be on localhost:
331 331 self.ip = LOCALHOST
332 332 try:
333 333 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
334 334 except:
335 335 # even catch KeyboardInterrupt
336 336 self.log.error("Could not setup tunnels", exc_info=True)
337 337 self.exit(1)
338 338
339 339 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
340 340
341 341 cf = self.connection_file
342 342 base,ext = os.path.splitext(cf)
343 343 base = os.path.basename(base)
344 344 self.connection_file = os.path.basename(base)+'-ssh'+ext
345 345 self.log.critical("To connect another client via this tunnel, use:")
346 346 self.log.critical("--existing %s" % self.connection_file)
347 347
348 348 def _new_connection_file(self):
349 349 return os.path.join(self.profile_dir.security_dir, 'kernel-%s.json' % uuid.uuid4())
350 350
351 351 def init_kernel_manager(self):
352 352 # Don't let Qt or ZMQ swallow KeyboardInterupts.
353 353 signal.signal(signal.SIGINT, signal.SIG_DFL)
354 354 sec = self.profile_dir.security_dir
355 355 try:
356 356 cf = filefind(self.connection_file, ['.', sec])
357 357 except IOError:
358 358 # file might not exist
359 359 if self.connection_file == os.path.basename(self.connection_file):
360 360 # just shortname, put it in security dir
361 361 cf = os.path.join(sec, self.connection_file)
362 362 else:
363 363 cf = self.connection_file
364 364
365 365 # Create a KernelManager and start a kernel.
366 366 self.kernel_manager = QtKernelManager(
367 367 ip=self.ip,
368 368 shell_port=self.shell_port,
369 369 iopub_port=self.iopub_port,
370 370 stdin_port=self.stdin_port,
371 371 hb_port=self.hb_port,
372 372 connection_file=cf,
373 373 config=self.config,
374 374 )
375 375 # start the kernel
376 376 if not self.existing:
377 377 kwargs = dict(ipython=not self.pure)
378 378 kwargs['extra_arguments'] = self.kernel_argv
379 379 self.kernel_manager.start_kernel(**kwargs)
380 380 elif self.sshserver:
381 381 # ssh, write new connection file
382 382 self.kernel_manager.write_connection_file()
383 383 self.kernel_manager.start_channels()
384 384
385 385 def new_frontend_master(self):
386 386 """ Create and return new frontend attached to new kernel, launched on localhost.
387 387 """
388 388 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
389 389 kernel_manager = QtKernelManager(
390 390 ip=ip,
391 391 connection_file=self._new_connection_file(),
392 392 config=self.config,
393 393 )
394 394 # start the kernel
395 395 kwargs = dict(ipython=not self.pure)
396 396 kwargs['extra_arguments'] = self.kernel_argv
397 397 kernel_manager.start_kernel(**kwargs)
398 398 kernel_manager.start_channels()
399 399 widget = self.widget_factory(config=self.config,
400 400 local_kernel=True)
401 401 widget.kernel_manager = kernel_manager
402 402 widget._existing = False
403 403 widget._may_close = True
404 404 widget._confirm_exit = self.confirm_exit
405 405 return widget
406 406
407 407 def new_frontend_slave(self, current_widget):
408 408 """Create and return a new frontend attached to an existing kernel.
409 409
410 410 Parameters
411 411 ----------
412 412 current_widget : IPythonWidget
413 413 The IPythonWidget whose kernel this frontend is to share
414 414 """
415 415 kernel_manager = QtKernelManager(
416 416 connection_file=current_widget.kernel_manager.connection_file,
417 417 config = self.config,
418 418 )
419 419 kernel_manager.load_connection_file()
420 420 kernel_manager.start_channels()
421 421 widget = self.widget_factory(config=self.config,
422 422 local_kernel=False)
423 423 widget._existing = True
424 424 widget._may_close = False
425 425 widget._confirm_exit = False
426 426 widget.kernel_manager = kernel_manager
427 427 return widget
428 428
429 429 def init_qt_elements(self):
430 430 # Create the widget.
431 431 self.app = QtGui.QApplication([])
432 432
433 433 base_path = os.path.abspath(os.path.dirname(__file__))
434 434 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
435 435 self.app.icon = QtGui.QIcon(icon_path)
436 436 QtGui.QApplication.setWindowIcon(self.app.icon)
437 437
438 438 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
439 439 self.widget = self.widget_factory(config=self.config,
440 440 local_kernel=local_kernel)
441 441 self.widget._existing = self.existing
442 442 self.widget._may_close = not self.existing
443 443 self.widget._confirm_exit = self.confirm_exit
444 444
445 445 self.widget.kernel_manager = self.kernel_manager
446 446 self.window = MainWindow(self.app,
447 447 confirm_exit=self.confirm_exit,
448 448 new_frontend_factory=self.new_frontend_master,
449 449 slave_frontend_factory=self.new_frontend_slave,
450 450 )
451 451 self.window.log = self.log
452 452 self.window.add_tab_with_frontend(self.widget)
453 453 self.window.init_menu_bar()
454 454
455 # we need to populate the 'Magic Menu' once the kernel has answer at
456 # least once
457
458 ########################################################################
459 ## TEMPORARILY DISABLED - see #1057 for details, uncomment the next
460 ## line when a proper fix is found:
461 ## self.kernel_manager.shell_channel.first_reply.connect(self.window.pop.trigger)
462 ## END TEMPORARY FIX
463 ########################################################################
464
465 455 self.window.setWindowTitle('Python' if self.pure else 'IPython')
466 456
467 457 def init_colors(self):
468 458 """Configure the coloring of the widget"""
469 459 # Note: This will be dramatically simplified when colors
470 460 # are removed from the backend.
471 461
472 462 if self.pure:
473 463 # only IPythonWidget supports styling
474 464 return
475 465
476 466 # parse the colors arg down to current known labels
477 467 try:
478 468 colors = self.config.ZMQInteractiveShell.colors
479 469 except AttributeError:
480 470 colors = None
481 471 try:
482 472 style = self.config.IPythonWidget.syntax_style
483 473 except AttributeError:
484 474 style = None
485 475
486 476 # find the value for colors:
487 477 if colors:
488 478 colors=colors.lower()
489 479 if colors in ('lightbg', 'light'):
490 480 colors='lightbg'
491 481 elif colors in ('dark', 'linux'):
492 482 colors='linux'
493 483 else:
494 484 colors='nocolor'
495 485 elif style:
496 486 if style=='bw':
497 487 colors='nocolor'
498 488 elif styles.dark_style(style):
499 489 colors='linux'
500 490 else:
501 491 colors='lightbg'
502 492 else:
503 493 colors=None
504 494
505 495 # Configure the style.
506 496 widget = self.widget
507 497 if style:
508 498 widget.style_sheet = styles.sheet_from_template(style, colors)
509 499 widget.syntax_style = style
510 500 widget._syntax_style_changed()
511 501 widget._style_sheet_changed()
512 502 elif colors:
513 503 # use a default style
514 504 widget.set_default_style(colors=colors)
515 505 else:
516 506 # this is redundant for now, but allows the widget's
517 507 # defaults to change
518 508 widget.set_default_style()
519 509
520 510 if self.stylesheet:
521 511 # we got an expicit stylesheet
522 512 if os.path.isfile(self.stylesheet):
523 513 with open(self.stylesheet) as f:
524 514 sheet = f.read()
525 515 widget.style_sheet = sheet
526 516 widget._style_sheet_changed()
527 517 else:
528 518 raise IOError("Stylesheet %r not found."%self.stylesheet)
529 519
530 520 @catch_config_error
531 521 def initialize(self, argv=None):
532 522 super(IPythonQtConsoleApp, self).initialize(argv)
533 523 self.init_connection_file()
534 524 default_secure(self.config)
535 525 self.init_ssh()
536 526 self.init_kernel_manager()
537 527 self.init_qt_elements()
538 528 self.init_colors()
539 529
540 530 def start(self):
541 531
542 532 # draw the window
543 533 self.window.show()
544 534
545 535 # Start the application main loop.
546 536 self.app.exec_()
547 537
548 538 #-----------------------------------------------------------------------------
549 539 # Main entry point
550 540 #-----------------------------------------------------------------------------
551 541
552 542 def main():
553 543 app = IPythonQtConsoleApp()
554 544 app.initialize()
555 545 app.start()
556 546
557 547
558 548 if __name__ == '__main__':
559 549 main()
General Comments 0
You need to be logged in to leave comments. Login now