##// END OF EJS Templates
tweaked close dialog and added prompts to prevent silent remote close
MinRK -
Show More
@@ -1,568 +1,590 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
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from PyQt4 import QtCore, QtGui
11 11
12 12 # Local imports
13 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 14 from IPython.core.oinspect import call_tip
15 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 16 from IPython.utils.traitlets import Bool
17 17 from bracket_matcher import BracketMatcher
18 18 from call_tip_widget import CallTipWidget
19 19 from completion_lexer import CompletionLexer
20 20 from history_console_widget import HistoryConsoleWidget
21 21 from pygments_highlighter import PygmentsHighlighter
22 22
23 23
24 24 class FrontendHighlighter(PygmentsHighlighter):
25 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 26 prompts.
27 27 """
28 28
29 29 def __init__(self, frontend):
30 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 31 self._current_offset = 0
32 32 self._frontend = frontend
33 33 self.highlighting_on = False
34 34
35 35 def highlightBlock(self, qstring):
36 36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 37 """
38 38 if not self.highlighting_on:
39 39 return
40 40
41 41 # The input to this function is unicode string that may contain
42 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 43 # the string as plain text so we can compare it.
44 44 current_block = self.currentBlock()
45 45 string = self._frontend._get_block_plain_text(current_block)
46 46
47 47 # Decide whether to check for the regular or continuation prompt.
48 48 if current_block.contains(self._frontend._prompt_pos):
49 49 prompt = self._frontend._prompt
50 50 else:
51 51 prompt = self._frontend._continuation_prompt
52 52
53 53 # Don't highlight the part of the string that contains the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 56 qstring.remove(0, len(prompt))
57 57 else:
58 58 self._current_offset = 0
59 59
60 60 PygmentsHighlighter.highlightBlock(self, qstring)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 PygmentsHighlighter.setFormat(self, start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # An option and corresponding signal for overriding the default kernel
82 82 # interrupt behavior.
83 83 custom_interrupt = Bool(False)
84 84 custom_interrupt_requested = QtCore.pyqtSignal()
85 85
86 86 # An option and corresponding signals for overriding the default kernel
87 87 # restart behavior.
88 88 custom_restart = Bool(False)
89 89 custom_restart_kernel_died = QtCore.pyqtSignal(float)
90 90 custom_restart_requested = QtCore.pyqtSignal()
91 91
92 92 # Emitted when an 'execute_reply' has been received from the kernel and
93 93 # processed by the FrontendWidget.
94 94 executed = QtCore.pyqtSignal(object)
95 95
96 96 # Emitted when an exit request has been received from the kernel.
97 97 exit_requested = QtCore.pyqtSignal()
98 98
99 99 # Protected class variables.
100 100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 103 _input_splitter_class = InputSplitter
104 _local_kernel = False
104 105
105 106 #---------------------------------------------------------------------------
106 107 # 'object' interface
107 108 #---------------------------------------------------------------------------
108 109
109 110 def __init__(self, *args, **kw):
110 111 super(FrontendWidget, self).__init__(*args, **kw)
111 112
112 113 # FrontendWidget protected variables.
113 114 self._bracket_matcher = BracketMatcher(self._control)
114 115 self._call_tip_widget = CallTipWidget(self._control)
115 116 self._completion_lexer = CompletionLexer(PythonLexer())
116 117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
117 118 self._hidden = False
118 119 self._highlighter = FrontendHighlighter(self)
119 120 self._input_splitter = self._input_splitter_class(input_mode='cell')
120 121 self._kernel_manager = None
121 122 self._request_info = {}
122 123
123 124 # Configure the ConsoleWidget.
124 125 self.tab_width = 4
125 126 self._set_continuation_prompt('... ')
126 127
127 128 # Configure the CallTipWidget.
128 129 self._call_tip_widget.setFont(self.font)
129 130 self.font_changed.connect(self._call_tip_widget.setFont)
130 131
131 132 # Configure actions.
132 133 action = self._copy_raw_action
133 134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
134 135 action.setEnabled(False)
135 136 action.setShortcut(QtGui.QKeySequence(key))
136 137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
137 138 action.triggered.connect(self.copy_raw)
138 139 self.copy_available.connect(action.setEnabled)
139 140 self.addAction(action)
140 141
141 142 # Connect signal handlers.
142 143 document = self._control.document()
143 144 document.contentsChange.connect(self._document_contents_change)
145
146 # set flag for whether we are connected via localhost
147 self._local_kernel = kw.get('local_kernel', False)
144 148
145 149 #---------------------------------------------------------------------------
146 150 # 'ConsoleWidget' public interface
147 151 #---------------------------------------------------------------------------
148 152
149 153 def copy(self):
150 154 """ Copy the currently selected text to the clipboard, removing prompts.
151 155 """
152 156 text = unicode(self._control.textCursor().selection().toPlainText())
153 157 if text:
154 158 lines = map(transform_classic_prompt, text.splitlines())
155 159 text = '\n'.join(lines)
156 160 QtGui.QApplication.clipboard().setText(text)
157 161
158 162 #---------------------------------------------------------------------------
159 163 # 'ConsoleWidget' abstract interface
160 164 #---------------------------------------------------------------------------
161 165
162 166 def _is_complete(self, source, interactive):
163 167 """ Returns whether 'source' can be completely processed and a new
164 168 prompt created. When triggered by an Enter/Return key press,
165 169 'interactive' is True; otherwise, it is False.
166 170 """
167 171 complete = self._input_splitter.push(source)
168 172 if interactive:
169 173 complete = not self._input_splitter.push_accepts_more()
170 174 return complete
171 175
172 176 def _execute(self, source, hidden):
173 177 """ Execute 'source'. If 'hidden', do not show any output.
174 178
175 179 See parent class :meth:`execute` docstring for full details.
176 180 """
177 181 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
178 182 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
179 183 self._hidden = hidden
180 184
181 185 def _prompt_started_hook(self):
182 186 """ Called immediately after a new prompt is displayed.
183 187 """
184 188 if not self._reading:
185 189 self._highlighter.highlighting_on = True
186 190
187 191 def _prompt_finished_hook(self):
188 192 """ Called immediately after a prompt is finished, i.e. when some input
189 193 will be processed and a new prompt displayed.
190 194 """
191 195 if not self._reading:
192 196 self._highlighter.highlighting_on = False
193 197
194 198 def _tab_pressed(self):
195 199 """ Called when the tab key is pressed. Returns whether to continue
196 200 processing the event.
197 201 """
198 202 # Perform tab completion if:
199 203 # 1) The cursor is in the input buffer.
200 204 # 2) There is a non-whitespace character before the cursor.
201 205 text = self._get_input_buffer_cursor_line()
202 206 if text is None:
203 207 return False
204 208 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
205 209 if complete:
206 210 self._complete()
207 211 return not complete
208 212
209 213 #---------------------------------------------------------------------------
210 214 # 'ConsoleWidget' protected interface
211 215 #---------------------------------------------------------------------------
212 216
213 217 def _context_menu_make(self, pos):
214 218 """ Reimplemented to add an action for raw copy.
215 219 """
216 220 menu = super(FrontendWidget, self)._context_menu_make(pos)
217 221 for before_action in menu.actions():
218 222 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
219 223 QtGui.QKeySequence.ExactMatch:
220 224 menu.insertAction(before_action, self._copy_raw_action)
221 225 break
222 226 return menu
223 227
224 228 def _event_filter_console_keypress(self, event):
225 229 """ Reimplemented for execution interruption and smart backspace.
226 230 """
227 231 key = event.key()
228 232 if self._control_key_down(event.modifiers(), include_command=False):
229 233
230 234 if key == QtCore.Qt.Key_C and self._executing:
231 235 self.interrupt_kernel()
232 236 return True
233 237
234 238 elif key == QtCore.Qt.Key_Period:
235 239 message = 'Are you sure you want to restart the kernel?'
236 240 self.restart_kernel(message, now=False)
237 241 return True
238 242
239 243 elif not event.modifiers() & QtCore.Qt.AltModifier:
240 244
241 245 # Smart backspace: remove four characters in one backspace if:
242 246 # 1) everything left of the cursor is whitespace
243 247 # 2) the four characters immediately left of the cursor are spaces
244 248 if key == QtCore.Qt.Key_Backspace:
245 249 col = self._get_input_buffer_cursor_column()
246 250 cursor = self._control.textCursor()
247 251 if col > 3 and not cursor.hasSelection():
248 252 text = self._get_input_buffer_cursor_line()[:col]
249 253 if text.endswith(' ') and not text.strip():
250 254 cursor.movePosition(QtGui.QTextCursor.Left,
251 255 QtGui.QTextCursor.KeepAnchor, 4)
252 256 cursor.removeSelectedText()
253 257 return True
254 258
255 259 return super(FrontendWidget, self)._event_filter_console_keypress(event)
256 260
257 261 def _insert_continuation_prompt(self, cursor):
258 262 """ Reimplemented for auto-indentation.
259 263 """
260 264 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
261 265 cursor.insertText(' ' * self._input_splitter.indent_spaces)
262 266
263 267 #---------------------------------------------------------------------------
264 268 # 'BaseFrontendMixin' abstract interface
265 269 #---------------------------------------------------------------------------
266 270
267 271 def _handle_complete_reply(self, rep):
268 272 """ Handle replies for tab completion.
269 273 """
270 274 cursor = self._get_cursor()
271 275 info = self._request_info.get('complete')
272 276 if info and info.id == rep['parent_header']['msg_id'] and \
273 277 info.pos == cursor.position():
274 278 text = '.'.join(self._get_context())
275 279 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
276 280 self._complete_with_items(cursor, rep['content']['matches'])
277 281
278 282 def _handle_execute_reply(self, msg):
279 283 """ Handles replies for code execution.
280 284 """
281 285 info = self._request_info.get('execute')
282 286 if info and info.id == msg['parent_header']['msg_id'] and \
283 287 info.kind == 'user' and not self._hidden:
284 288 # Make sure that all output from the SUB channel has been processed
285 289 # before writing a new prompt.
286 290 self.kernel_manager.sub_channel.flush()
287 291
288 292 # Reset the ANSI style information to prevent bad text in stdout
289 293 # from messing up our colors. We're not a true terminal so we're
290 294 # allowed to do this.
291 295 if self.ansi_codes:
292 296 self._ansi_processor.reset_sgr()
293 297
294 298 content = msg['content']
295 299 status = content['status']
296 300 if status == 'ok':
297 301 self._process_execute_ok(msg)
298 302 elif status == 'error':
299 303 self._process_execute_error(msg)
300 304 elif status == 'abort':
301 305 self._process_execute_abort(msg)
302 306
303 307 self._show_interpreter_prompt_for_reply(msg)
304 308 self.executed.emit(msg)
305 309
306 310 def _handle_input_request(self, msg):
307 311 """ Handle requests for raw_input.
308 312 """
309 313 if self._hidden:
310 314 raise RuntimeError('Request for raw input during hidden execution.')
311 315
312 316 # Make sure that all output from the SUB channel has been processed
313 317 # before entering readline mode.
314 318 self.kernel_manager.sub_channel.flush()
315 319
316 320 def callback(line):
317 321 self.kernel_manager.rep_channel.input(line)
318 322 self._readline(msg['content']['prompt'], callback=callback)
319 323
320 324 def _handle_kernel_died(self, since_last_heartbeat):
321 325 """ Handle the kernel's death by asking if the user wants to restart.
322 326 """
323 327 if self.custom_restart:
324 328 self.custom_restart_kernel_died.emit(since_last_heartbeat)
325 329 else:
326 330 message = 'The kernel heartbeat has been inactive for %.2f ' \
327 331 'seconds. Do you want to restart the kernel? You may ' \
328 332 'first want to check the network connection.' % \
329 333 since_last_heartbeat
330 334 self.restart_kernel(message, now=True)
331 335
332 336 def _handle_object_info_reply(self, rep):
333 337 """ Handle replies for call tips.
334 338 """
335 339 cursor = self._get_cursor()
336 340 info = self._request_info.get('call_tip')
337 341 if info and info.id == rep['parent_header']['msg_id'] and \
338 342 info.pos == cursor.position():
339 343 # Get the information for a call tip. For now we format the call
340 344 # line as string, later we can pass False to format_call and
341 345 # syntax-highlight it ourselves for nicer formatting in the
342 346 # calltip.
343 347 call_info, doc = call_tip(rep['content'], format_call=True)
344 348 if call_info or doc:
345 349 self._call_tip_widget.show_call_info(call_info, doc)
346 350
347 351 def _handle_pyout(self, msg):
348 352 """ Handle display hook output.
349 353 """
350 354 if not self._hidden and self._is_from_this_session(msg):
351 355 self._append_plain_text(msg['content']['data'] + '\n')
352 356
353 357 def _handle_stream(self, msg):
354 358 """ Handle stdout, stderr, and stdin.
355 359 """
356 360 if not self._hidden and self._is_from_this_session(msg):
357 361 # Most consoles treat tabs as being 8 space characters. Convert tabs
358 362 # to spaces so that output looks as expected regardless of this
359 363 # widget's tab width.
360 364 text = msg['content']['data'].expandtabs(8)
361 365
362 366 self._append_plain_text(text)
363 367 self._control.moveCursor(QtGui.QTextCursor.End)
364 368
365 369 def _handle_shutdown_reply(self, msg):
366 370 """ Handle shutdown signal, only if from other console.
367 371 """
368 372 if not self._hidden and not self._is_from_this_session(msg):
369 if not msg['content']['restart']:
370 sys.exit(0)
371 else:
372 # we just got notified of a restart!
373 time.sleep(0.25) # wait 1/4 sec to reset
374 # lest the request for a new prompt
375 # goes to the old kernel
376 self.reset()
373 if self._local_kernel:
374 if not msg['content']['restart']:
375 sys.exit(0)
376 else:
377 # we just got notified of a restart!
378 time.sleep(0.25) # wait 1/4 sec to reset
379 # lest the request for a new prompt
380 # goes to the old kernel
381 self.reset()
382 else: # remote kernel, prompt on Kernel shutdown/reset
383 title = self.window().windowTitle()
384 if not msg['content']['restart']:
385 reply = QtGui.QMessageBox.question(self, title,
386 "Kernel has been shutdown permanently. Close the Console?",
387 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
388 if reply == QtGui.QMessageBox.Yes:
389 sys.exit(0)
390 else:
391 reply = QtGui.QMessageBox.question(self, title,
392 "Kernel has been reset. Clear the Console?",
393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
394 if reply == QtGui.QMessageBox.Yes:
395 time.sleep(0.25) # wait 1/4 sec to reset
396 # lest the request for a new prompt
397 # goes to the old kernel
398 self.reset()
377 399
378 400 def _started_channels(self):
379 401 """ Called when the KernelManager channels have started listening or
380 402 when the frontend is assigned an already listening KernelManager.
381 403 """
382 404 self.reset()
383 405
384 406 #---------------------------------------------------------------------------
385 407 # 'FrontendWidget' public interface
386 408 #---------------------------------------------------------------------------
387 409
388 410 def copy_raw(self):
389 411 """ Copy the currently selected text to the clipboard without attempting
390 412 to remove prompts or otherwise alter the text.
391 413 """
392 414 self._control.copy()
393 415
394 416 def execute_file(self, path, hidden=False):
395 417 """ Attempts to execute file with 'path'. If 'hidden', no output is
396 418 shown.
397 419 """
398 420 self.execute('execfile("%s")' % path, hidden=hidden)
399 421
400 422 def interrupt_kernel(self):
401 423 """ Attempts to interrupt the running kernel.
402 424 """
403 425 if self.custom_interrupt:
404 426 self.custom_interrupt_requested.emit()
405 427 elif self.kernel_manager.has_kernel:
406 428 self.kernel_manager.interrupt_kernel()
407 429 else:
408 430 self._append_plain_text('Kernel process is either remote or '
409 431 'unspecified. Cannot interrupt.\n')
410 432
411 433 def reset(self):
412 434 """ Resets the widget to its initial state. Similar to ``clear``, but
413 435 also re-writes the banner and aborts execution if necessary.
414 436 """
415 437 if self._executing:
416 438 self._executing = False
417 439 self._request_info['execute'] = None
418 440 self._reading = False
419 441 self._highlighter.highlighting_on = False
420 442
421 443 self._control.clear()
422 444 self._append_plain_text(self._get_banner())
423 445 self._show_interpreter_prompt()
424 446
425 447 def restart_kernel(self, message, now=False):
426 448 """ Attempts to restart the running kernel.
427 449 """
428 450 # FIXME: now should be configurable via a checkbox in the dialog. Right
429 451 # now at least the heartbeat path sets it to True and the manual restart
430 452 # to False. But those should just be the pre-selected states of a
431 453 # checkbox that the user could override if so desired. But I don't know
432 454 # enough Qt to go implementing the checkbox now.
433 455
434 456 if self.custom_restart:
435 457 self.custom_restart_requested.emit()
436 458
437 459 elif self.kernel_manager.has_kernel:
438 460 # Pause the heart beat channel to prevent further warnings.
439 461 self.kernel_manager.hb_channel.pause()
440 462
441 463 # Prompt the user to restart the kernel. Un-pause the heartbeat if
442 464 # they decline. (If they accept, the heartbeat will be un-paused
443 465 # automatically when the kernel is restarted.)
444 466 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
445 467 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
446 468 message, buttons)
447 469 if result == QtGui.QMessageBox.Yes:
448 470 try:
449 471 self.kernel_manager.restart_kernel(now=now)
450 472 except RuntimeError:
451 473 self._append_plain_text('Kernel started externally. '
452 474 'Cannot restart.\n')
453 475 else:
454 476 self.reset()
455 477 else:
456 478 self.kernel_manager.hb_channel.unpause()
457 479
458 480 else:
459 481 self._append_plain_text('Kernel process is either remote or '
460 482 'unspecified. Cannot restart.\n')
461 483
462 484 #---------------------------------------------------------------------------
463 485 # 'FrontendWidget' protected interface
464 486 #---------------------------------------------------------------------------
465 487
466 488 def _call_tip(self):
467 489 """ Shows a call tip, if appropriate, at the current cursor location.
468 490 """
469 491 # Decide if it makes sense to show a call tip
470 492 cursor = self._get_cursor()
471 493 cursor.movePosition(QtGui.QTextCursor.Left)
472 494 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
473 495 return False
474 496 context = self._get_context(cursor)
475 497 if not context:
476 498 return False
477 499
478 500 # Send the metadata request to the kernel
479 501 name = '.'.join(context)
480 502 msg_id = self.kernel_manager.xreq_channel.object_info(name)
481 503 pos = self._get_cursor().position()
482 504 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
483 505 return True
484 506
485 507 def _complete(self):
486 508 """ Performs completion at the current cursor location.
487 509 """
488 510 context = self._get_context()
489 511 if context:
490 512 # Send the completion request to the kernel
491 513 msg_id = self.kernel_manager.xreq_channel.complete(
492 514 '.'.join(context), # text
493 515 self._get_input_buffer_cursor_line(), # line
494 516 self._get_input_buffer_cursor_column(), # cursor_pos
495 517 self.input_buffer) # block
496 518 pos = self._get_cursor().position()
497 519 info = self._CompletionRequest(msg_id, pos)
498 520 self._request_info['complete'] = info
499 521
500 522 def _get_banner(self):
501 523 """ Gets a banner to display at the beginning of a session.
502 524 """
503 525 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
504 526 '"license" for more information.'
505 527 return banner % (sys.version, sys.platform)
506 528
507 529 def _get_context(self, cursor=None):
508 530 """ Gets the context for the specified cursor (or the current cursor
509 531 if none is specified).
510 532 """
511 533 if cursor is None:
512 534 cursor = self._get_cursor()
513 535 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
514 536 QtGui.QTextCursor.KeepAnchor)
515 537 text = unicode(cursor.selection().toPlainText())
516 538 return self._completion_lexer.get_context(text)
517 539
518 540 def _process_execute_abort(self, msg):
519 541 """ Process a reply for an aborted execution request.
520 542 """
521 543 self._append_plain_text("ERROR: execution aborted\n")
522 544
523 545 def _process_execute_error(self, msg):
524 546 """ Process a reply for an execution request that resulted in an error.
525 547 """
526 548 content = msg['content']
527 549 traceback = ''.join(content['traceback'])
528 550 self._append_plain_text(traceback)
529 551
530 552 def _process_execute_ok(self, msg):
531 553 """ Process a reply for a successful execution equest.
532 554 """
533 555 payload = msg['content']['payload']
534 556 for item in payload:
535 557 if not self._process_execute_payload(item):
536 558 warning = 'Warning: received unknown payload of type %s'
537 559 print(warning % repr(item['source']))
538 560
539 561 def _process_execute_payload(self, item):
540 562 """ Process a single payload item from the list of payload items in an
541 563 execution reply. Returns whether the payload was handled.
542 564 """
543 565 # The basic FrontendWidget doesn't handle payloads, as they are a
544 566 # mechanism for going beyond the standard Python interpreter model.
545 567 return False
546 568
547 569 def _show_interpreter_prompt(self):
548 570 """ Shows a prompt for the interpreter.
549 571 """
550 572 self._show_prompt('>>> ')
551 573
552 574 def _show_interpreter_prompt_for_reply(self, msg):
553 575 """ Shows a prompt for the interpreter given an 'execute_reply' message.
554 576 """
555 577 self._show_interpreter_prompt()
556 578
557 579 #------ Signal handlers ----------------------------------------------------
558 580
559 581 def _document_contents_change(self, position, removed, added):
560 582 """ Called whenever the document's content changes. Display a call tip
561 583 if appropriate.
562 584 """
563 585 # Calculate where the cursor should be *after* the change:
564 586 position += added
565 587
566 588 document = self._control.document()
567 589 if position == self._get_cursor().position():
568 590 self._call_tip()
@@ -1,158 +1,160 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2 """
3 3
4 4 #-----------------------------------------------------------------------------
5 5 # Imports
6 6 #-----------------------------------------------------------------------------
7 7
8 8 # Systemm library imports
9 9 from PyQt4 import QtGui
10 10
11 11 # Local imports
12 12 from IPython.external.argparse import ArgumentParser
13 13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
14 14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
15 15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
16 16 from IPython.frontend.qt.kernelmanager import QtKernelManager
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Constants
20 20 #-----------------------------------------------------------------------------
21 21
22 22 LOCALHOST = '127.0.0.1'
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Classes
26 26 #-----------------------------------------------------------------------------
27 27
28 28 class MainWindow(QtGui.QMainWindow):
29 29
30 30 #---------------------------------------------------------------------------
31 31 # 'object' interface
32 32 #---------------------------------------------------------------------------
33 33
34 34 def __init__(self, app, frontend, existing=False):
35 35 """ Create a MainWindow for the specified FrontendWidget.
36 36
37 37 The app is passed as an argument to allow for different
38 38 closing behavior depending on whether we are the Kernel's parent.
39 39
40 40 If existing is True, then this Window does not own the Kernel.
41 41 """
42 42 super(MainWindow, self).__init__()
43 43 self._app = app
44 44 self._frontend = frontend
45 45 self._existing = existing
46 46 self._frontend.exit_requested.connect(self.close)
47 47 self.setCentralWidget(frontend)
48 48
49 49 #---------------------------------------------------------------------------
50 50 # QWidget interface
51 51 #---------------------------------------------------------------------------
52 52
53 53 def closeEvent(self, event):
54 54 """ Reimplemented to prompt the user and close the kernel cleanly.
55 55 """
56 56 kernel_manager = self._frontend.kernel_manager
57 57 if kernel_manager and kernel_manager.channels_running:
58 58 title = self.window().windowTitle()
59 59 reply = QtGui.QMessageBox.question(self, title,
60 "Close just this console, or shutdown the kernel and close "+
61 "all windows attached to it?",
62 'Cancel', 'Close Console', 'Close All')
60 "You are closing this Console window."+
61 "\nWould you like to quit the Kernel and all attached Consoles as well?",
62 'Cancel', 'No, just this Console', 'Yes, quit everything')
63 63 if reply == 2: # close All
64 64 kernel_manager.shutdown_kernel()
65 65 #kernel_manager.stop_channels()
66 66 event.accept()
67 67 elif reply == 1: # close Console
68 68 if not self._existing:
69 69 # I have the kernel: don't quit, just close the window
70 70 self._app.setQuitOnLastWindowClosed(False)
71 self.deleteLater()
71 72 event.accept()
72 73 else:
73 74 event.ignore()
74 75
75 76 #-----------------------------------------------------------------------------
76 77 # Main entry point
77 78 #-----------------------------------------------------------------------------
78 79
79 80 def main():
80 81 """ Entry point for application.
81 82 """
82 83 # Parse command line arguments.
83 84 parser = ArgumentParser()
84 85 kgroup = parser.add_argument_group('kernel options')
85 86 kgroup.add_argument('-e', '--existing', action='store_true',
86 87 help='connect to an existing kernel')
87 88 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
88 89 help='set the kernel\'s IP address [default localhost]')
89 90 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
90 91 help='set the XREQ channel port [default random]')
91 92 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
92 93 help='set the SUB channel port [default random]')
93 94 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
94 95 help='set the REP channel port [default random]')
95 96 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
96 97 help='set the heartbeat port [default: random]')
97 98
98 99 egroup = kgroup.add_mutually_exclusive_group()
99 100 egroup.add_argument('--pure', action='store_true', help = \
100 101 'use a pure Python kernel instead of an IPython kernel')
101 102 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
102 103 const='auto', help = \
103 104 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
104 105 given, the GUI backend is matplotlib's, otherwise use one of: \
105 106 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
106 107
107 108 wgroup = parser.add_argument_group('widget options')
108 109 wgroup.add_argument('--paging', type=str, default='inside',
109 110 choices = ['inside', 'hsplit', 'vsplit', 'none'],
110 111 help='set the paging style [default inside]')
111 112 wgroup.add_argument('--rich', action='store_true',
112 113 help='enable rich text support')
113 114 wgroup.add_argument('--gui-completion', action='store_true',
114 115 help='use a GUI widget for tab completion')
115 116
116 117 args = parser.parse_args()
117 118
118 119 # Don't let Qt or ZMQ swallow KeyboardInterupts.
119 120 import signal
120 121 signal.signal(signal.SIGINT, signal.SIG_DFL)
121 122
122 123 # Create a KernelManager and start a kernel.
123 124 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
124 125 sub_address=(args.ip, args.sub),
125 126 rep_address=(args.ip, args.rep),
126 127 hb_address=(args.ip, args.hb))
127 128 if args.ip == LOCALHOST and not args.existing:
128 129 if args.pure:
129 130 kernel_manager.start_kernel(ipython=False)
130 131 elif args.pylab:
131 132 kernel_manager.start_kernel(pylab=args.pylab)
132 133 else:
133 134 kernel_manager.start_kernel()
134 135 kernel_manager.start_channels()
135 136
137 local_kernel = (args.ip == LOCALHOST)
136 138 # Create the widget.
137 139 app = QtGui.QApplication([])
138 140 if args.pure:
139 141 kind = 'rich' if args.rich else 'plain'
140 widget = FrontendWidget(kind=kind, paging=args.paging)
142 widget = FrontendWidget(kind=kind, paging=args.paging, local_kernel=local_kernel)
141 143 elif args.rich or args.pylab:
142 widget = RichIPythonWidget(paging=args.paging)
144 widget = RichIPythonWidget(paging=args.paging, local_kernel=local_kernel)
143 145 else:
144 widget = IPythonWidget(paging=args.paging)
146 widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
145 147 widget.gui_completion = args.gui_completion
146 148 widget.kernel_manager = kernel_manager
147 149
148 150 # Create the main window.
149 151 window = MainWindow(app, widget, args.existing)
150 152 window.setWindowTitle('Python' if args.pure else 'IPython')
151 153 window.show()
152 154
153 155 # Start the application main loop.
154 156 app.exec_()
155 157
156 158
157 159 if __name__ == '__main__':
158 160 main()
General Comments 0
You need to be logged in to leave comments. Login now