##// END OF EJS Templates
Merge PR #1091 (qtconsole pager copy)...
MinRK -
r5648:7a144e2e merge
parent child Browse files
Show More
@@ -1,1822 +1,1822 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 import os
9 9 from os.path import commonprefix
10 10 import re
11 11 import sys
12 12 from textwrap import dedent
13 13 from unicodedata import category
14 14
15 15 # System library imports
16 16 from IPython.external.qt import QtCore, QtGui
17 17
18 18 # Local imports
19 19 from IPython.config.configurable import LoggingConfigurable
20 20 from IPython.frontend.qt.rich_text import HtmlExporter
21 21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 22 from IPython.utils.text import columnize
23 23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 24 from ansi_code_processor import QtAnsiCodeProcessor
25 25 from completion_widget import CompletionWidget
26 26 from kill_ring import QtKillRing
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Functions
30 30 #-----------------------------------------------------------------------------
31 31
32 32 def is_letter_or_number(char):
33 33 """ Returns whether the specified unicode character is a letter or a number.
34 34 """
35 35 cat = category(char)
36 36 return cat.startswith('L') or cat.startswith('N')
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 43 """ An abstract base class for console-type widgets. This class has
44 44 functionality for:
45 45
46 46 * Maintaining a prompt and editing region
47 47 * Providing the traditional Unix-style console keyboard shortcuts
48 48 * Performing tab completion
49 49 * Paging text
50 50 * Handling ANSI escape codes
51 51
52 52 ConsoleWidget also provides a number of utility methods that will be
53 53 convenient to implementors of a console-style widget.
54 54 """
55 55 __metaclass__ = MetaQObjectHasTraits
56 56
57 57 #------ Configuration ------------------------------------------------------
58 58
59 59 ansi_codes = Bool(True, config=True,
60 60 help="Whether to process ANSI escape codes."
61 61 )
62 62 buffer_size = Integer(500, config=True,
63 63 help="""
64 64 The maximum number of lines of text before truncation. Specifying a
65 65 non-positive number disables text truncation (not recommended).
66 66 """
67 67 )
68 68 gui_completion = Bool(False, config=True,
69 69 help="""
70 70 Use a list widget instead of plain text output for tab completion.
71 71 """
72 72 )
73 73 # NOTE: this value can only be specified during initialization.
74 74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 75 help="""
76 76 The type of underlying text widget to use. Valid values are 'plain',
77 77 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 78 QTextEdit.
79 79 """
80 80 )
81 81 # NOTE: this value can only be specified during initialization.
82 82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 83 default_value='inside', config=True,
84 84 help="""
85 85 The type of paging to use. Valid values are:
86 86
87 87 'inside' : The widget pages like a traditional terminal.
88 88 'hsplit' : When paging is requested, the widget is split
89 89 horizontally. The top pane contains the console, and the
90 90 bottom pane contains the paged text.
91 91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 92 used.
93 93 'custom' : No action is taken by the widget beyond emitting a
94 94 'custom_page_requested(str)' signal.
95 95 'none' : The text is written directly to the console.
96 96 """)
97 97
98 98 font_family = Unicode(config=True,
99 99 help="""The font family to use for the console.
100 100 On OSX this defaults to Monaco, on Windows the default is
101 101 Consolas with fallback of Courier, and on other platforms
102 102 the default is Monospace.
103 103 """)
104 104 def _font_family_default(self):
105 105 if sys.platform == 'win32':
106 106 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 107 return 'Consolas'
108 108 elif sys.platform == 'darwin':
109 109 # OSX always has Monaco, no need for a fallback
110 110 return 'Monaco'
111 111 else:
112 112 # Monospace should always exist, no need for a fallback
113 113 return 'Monospace'
114 114
115 115 font_size = Integer(config=True,
116 116 help="""The font size. If unconfigured, Qt will be entrusted
117 117 with the size of the font.
118 118 """)
119 119
120 120 # Whether to override ShortcutEvents for the keybindings defined by this
121 121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 123 override_shortcuts = Bool(False)
124 124
125 125 #------ Signals ------------------------------------------------------------
126 126
127 127 # Signals that indicate ConsoleWidget state.
128 128 copy_available = QtCore.Signal(bool)
129 129 redo_available = QtCore.Signal(bool)
130 130 undo_available = QtCore.Signal(bool)
131 131
132 132 # Signal emitted when paging is needed and the paging style has been
133 133 # specified as 'custom'.
134 134 custom_page_requested = QtCore.Signal(object)
135 135
136 136 # Signal emitted when the font is changed.
137 137 font_changed = QtCore.Signal(QtGui.QFont)
138 138
139 139 #------ Protected class variables ------------------------------------------
140 140
141 141 # When the control key is down, these keys are mapped.
142 142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
148 148 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
149 149 if not sys.platform == 'darwin':
150 150 # On OS X, Ctrl-E already does the right thing, whereas End moves the
151 151 # cursor to the bottom of the buffer.
152 152 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
153 153
154 154 # The shortcuts defined by this widget. We need to keep track of these to
155 155 # support 'override_shortcuts' above.
156 156 _shortcuts = set(_ctrl_down_remap.keys() +
157 157 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
158 158 QtCore.Qt.Key_V ])
159 159
160 160 #---------------------------------------------------------------------------
161 161 # 'QObject' interface
162 162 #---------------------------------------------------------------------------
163 163
164 164 def __init__(self, parent=None, **kw):
165 165 """ Create a ConsoleWidget.
166 166
167 167 Parameters:
168 168 -----------
169 169 parent : QWidget, optional [default None]
170 170 The parent for this widget.
171 171 """
172 172 QtGui.QWidget.__init__(self, parent)
173 173 LoggingConfigurable.__init__(self, **kw)
174 174
175 175 # Create the layout and underlying text widget.
176 176 layout = QtGui.QStackedLayout(self)
177 177 layout.setContentsMargins(0, 0, 0, 0)
178 178 self._control = self._create_control()
179 179 self._page_control = None
180 180 self._splitter = None
181 181 if self.paging in ('hsplit', 'vsplit'):
182 182 self._splitter = QtGui.QSplitter()
183 183 if self.paging == 'hsplit':
184 184 self._splitter.setOrientation(QtCore.Qt.Horizontal)
185 185 else:
186 186 self._splitter.setOrientation(QtCore.Qt.Vertical)
187 187 self._splitter.addWidget(self._control)
188 188 layout.addWidget(self._splitter)
189 189 else:
190 190 layout.addWidget(self._control)
191 191
192 192 # Create the paging widget, if necessary.
193 193 if self.paging in ('inside', 'hsplit', 'vsplit'):
194 194 self._page_control = self._create_page_control()
195 195 if self._splitter:
196 196 self._page_control.hide()
197 197 self._splitter.addWidget(self._page_control)
198 198 else:
199 199 layout.addWidget(self._page_control)
200 200
201 201 # Initialize protected variables. Some variables contain useful state
202 202 # information for subclasses; they should be considered read-only.
203 203 self._append_before_prompt_pos = 0
204 204 self._ansi_processor = QtAnsiCodeProcessor()
205 205 self._completion_widget = CompletionWidget(self._control)
206 206 self._continuation_prompt = '> '
207 207 self._continuation_prompt_html = None
208 208 self._executing = False
209 209 self._filter_drag = False
210 210 self._filter_resize = False
211 211 self._html_exporter = HtmlExporter(self._control)
212 212 self._input_buffer_executing = ''
213 213 self._input_buffer_pending = ''
214 214 self._kill_ring = QtKillRing(self._control)
215 215 self._prompt = ''
216 216 self._prompt_html = None
217 217 self._prompt_pos = 0
218 218 self._prompt_sep = ''
219 219 self._reading = False
220 220 self._reading_callback = None
221 221 self._tab_width = 8
222 222 self._text_completing_pos = 0
223 223
224 224 # Set a monospaced font.
225 225 self.reset_font()
226 226
227 227 # Configure actions.
228 228 action = QtGui.QAction('Print', None)
229 229 action.setEnabled(True)
230 230 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
231 231 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
232 232 # Only override the default if there is a collision.
233 233 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
234 234 printkey = "Ctrl+Shift+P"
235 235 action.setShortcut(printkey)
236 236 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
237 237 action.triggered.connect(self.print_)
238 238 self.addAction(action)
239 239 self.print_action = action
240 240
241 241 action = QtGui.QAction('Save as HTML/XML', None)
242 242 action.setShortcut(QtGui.QKeySequence.Save)
243 243 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
244 244 action.triggered.connect(self.export_html)
245 245 self.addAction(action)
246 246 self.export_action = action
247 247
248 248 action = QtGui.QAction('Select All', None)
249 249 action.setEnabled(True)
250 250 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
251 251 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
252 252 # Only override the default if there is a collision.
253 253 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
254 254 selectall = "Ctrl+Shift+A"
255 255 action.setShortcut(selectall)
256 256 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
257 257 action.triggered.connect(self.select_all)
258 258 self.addAction(action)
259 259 self.select_all_action = action
260 260
261 261 self.increase_font_size = QtGui.QAction("Bigger Font",
262 262 self,
263 263 shortcut=QtGui.QKeySequence.ZoomIn,
264 264 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
265 265 statusTip="Increase the font size by one point",
266 266 triggered=self._increase_font_size)
267 267 self.addAction(self.increase_font_size)
268 268
269 269 self.decrease_font_size = QtGui.QAction("Smaller Font",
270 270 self,
271 271 shortcut=QtGui.QKeySequence.ZoomOut,
272 272 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
273 273 statusTip="Decrease the font size by one point",
274 274 triggered=self._decrease_font_size)
275 275 self.addAction(self.decrease_font_size)
276 276
277 277 self.reset_font_size = QtGui.QAction("Normal Font",
278 278 self,
279 279 shortcut="Ctrl+0",
280 280 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
281 281 statusTip="Restore the Normal font size",
282 282 triggered=self.reset_font)
283 283 self.addAction(self.reset_font_size)
284 284
285 285
286 286
287 287 def eventFilter(self, obj, event):
288 288 """ Reimplemented to ensure a console-like behavior in the underlying
289 289 text widgets.
290 290 """
291 291 etype = event.type()
292 292 if etype == QtCore.QEvent.KeyPress:
293 293
294 294 # Re-map keys for all filtered widgets.
295 295 key = event.key()
296 296 if self._control_key_down(event.modifiers()) and \
297 297 key in self._ctrl_down_remap:
298 298 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
299 299 self._ctrl_down_remap[key],
300 300 QtCore.Qt.NoModifier)
301 301 QtGui.qApp.sendEvent(obj, new_event)
302 302 return True
303 303
304 304 elif obj == self._control:
305 305 return self._event_filter_console_keypress(event)
306 306
307 307 elif obj == self._page_control:
308 308 return self._event_filter_page_keypress(event)
309 309
310 310 # Make middle-click paste safe.
311 311 elif etype == QtCore.QEvent.MouseButtonRelease and \
312 312 event.button() == QtCore.Qt.MidButton and \
313 313 obj == self._control.viewport():
314 314 cursor = self._control.cursorForPosition(event.pos())
315 315 self._control.setTextCursor(cursor)
316 316 self.paste(QtGui.QClipboard.Selection)
317 317 return True
318 318
319 319 # Manually adjust the scrollbars *after* a resize event is dispatched.
320 320 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
321 321 self._filter_resize = True
322 322 QtGui.qApp.sendEvent(obj, event)
323 323 self._adjust_scrollbars()
324 324 self._filter_resize = False
325 325 return True
326 326
327 327 # Override shortcuts for all filtered widgets.
328 328 elif etype == QtCore.QEvent.ShortcutOverride and \
329 329 self.override_shortcuts and \
330 330 self._control_key_down(event.modifiers()) and \
331 331 event.key() in self._shortcuts:
332 332 event.accept()
333 333
334 334 # Ensure that drags are safe. The problem is that the drag starting
335 335 # logic, which determines whether the drag is a Copy or Move, is locked
336 336 # down in QTextControl. If the widget is editable, which it must be if
337 337 # we're not executing, the drag will be a Move. The following hack
338 338 # prevents QTextControl from deleting the text by clearing the selection
339 339 # when a drag leave event originating from this widget is dispatched.
340 340 # The fact that we have to clear the user's selection is unfortunate,
341 341 # but the alternative--trying to prevent Qt from using its hardwired
342 342 # drag logic and writing our own--is worse.
343 343 elif etype == QtCore.QEvent.DragEnter and \
344 344 obj == self._control.viewport() and \
345 345 event.source() == self._control.viewport():
346 346 self._filter_drag = True
347 347 elif etype == QtCore.QEvent.DragLeave and \
348 348 obj == self._control.viewport() and \
349 349 self._filter_drag:
350 350 cursor = self._control.textCursor()
351 351 cursor.clearSelection()
352 352 self._control.setTextCursor(cursor)
353 353 self._filter_drag = False
354 354
355 355 # Ensure that drops are safe.
356 356 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
357 357 cursor = self._control.cursorForPosition(event.pos())
358 358 if self._in_buffer(cursor.position()):
359 359 text = event.mimeData().text()
360 360 self._insert_plain_text_into_buffer(cursor, text)
361 361
362 362 # Qt is expecting to get something here--drag and drop occurs in its
363 363 # own event loop. Send a DragLeave event to end it.
364 364 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
365 365 return True
366 366
367 367 return super(ConsoleWidget, self).eventFilter(obj, event)
368 368
369 369 #---------------------------------------------------------------------------
370 370 # 'QWidget' interface
371 371 #---------------------------------------------------------------------------
372 372
373 373 def sizeHint(self):
374 374 """ Reimplemented to suggest a size that is 80 characters wide and
375 375 25 lines high.
376 376 """
377 377 font_metrics = QtGui.QFontMetrics(self.font)
378 378 margin = (self._control.frameWidth() +
379 379 self._control.document().documentMargin()) * 2
380 380 style = self.style()
381 381 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
382 382
383 383 # Note 1: Despite my best efforts to take the various margins into
384 384 # account, the width is still coming out a bit too small, so we include
385 385 # a fudge factor of one character here.
386 386 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
387 387 # to a Qt bug on certain Mac OS systems where it returns 0.
388 388 width = font_metrics.width(' ') * 81 + margin
389 389 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
390 390 if self.paging == 'hsplit':
391 391 width = width * 2 + splitwidth
392 392
393 393 height = font_metrics.height() * 25 + margin
394 394 if self.paging == 'vsplit':
395 395 height = height * 2 + splitwidth
396 396
397 397 return QtCore.QSize(width, height)
398 398
399 399 #---------------------------------------------------------------------------
400 400 # 'ConsoleWidget' public interface
401 401 #---------------------------------------------------------------------------
402 402
403 403 def can_copy(self):
404 404 """ Returns whether text can be copied to the clipboard.
405 405 """
406 406 return self._control.textCursor().hasSelection()
407 407
408 408 def can_cut(self):
409 409 """ Returns whether text can be cut to the clipboard.
410 410 """
411 411 cursor = self._control.textCursor()
412 412 return (cursor.hasSelection() and
413 413 self._in_buffer(cursor.anchor()) and
414 414 self._in_buffer(cursor.position()))
415 415
416 416 def can_paste(self):
417 417 """ Returns whether text can be pasted from the clipboard.
418 418 """
419 419 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
420 420 return bool(QtGui.QApplication.clipboard().text())
421 421 return False
422 422
423 423 def clear(self, keep_input=True):
424 424 """ Clear the console.
425 425
426 426 Parameters:
427 427 -----------
428 428 keep_input : bool, optional (default True)
429 429 If set, restores the old input buffer if a new prompt is written.
430 430 """
431 431 if self._executing:
432 432 self._control.clear()
433 433 else:
434 434 if keep_input:
435 435 input_buffer = self.input_buffer
436 436 self._control.clear()
437 437 self._show_prompt()
438 438 if keep_input:
439 439 self.input_buffer = input_buffer
440 440
441 441 def copy(self):
442 442 """ Copy the currently selected text to the clipboard.
443 443 """
444 self._control.copy()
444 self.layout().currentWidget().copy()
445 445
446 446 def cut(self):
447 447 """ Copy the currently selected text to the clipboard and delete it
448 448 if it's inside the input buffer.
449 449 """
450 450 self.copy()
451 451 if self.can_cut():
452 452 self._control.textCursor().removeSelectedText()
453 453
454 454 def execute(self, source=None, hidden=False, interactive=False):
455 455 """ Executes source or the input buffer, possibly prompting for more
456 456 input.
457 457
458 458 Parameters:
459 459 -----------
460 460 source : str, optional
461 461
462 462 The source to execute. If not specified, the input buffer will be
463 463 used. If specified and 'hidden' is False, the input buffer will be
464 464 replaced with the source before execution.
465 465
466 466 hidden : bool, optional (default False)
467 467
468 468 If set, no output will be shown and the prompt will not be modified.
469 469 In other words, it will be completely invisible to the user that
470 470 an execution has occurred.
471 471
472 472 interactive : bool, optional (default False)
473 473
474 474 Whether the console is to treat the source as having been manually
475 475 entered by the user. The effect of this parameter depends on the
476 476 subclass implementation.
477 477
478 478 Raises:
479 479 -------
480 480 RuntimeError
481 481 If incomplete input is given and 'hidden' is True. In this case,
482 482 it is not possible to prompt for more input.
483 483
484 484 Returns:
485 485 --------
486 486 A boolean indicating whether the source was executed.
487 487 """
488 488 # WARNING: The order in which things happen here is very particular, in
489 489 # large part because our syntax highlighting is fragile. If you change
490 490 # something, test carefully!
491 491
492 492 # Decide what to execute.
493 493 if source is None:
494 494 source = self.input_buffer
495 495 if not hidden:
496 496 # A newline is appended later, but it should be considered part
497 497 # of the input buffer.
498 498 source += '\n'
499 499 elif not hidden:
500 500 self.input_buffer = source
501 501
502 502 # Execute the source or show a continuation prompt if it is incomplete.
503 503 complete = self._is_complete(source, interactive)
504 504 if hidden:
505 505 if complete:
506 506 self._execute(source, hidden)
507 507 else:
508 508 error = 'Incomplete noninteractive input: "%s"'
509 509 raise RuntimeError(error % source)
510 510 else:
511 511 if complete:
512 512 self._append_plain_text('\n')
513 513 self._input_buffer_executing = self.input_buffer
514 514 self._executing = True
515 515 self._prompt_finished()
516 516
517 517 # The maximum block count is only in effect during execution.
518 518 # This ensures that _prompt_pos does not become invalid due to
519 519 # text truncation.
520 520 self._control.document().setMaximumBlockCount(self.buffer_size)
521 521
522 522 # Setting a positive maximum block count will automatically
523 523 # disable the undo/redo history, but just to be safe:
524 524 self._control.setUndoRedoEnabled(False)
525 525
526 526 # Perform actual execution.
527 527 self._execute(source, hidden)
528 528
529 529 else:
530 530 # Do this inside an edit block so continuation prompts are
531 531 # removed seamlessly via undo/redo.
532 532 cursor = self._get_end_cursor()
533 533 cursor.beginEditBlock()
534 534 cursor.insertText('\n')
535 535 self._insert_continuation_prompt(cursor)
536 536 cursor.endEditBlock()
537 537
538 538 # Do not do this inside the edit block. It works as expected
539 539 # when using a QPlainTextEdit control, but does not have an
540 540 # effect when using a QTextEdit. I believe this is a Qt bug.
541 541 self._control.moveCursor(QtGui.QTextCursor.End)
542 542
543 543 return complete
544 544
545 545 def export_html(self):
546 546 """ Shows a dialog to export HTML/XML in various formats.
547 547 """
548 548 self._html_exporter.export()
549 549
550 550 def _get_input_buffer(self, force=False):
551 551 """ The text that the user has entered entered at the current prompt.
552 552
553 553 If the console is currently executing, the text that is executing will
554 554 always be returned.
555 555 """
556 556 # If we're executing, the input buffer may not even exist anymore due to
557 557 # the limit imposed by 'buffer_size'. Therefore, we store it.
558 558 if self._executing and not force:
559 559 return self._input_buffer_executing
560 560
561 561 cursor = self._get_end_cursor()
562 562 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
563 563 input_buffer = cursor.selection().toPlainText()
564 564
565 565 # Strip out continuation prompts.
566 566 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
567 567
568 568 def _set_input_buffer(self, string):
569 569 """ Sets the text in the input buffer.
570 570
571 571 If the console is currently executing, this call has no *immediate*
572 572 effect. When the execution is finished, the input buffer will be updated
573 573 appropriately.
574 574 """
575 575 # If we're executing, store the text for later.
576 576 if self._executing:
577 577 self._input_buffer_pending = string
578 578 return
579 579
580 580 # Remove old text.
581 581 cursor = self._get_end_cursor()
582 582 cursor.beginEditBlock()
583 583 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
584 584 cursor.removeSelectedText()
585 585
586 586 # Insert new text with continuation prompts.
587 587 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
588 588 cursor.endEditBlock()
589 589 self._control.moveCursor(QtGui.QTextCursor.End)
590 590
591 591 input_buffer = property(_get_input_buffer, _set_input_buffer)
592 592
593 593 def _get_font(self):
594 594 """ The base font being used by the ConsoleWidget.
595 595 """
596 596 return self._control.document().defaultFont()
597 597
598 598 def _set_font(self, font):
599 599 """ Sets the base font for the ConsoleWidget to the specified QFont.
600 600 """
601 601 font_metrics = QtGui.QFontMetrics(font)
602 602 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
603 603
604 604 self._completion_widget.setFont(font)
605 605 self._control.document().setDefaultFont(font)
606 606 if self._page_control:
607 607 self._page_control.document().setDefaultFont(font)
608 608
609 609 self.font_changed.emit(font)
610 610
611 611 font = property(_get_font, _set_font)
612 612
613 613 def paste(self, mode=QtGui.QClipboard.Clipboard):
614 614 """ Paste the contents of the clipboard into the input region.
615 615
616 616 Parameters:
617 617 -----------
618 618 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
619 619
620 620 Controls which part of the system clipboard is used. This can be
621 621 used to access the selection clipboard in X11 and the Find buffer
622 622 in Mac OS. By default, the regular clipboard is used.
623 623 """
624 624 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
625 625 # Make sure the paste is safe.
626 626 self._keep_cursor_in_buffer()
627 627 cursor = self._control.textCursor()
628 628
629 629 # Remove any trailing newline, which confuses the GUI and forces the
630 630 # user to backspace.
631 631 text = QtGui.QApplication.clipboard().text(mode).rstrip()
632 632 self._insert_plain_text_into_buffer(cursor, dedent(text))
633 633
634 634 def print_(self, printer = None):
635 635 """ Print the contents of the ConsoleWidget to the specified QPrinter.
636 636 """
637 637 if (not printer):
638 638 printer = QtGui.QPrinter()
639 639 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
640 640 return
641 641 self._control.print_(printer)
642 642
643 643 def prompt_to_top(self):
644 644 """ Moves the prompt to the top of the viewport.
645 645 """
646 646 if not self._executing:
647 647 prompt_cursor = self._get_prompt_cursor()
648 648 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
649 649 self._set_cursor(prompt_cursor)
650 650 self._set_top_cursor(prompt_cursor)
651 651
652 652 def redo(self):
653 653 """ Redo the last operation. If there is no operation to redo, nothing
654 654 happens.
655 655 """
656 656 self._control.redo()
657 657
658 658 def reset_font(self):
659 659 """ Sets the font to the default fixed-width font for this platform.
660 660 """
661 661 if sys.platform == 'win32':
662 662 # Consolas ships with Vista/Win7, fallback to Courier if needed
663 663 fallback = 'Courier'
664 664 elif sys.platform == 'darwin':
665 665 # OSX always has Monaco
666 666 fallback = 'Monaco'
667 667 else:
668 668 # Monospace should always exist
669 669 fallback = 'Monospace'
670 670 font = get_font(self.font_family, fallback)
671 671 if self.font_size:
672 672 font.setPointSize(self.font_size)
673 673 else:
674 674 font.setPointSize(QtGui.qApp.font().pointSize())
675 675 font.setStyleHint(QtGui.QFont.TypeWriter)
676 676 self._set_font(font)
677 677
678 678 def change_font_size(self, delta):
679 679 """Change the font size by the specified amount (in points).
680 680 """
681 681 font = self.font
682 682 size = max(font.pointSize() + delta, 1) # minimum 1 point
683 683 font.setPointSize(size)
684 684 self._set_font(font)
685 685
686 686 def _increase_font_size(self):
687 687 self.change_font_size(1)
688 688
689 689 def _decrease_font_size(self):
690 690 self.change_font_size(-1)
691 691
692 692 def select_all(self):
693 693 """ Selects all the text in the buffer.
694 694 """
695 695 self._control.selectAll()
696 696
697 697 def _get_tab_width(self):
698 698 """ The width (in terms of space characters) for tab characters.
699 699 """
700 700 return self._tab_width
701 701
702 702 def _set_tab_width(self, tab_width):
703 703 """ Sets the width (in terms of space characters) for tab characters.
704 704 """
705 705 font_metrics = QtGui.QFontMetrics(self.font)
706 706 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
707 707
708 708 self._tab_width = tab_width
709 709
710 710 tab_width = property(_get_tab_width, _set_tab_width)
711 711
712 712 def undo(self):
713 713 """ Undo the last operation. If there is no operation to undo, nothing
714 714 happens.
715 715 """
716 716 self._control.undo()
717 717
718 718 #---------------------------------------------------------------------------
719 719 # 'ConsoleWidget' abstract interface
720 720 #---------------------------------------------------------------------------
721 721
722 722 def _is_complete(self, source, interactive):
723 723 """ Returns whether 'source' can be executed. When triggered by an
724 724 Enter/Return key press, 'interactive' is True; otherwise, it is
725 725 False.
726 726 """
727 727 raise NotImplementedError
728 728
729 729 def _execute(self, source, hidden):
730 730 """ Execute 'source'. If 'hidden', do not show any output.
731 731 """
732 732 raise NotImplementedError
733 733
734 734 def _prompt_started_hook(self):
735 735 """ Called immediately after a new prompt is displayed.
736 736 """
737 737 pass
738 738
739 739 def _prompt_finished_hook(self):
740 740 """ Called immediately after a prompt is finished, i.e. when some input
741 741 will be processed and a new prompt displayed.
742 742 """
743 743 pass
744 744
745 745 def _up_pressed(self, shift_modifier):
746 746 """ Called when the up key is pressed. Returns whether to continue
747 747 processing the event.
748 748 """
749 749 return True
750 750
751 751 def _down_pressed(self, shift_modifier):
752 752 """ Called when the down key is pressed. Returns whether to continue
753 753 processing the event.
754 754 """
755 755 return True
756 756
757 757 def _tab_pressed(self):
758 758 """ Called when the tab key is pressed. Returns whether to continue
759 759 processing the event.
760 760 """
761 761 return False
762 762
763 763 #--------------------------------------------------------------------------
764 764 # 'ConsoleWidget' protected interface
765 765 #--------------------------------------------------------------------------
766 766
767 767 def _append_custom(self, insert, input, before_prompt=False):
768 768 """ A low-level method for appending content to the end of the buffer.
769 769
770 770 If 'before_prompt' is enabled, the content will be inserted before the
771 771 current prompt, if there is one.
772 772 """
773 773 # Determine where to insert the content.
774 774 cursor = self._control.textCursor()
775 775 if before_prompt and (self._reading or not self._executing):
776 776 cursor.setPosition(self._append_before_prompt_pos)
777 777 else:
778 778 cursor.movePosition(QtGui.QTextCursor.End)
779 779 start_pos = cursor.position()
780 780
781 781 # Perform the insertion.
782 782 result = insert(cursor, input)
783 783
784 784 # Adjust the prompt position if we have inserted before it. This is safe
785 785 # because buffer truncation is disabled when not executing.
786 786 if before_prompt and not self._executing:
787 787 diff = cursor.position() - start_pos
788 788 self._append_before_prompt_pos += diff
789 789 self._prompt_pos += diff
790 790
791 791 return result
792 792
793 793 def _append_html(self, html, before_prompt=False):
794 794 """ Appends HTML at the end of the console buffer.
795 795 """
796 796 self._append_custom(self._insert_html, html, before_prompt)
797 797
798 798 def _append_html_fetching_plain_text(self, html, before_prompt=False):
799 799 """ Appends HTML, then returns the plain text version of it.
800 800 """
801 801 return self._append_custom(self._insert_html_fetching_plain_text,
802 802 html, before_prompt)
803 803
804 804 def _append_plain_text(self, text, before_prompt=False):
805 805 """ Appends plain text, processing ANSI codes if enabled.
806 806 """
807 807 self._append_custom(self._insert_plain_text, text, before_prompt)
808 808
809 809 def _cancel_text_completion(self):
810 810 """ If text completion is progress, cancel it.
811 811 """
812 812 if self._text_completing_pos:
813 813 self._clear_temporary_buffer()
814 814 self._text_completing_pos = 0
815 815
816 816 def _clear_temporary_buffer(self):
817 817 """ Clears the "temporary text" buffer, i.e. all the text following
818 818 the prompt region.
819 819 """
820 820 # Select and remove all text below the input buffer.
821 821 cursor = self._get_prompt_cursor()
822 822 prompt = self._continuation_prompt.lstrip()
823 823 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
824 824 temp_cursor = QtGui.QTextCursor(cursor)
825 825 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
826 826 text = temp_cursor.selection().toPlainText().lstrip()
827 827 if not text.startswith(prompt):
828 828 break
829 829 else:
830 830 # We've reached the end of the input buffer and no text follows.
831 831 return
832 832 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
833 833 cursor.movePosition(QtGui.QTextCursor.End,
834 834 QtGui.QTextCursor.KeepAnchor)
835 835 cursor.removeSelectedText()
836 836
837 837 # After doing this, we have no choice but to clear the undo/redo
838 838 # history. Otherwise, the text is not "temporary" at all, because it
839 839 # can be recalled with undo/redo. Unfortunately, Qt does not expose
840 840 # fine-grained control to the undo/redo system.
841 841 if self._control.isUndoRedoEnabled():
842 842 self._control.setUndoRedoEnabled(False)
843 843 self._control.setUndoRedoEnabled(True)
844 844
845 845 def _complete_with_items(self, cursor, items):
846 846 """ Performs completion with 'items' at the specified cursor location.
847 847 """
848 848 self._cancel_text_completion()
849 849
850 850 if len(items) == 1:
851 851 cursor.setPosition(self._control.textCursor().position(),
852 852 QtGui.QTextCursor.KeepAnchor)
853 853 cursor.insertText(items[0])
854 854
855 855 elif len(items) > 1:
856 856 current_pos = self._control.textCursor().position()
857 857 prefix = commonprefix(items)
858 858 if prefix:
859 859 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
860 860 cursor.insertText(prefix)
861 861 current_pos = cursor.position()
862 862
863 863 if self.gui_completion:
864 864 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
865 865 self._completion_widget.show_items(cursor, items)
866 866 else:
867 867 cursor.beginEditBlock()
868 868 self._append_plain_text('\n')
869 869 self._page(self._format_as_columns(items))
870 870 cursor.endEditBlock()
871 871
872 872 cursor.setPosition(current_pos)
873 873 self._control.moveCursor(QtGui.QTextCursor.End)
874 874 self._control.setTextCursor(cursor)
875 875 self._text_completing_pos = current_pos
876 876
877 877 def _context_menu_make(self, pos):
878 878 """ Creates a context menu for the given QPoint (in widget coordinates).
879 879 """
880 880 menu = QtGui.QMenu(self)
881 881
882 882 self.cut_action = menu.addAction('Cut', self.cut)
883 883 self.cut_action.setEnabled(self.can_cut())
884 884 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
885 885
886 886 self.copy_action = menu.addAction('Copy', self.copy)
887 887 self.copy_action.setEnabled(self.can_copy())
888 888 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
889 889
890 890 self.paste_action = menu.addAction('Paste', self.paste)
891 891 self.paste_action.setEnabled(self.can_paste())
892 892 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
893 893
894 894 menu.addSeparator()
895 895 menu.addAction(self.select_all_action)
896 896
897 897 menu.addSeparator()
898 898 menu.addAction(self.export_action)
899 899 menu.addAction(self.print_action)
900 900
901 901 return menu
902 902
903 903 def _control_key_down(self, modifiers, include_command=False):
904 904 """ Given a KeyboardModifiers flags object, return whether the Control
905 905 key is down.
906 906
907 907 Parameters:
908 908 -----------
909 909 include_command : bool, optional (default True)
910 910 Whether to treat the Command key as a (mutually exclusive) synonym
911 911 for Control when in Mac OS.
912 912 """
913 913 # Note that on Mac OS, ControlModifier corresponds to the Command key
914 914 # while MetaModifier corresponds to the Control key.
915 915 if sys.platform == 'darwin':
916 916 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
917 917 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
918 918 else:
919 919 return bool(modifiers & QtCore.Qt.ControlModifier)
920 920
921 921 def _create_control(self):
922 922 """ Creates and connects the underlying text widget.
923 923 """
924 924 # Create the underlying control.
925 925 if self.kind == 'plain':
926 926 control = QtGui.QPlainTextEdit()
927 927 elif self.kind == 'rich':
928 928 control = QtGui.QTextEdit()
929 929 control.setAcceptRichText(False)
930 930
931 931 # Install event filters. The filter on the viewport is needed for
932 932 # mouse events and drag events.
933 933 control.installEventFilter(self)
934 934 control.viewport().installEventFilter(self)
935 935
936 936 # Connect signals.
937 937 control.cursorPositionChanged.connect(self._cursor_position_changed)
938 938 control.customContextMenuRequested.connect(
939 939 self._custom_context_menu_requested)
940 940 control.copyAvailable.connect(self.copy_available)
941 941 control.redoAvailable.connect(self.redo_available)
942 942 control.undoAvailable.connect(self.undo_available)
943 943
944 944 # Hijack the document size change signal to prevent Qt from adjusting
945 945 # the viewport's scrollbar. We are relying on an implementation detail
946 946 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
947 947 # this functionality we cannot create a nice terminal interface.
948 948 layout = control.document().documentLayout()
949 949 layout.documentSizeChanged.disconnect()
950 950 layout.documentSizeChanged.connect(self._adjust_scrollbars)
951 951
952 952 # Configure the control.
953 953 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
954 954 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
955 955 control.setReadOnly(True)
956 956 control.setUndoRedoEnabled(False)
957 957 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
958 958 return control
959 959
960 960 def _create_page_control(self):
961 961 """ Creates and connects the underlying paging widget.
962 962 """
963 963 if self.kind == 'plain':
964 964 control = QtGui.QPlainTextEdit()
965 965 elif self.kind == 'rich':
966 966 control = QtGui.QTextEdit()
967 967 control.installEventFilter(self)
968 968 control.setReadOnly(True)
969 969 control.setUndoRedoEnabled(False)
970 970 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
971 971 return control
972 972
973 973 def _event_filter_console_keypress(self, event):
974 974 """ Filter key events for the underlying text widget to create a
975 975 console-like interface.
976 976 """
977 977 intercepted = False
978 978 cursor = self._control.textCursor()
979 979 position = cursor.position()
980 980 key = event.key()
981 981 ctrl_down = self._control_key_down(event.modifiers())
982 982 alt_down = event.modifiers() & QtCore.Qt.AltModifier
983 983 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
984 984
985 985 #------ Special sequences ----------------------------------------------
986 986
987 987 if event.matches(QtGui.QKeySequence.Copy):
988 988 self.copy()
989 989 intercepted = True
990 990
991 991 elif event.matches(QtGui.QKeySequence.Cut):
992 992 self.cut()
993 993 intercepted = True
994 994
995 995 elif event.matches(QtGui.QKeySequence.Paste):
996 996 self.paste()
997 997 intercepted = True
998 998
999 999 #------ Special modifier logic -----------------------------------------
1000 1000
1001 1001 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1002 1002 intercepted = True
1003 1003
1004 1004 # Special handling when tab completing in text mode.
1005 1005 self._cancel_text_completion()
1006 1006
1007 1007 if self._in_buffer(position):
1008 1008 # Special handling when a reading a line of raw input.
1009 1009 if self._reading:
1010 1010 self._append_plain_text('\n')
1011 1011 self._reading = False
1012 1012 if self._reading_callback:
1013 1013 self._reading_callback()
1014 1014
1015 1015 # If the input buffer is a single line or there is only
1016 1016 # whitespace after the cursor, execute. Otherwise, split the
1017 1017 # line with a continuation prompt.
1018 1018 elif not self._executing:
1019 1019 cursor.movePosition(QtGui.QTextCursor.End,
1020 1020 QtGui.QTextCursor.KeepAnchor)
1021 1021 at_end = len(cursor.selectedText().strip()) == 0
1022 1022 single_line = (self._get_end_cursor().blockNumber() ==
1023 1023 self._get_prompt_cursor().blockNumber())
1024 1024 if (at_end or shift_down or single_line) and not ctrl_down:
1025 1025 self.execute(interactive = not shift_down)
1026 1026 else:
1027 1027 # Do this inside an edit block for clean undo/redo.
1028 1028 cursor.beginEditBlock()
1029 1029 cursor.setPosition(position)
1030 1030 cursor.insertText('\n')
1031 1031 self._insert_continuation_prompt(cursor)
1032 1032 cursor.endEditBlock()
1033 1033
1034 1034 # Ensure that the whole input buffer is visible.
1035 1035 # FIXME: This will not be usable if the input buffer is
1036 1036 # taller than the console widget.
1037 1037 self._control.moveCursor(QtGui.QTextCursor.End)
1038 1038 self._control.setTextCursor(cursor)
1039 1039
1040 1040 #------ Control/Cmd modifier -------------------------------------------
1041 1041
1042 1042 elif ctrl_down:
1043 1043 if key == QtCore.Qt.Key_G:
1044 1044 self._keyboard_quit()
1045 1045 intercepted = True
1046 1046
1047 1047 elif key == QtCore.Qt.Key_K:
1048 1048 if self._in_buffer(position):
1049 1049 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1050 1050 QtGui.QTextCursor.KeepAnchor)
1051 1051 if not cursor.hasSelection():
1052 1052 # Line deletion (remove continuation prompt)
1053 1053 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1054 1054 QtGui.QTextCursor.KeepAnchor)
1055 1055 cursor.movePosition(QtGui.QTextCursor.Right,
1056 1056 QtGui.QTextCursor.KeepAnchor,
1057 1057 len(self._continuation_prompt))
1058 1058 self._kill_ring.kill_cursor(cursor)
1059 1059 intercepted = True
1060 1060
1061 1061 elif key == QtCore.Qt.Key_L:
1062 1062 self.prompt_to_top()
1063 1063 intercepted = True
1064 1064
1065 1065 elif key == QtCore.Qt.Key_O:
1066 1066 if self._page_control and self._page_control.isVisible():
1067 1067 self._page_control.setFocus()
1068 1068 intercepted = True
1069 1069
1070 1070 elif key == QtCore.Qt.Key_U:
1071 1071 if self._in_buffer(position):
1072 1072 start_line = cursor.blockNumber()
1073 1073 if start_line == self._get_prompt_cursor().blockNumber():
1074 1074 offset = len(self._prompt)
1075 1075 else:
1076 1076 offset = len(self._continuation_prompt)
1077 1077 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1078 1078 QtGui.QTextCursor.KeepAnchor)
1079 1079 cursor.movePosition(QtGui.QTextCursor.Right,
1080 1080 QtGui.QTextCursor.KeepAnchor, offset)
1081 1081 self._kill_ring.kill_cursor(cursor)
1082 1082 intercepted = True
1083 1083
1084 1084 elif key == QtCore.Qt.Key_Y:
1085 1085 self._keep_cursor_in_buffer()
1086 1086 self._kill_ring.yank()
1087 1087 intercepted = True
1088 1088
1089 1089 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1090 1090 if key == QtCore.Qt.Key_Backspace:
1091 1091 cursor = self._get_word_start_cursor(position)
1092 1092 else: # key == QtCore.Qt.Key_Delete
1093 1093 cursor = self._get_word_end_cursor(position)
1094 1094 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1095 1095 self._kill_ring.kill_cursor(cursor)
1096 1096 intercepted = True
1097 1097
1098 1098 #------ Alt modifier ---------------------------------------------------
1099 1099
1100 1100 elif alt_down:
1101 1101 if key == QtCore.Qt.Key_B:
1102 1102 self._set_cursor(self._get_word_start_cursor(position))
1103 1103 intercepted = True
1104 1104
1105 1105 elif key == QtCore.Qt.Key_F:
1106 1106 self._set_cursor(self._get_word_end_cursor(position))
1107 1107 intercepted = True
1108 1108
1109 1109 elif key == QtCore.Qt.Key_Y:
1110 1110 self._kill_ring.rotate()
1111 1111 intercepted = True
1112 1112
1113 1113 elif key == QtCore.Qt.Key_Backspace:
1114 1114 cursor = self._get_word_start_cursor(position)
1115 1115 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1116 1116 self._kill_ring.kill_cursor(cursor)
1117 1117 intercepted = True
1118 1118
1119 1119 elif key == QtCore.Qt.Key_D:
1120 1120 cursor = self._get_word_end_cursor(position)
1121 1121 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1122 1122 self._kill_ring.kill_cursor(cursor)
1123 1123 intercepted = True
1124 1124
1125 1125 elif key == QtCore.Qt.Key_Delete:
1126 1126 intercepted = True
1127 1127
1128 1128 elif key == QtCore.Qt.Key_Greater:
1129 1129 self._control.moveCursor(QtGui.QTextCursor.End)
1130 1130 intercepted = True
1131 1131
1132 1132 elif key == QtCore.Qt.Key_Less:
1133 1133 self._control.setTextCursor(self._get_prompt_cursor())
1134 1134 intercepted = True
1135 1135
1136 1136 #------ No modifiers ---------------------------------------------------
1137 1137
1138 1138 else:
1139 1139 if shift_down:
1140 1140 anchormode = QtGui.QTextCursor.KeepAnchor
1141 1141 else:
1142 1142 anchormode = QtGui.QTextCursor.MoveAnchor
1143 1143
1144 1144 if key == QtCore.Qt.Key_Escape:
1145 1145 self._keyboard_quit()
1146 1146 intercepted = True
1147 1147
1148 1148 elif key == QtCore.Qt.Key_Up:
1149 1149 if self._reading or not self._up_pressed(shift_down):
1150 1150 intercepted = True
1151 1151 else:
1152 1152 prompt_line = self._get_prompt_cursor().blockNumber()
1153 1153 intercepted = cursor.blockNumber() <= prompt_line
1154 1154
1155 1155 elif key == QtCore.Qt.Key_Down:
1156 1156 if self._reading or not self._down_pressed(shift_down):
1157 1157 intercepted = True
1158 1158 else:
1159 1159 end_line = self._get_end_cursor().blockNumber()
1160 1160 intercepted = cursor.blockNumber() == end_line
1161 1161
1162 1162 elif key == QtCore.Qt.Key_Tab:
1163 1163 if not self._reading:
1164 1164 if self._tab_pressed():
1165 1165 # real tab-key, insert four spaces
1166 1166 cursor.insertText(' '*4)
1167 1167 intercepted = True
1168 1168
1169 1169 elif key == QtCore.Qt.Key_Left:
1170 1170
1171 1171 # Move to the previous line
1172 1172 line, col = cursor.blockNumber(), cursor.columnNumber()
1173 1173 if line > self._get_prompt_cursor().blockNumber() and \
1174 1174 col == len(self._continuation_prompt):
1175 1175 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1176 1176 mode=anchormode)
1177 1177 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1178 1178 mode=anchormode)
1179 1179 intercepted = True
1180 1180
1181 1181 # Regular left movement
1182 1182 else:
1183 1183 intercepted = not self._in_buffer(position - 1)
1184 1184
1185 1185 elif key == QtCore.Qt.Key_Right:
1186 1186 original_block_number = cursor.blockNumber()
1187 1187 cursor.movePosition(QtGui.QTextCursor.Right,
1188 1188 mode=anchormode)
1189 1189 if cursor.blockNumber() != original_block_number:
1190 1190 cursor.movePosition(QtGui.QTextCursor.Right,
1191 1191 n=len(self._continuation_prompt),
1192 1192 mode=anchormode)
1193 1193 self._set_cursor(cursor)
1194 1194 intercepted = True
1195 1195
1196 1196 elif key == QtCore.Qt.Key_Home:
1197 1197 start_line = cursor.blockNumber()
1198 1198 if start_line == self._get_prompt_cursor().blockNumber():
1199 1199 start_pos = self._prompt_pos
1200 1200 else:
1201 1201 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1202 1202 QtGui.QTextCursor.KeepAnchor)
1203 1203 start_pos = cursor.position()
1204 1204 start_pos += len(self._continuation_prompt)
1205 1205 cursor.setPosition(position)
1206 1206 if shift_down and self._in_buffer(position):
1207 1207 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1208 1208 else:
1209 1209 cursor.setPosition(start_pos)
1210 1210 self._set_cursor(cursor)
1211 1211 intercepted = True
1212 1212
1213 1213 elif key == QtCore.Qt.Key_Backspace:
1214 1214
1215 1215 # Line deletion (remove continuation prompt)
1216 1216 line, col = cursor.blockNumber(), cursor.columnNumber()
1217 1217 if not self._reading and \
1218 1218 col == len(self._continuation_prompt) and \
1219 1219 line > self._get_prompt_cursor().blockNumber():
1220 1220 cursor.beginEditBlock()
1221 1221 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1222 1222 QtGui.QTextCursor.KeepAnchor)
1223 1223 cursor.removeSelectedText()
1224 1224 cursor.deletePreviousChar()
1225 1225 cursor.endEditBlock()
1226 1226 intercepted = True
1227 1227
1228 1228 # Regular backwards deletion
1229 1229 else:
1230 1230 anchor = cursor.anchor()
1231 1231 if anchor == position:
1232 1232 intercepted = not self._in_buffer(position - 1)
1233 1233 else:
1234 1234 intercepted = not self._in_buffer(min(anchor, position))
1235 1235
1236 1236 elif key == QtCore.Qt.Key_Delete:
1237 1237
1238 1238 # Line deletion (remove continuation prompt)
1239 1239 if not self._reading and self._in_buffer(position) and \
1240 1240 cursor.atBlockEnd() and not cursor.hasSelection():
1241 1241 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1242 1242 QtGui.QTextCursor.KeepAnchor)
1243 1243 cursor.movePosition(QtGui.QTextCursor.Right,
1244 1244 QtGui.QTextCursor.KeepAnchor,
1245 1245 len(self._continuation_prompt))
1246 1246 cursor.removeSelectedText()
1247 1247 intercepted = True
1248 1248
1249 1249 # Regular forwards deletion:
1250 1250 else:
1251 1251 anchor = cursor.anchor()
1252 1252 intercepted = (not self._in_buffer(anchor) or
1253 1253 not self._in_buffer(position))
1254 1254
1255 1255 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1256 1256 # using the keyboard in any part of the buffer. Also, permit scrolling
1257 1257 # with Page Up/Down keys. Finally, if we're executing, don't move the
1258 1258 # cursor (if even this made sense, we can't guarantee that the prompt
1259 1259 # position is still valid due to text truncation).
1260 1260 if not (self._control_key_down(event.modifiers(), include_command=True)
1261 1261 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1262 1262 or (self._executing and not self._reading)):
1263 1263 self._keep_cursor_in_buffer()
1264 1264
1265 1265 return intercepted
1266 1266
1267 1267 def _event_filter_page_keypress(self, event):
1268 1268 """ Filter key events for the paging widget to create console-like
1269 1269 interface.
1270 1270 """
1271 1271 key = event.key()
1272 1272 ctrl_down = self._control_key_down(event.modifiers())
1273 1273 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1274 1274
1275 1275 if ctrl_down:
1276 1276 if key == QtCore.Qt.Key_O:
1277 1277 self._control.setFocus()
1278 1278 intercept = True
1279 1279
1280 1280 elif alt_down:
1281 1281 if key == QtCore.Qt.Key_Greater:
1282 1282 self._page_control.moveCursor(QtGui.QTextCursor.End)
1283 1283 intercepted = True
1284 1284
1285 1285 elif key == QtCore.Qt.Key_Less:
1286 1286 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1287 1287 intercepted = True
1288 1288
1289 1289 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1290 1290 if self._splitter:
1291 1291 self._page_control.hide()
1292 1292 self._control.setFocus()
1293 1293 else:
1294 1294 self.layout().setCurrentWidget(self._control)
1295 1295 return True
1296 1296
1297 1297 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1298 1298 QtCore.Qt.Key_Tab):
1299 1299 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1300 1300 QtCore.Qt.Key_PageDown,
1301 1301 QtCore.Qt.NoModifier)
1302 1302 QtGui.qApp.sendEvent(self._page_control, new_event)
1303 1303 return True
1304 1304
1305 1305 elif key == QtCore.Qt.Key_Backspace:
1306 1306 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1307 1307 QtCore.Qt.Key_PageUp,
1308 1308 QtCore.Qt.NoModifier)
1309 1309 QtGui.qApp.sendEvent(self._page_control, new_event)
1310 1310 return True
1311 1311
1312 1312 return False
1313 1313
1314 1314 def _format_as_columns(self, items, separator=' '):
1315 1315 """ Transform a list of strings into a single string with columns.
1316 1316
1317 1317 Parameters
1318 1318 ----------
1319 1319 items : sequence of strings
1320 1320 The strings to process.
1321 1321
1322 1322 separator : str, optional [default is two spaces]
1323 1323 The string that separates columns.
1324 1324
1325 1325 Returns
1326 1326 -------
1327 1327 The formatted string.
1328 1328 """
1329 1329 # Calculate the number of characters available.
1330 1330 width = self._control.viewport().width()
1331 1331 char_width = QtGui.QFontMetrics(self.font).width(' ')
1332 1332 displaywidth = max(10, (width / char_width) - 1)
1333 1333
1334 1334 return columnize(items, separator, displaywidth)
1335 1335
1336 1336 def _get_block_plain_text(self, block):
1337 1337 """ Given a QTextBlock, return its unformatted text.
1338 1338 """
1339 1339 cursor = QtGui.QTextCursor(block)
1340 1340 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1341 1341 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1342 1342 QtGui.QTextCursor.KeepAnchor)
1343 1343 return cursor.selection().toPlainText()
1344 1344
1345 1345 def _get_cursor(self):
1346 1346 """ Convenience method that returns a cursor for the current position.
1347 1347 """
1348 1348 return self._control.textCursor()
1349 1349
1350 1350 def _get_end_cursor(self):
1351 1351 """ Convenience method that returns a cursor for the last character.
1352 1352 """
1353 1353 cursor = self._control.textCursor()
1354 1354 cursor.movePosition(QtGui.QTextCursor.End)
1355 1355 return cursor
1356 1356
1357 1357 def _get_input_buffer_cursor_column(self):
1358 1358 """ Returns the column of the cursor in the input buffer, excluding the
1359 1359 contribution by the prompt, or -1 if there is no such column.
1360 1360 """
1361 1361 prompt = self._get_input_buffer_cursor_prompt()
1362 1362 if prompt is None:
1363 1363 return -1
1364 1364 else:
1365 1365 cursor = self._control.textCursor()
1366 1366 return cursor.columnNumber() - len(prompt)
1367 1367
1368 1368 def _get_input_buffer_cursor_line(self):
1369 1369 """ Returns the text of the line of the input buffer that contains the
1370 1370 cursor, or None if there is no such line.
1371 1371 """
1372 1372 prompt = self._get_input_buffer_cursor_prompt()
1373 1373 if prompt is None:
1374 1374 return None
1375 1375 else:
1376 1376 cursor = self._control.textCursor()
1377 1377 text = self._get_block_plain_text(cursor.block())
1378 1378 return text[len(prompt):]
1379 1379
1380 1380 def _get_input_buffer_cursor_prompt(self):
1381 1381 """ Returns the (plain text) prompt for line of the input buffer that
1382 1382 contains the cursor, or None if there is no such line.
1383 1383 """
1384 1384 if self._executing:
1385 1385 return None
1386 1386 cursor = self._control.textCursor()
1387 1387 if cursor.position() >= self._prompt_pos:
1388 1388 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1389 1389 return self._prompt
1390 1390 else:
1391 1391 return self._continuation_prompt
1392 1392 else:
1393 1393 return None
1394 1394
1395 1395 def _get_prompt_cursor(self):
1396 1396 """ Convenience method that returns a cursor for the prompt position.
1397 1397 """
1398 1398 cursor = self._control.textCursor()
1399 1399 cursor.setPosition(self._prompt_pos)
1400 1400 return cursor
1401 1401
1402 1402 def _get_selection_cursor(self, start, end):
1403 1403 """ Convenience method that returns a cursor with text selected between
1404 1404 the positions 'start' and 'end'.
1405 1405 """
1406 1406 cursor = self._control.textCursor()
1407 1407 cursor.setPosition(start)
1408 1408 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1409 1409 return cursor
1410 1410
1411 1411 def _get_word_start_cursor(self, position):
1412 1412 """ Find the start of the word to the left the given position. If a
1413 1413 sequence of non-word characters precedes the first word, skip over
1414 1414 them. (This emulates the behavior of bash, emacs, etc.)
1415 1415 """
1416 1416 document = self._control.document()
1417 1417 position -= 1
1418 1418 while position >= self._prompt_pos and \
1419 1419 not is_letter_or_number(document.characterAt(position)):
1420 1420 position -= 1
1421 1421 while position >= self._prompt_pos and \
1422 1422 is_letter_or_number(document.characterAt(position)):
1423 1423 position -= 1
1424 1424 cursor = self._control.textCursor()
1425 1425 cursor.setPosition(position + 1)
1426 1426 return cursor
1427 1427
1428 1428 def _get_word_end_cursor(self, position):
1429 1429 """ Find the end of the word to the right the given position. If a
1430 1430 sequence of non-word characters precedes the first word, skip over
1431 1431 them. (This emulates the behavior of bash, emacs, etc.)
1432 1432 """
1433 1433 document = self._control.document()
1434 1434 end = self._get_end_cursor().position()
1435 1435 while position < end and \
1436 1436 not is_letter_or_number(document.characterAt(position)):
1437 1437 position += 1
1438 1438 while position < end and \
1439 1439 is_letter_or_number(document.characterAt(position)):
1440 1440 position += 1
1441 1441 cursor = self._control.textCursor()
1442 1442 cursor.setPosition(position)
1443 1443 return cursor
1444 1444
1445 1445 def _insert_continuation_prompt(self, cursor):
1446 1446 """ Inserts new continuation prompt using the specified cursor.
1447 1447 """
1448 1448 if self._continuation_prompt_html is None:
1449 1449 self._insert_plain_text(cursor, self._continuation_prompt)
1450 1450 else:
1451 1451 self._continuation_prompt = self._insert_html_fetching_plain_text(
1452 1452 cursor, self._continuation_prompt_html)
1453 1453
1454 1454 def _insert_html(self, cursor, html):
1455 1455 """ Inserts HTML using the specified cursor in such a way that future
1456 1456 formatting is unaffected.
1457 1457 """
1458 1458 cursor.beginEditBlock()
1459 1459 cursor.insertHtml(html)
1460 1460
1461 1461 # After inserting HTML, the text document "remembers" it's in "html
1462 1462 # mode", which means that subsequent calls adding plain text will result
1463 1463 # in unwanted formatting, lost tab characters, etc. The following code
1464 1464 # hacks around this behavior, which I consider to be a bug in Qt, by
1465 1465 # (crudely) resetting the document's style state.
1466 1466 cursor.movePosition(QtGui.QTextCursor.Left,
1467 1467 QtGui.QTextCursor.KeepAnchor)
1468 1468 if cursor.selection().toPlainText() == ' ':
1469 1469 cursor.removeSelectedText()
1470 1470 else:
1471 1471 cursor.movePosition(QtGui.QTextCursor.Right)
1472 1472 cursor.insertText(' ', QtGui.QTextCharFormat())
1473 1473 cursor.endEditBlock()
1474 1474
1475 1475 def _insert_html_fetching_plain_text(self, cursor, html):
1476 1476 """ Inserts HTML using the specified cursor, then returns its plain text
1477 1477 version.
1478 1478 """
1479 1479 cursor.beginEditBlock()
1480 1480 cursor.removeSelectedText()
1481 1481
1482 1482 start = cursor.position()
1483 1483 self._insert_html(cursor, html)
1484 1484 end = cursor.position()
1485 1485 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1486 1486 text = cursor.selection().toPlainText()
1487 1487
1488 1488 cursor.setPosition(end)
1489 1489 cursor.endEditBlock()
1490 1490 return text
1491 1491
1492 1492 def _insert_plain_text(self, cursor, text):
1493 1493 """ Inserts plain text using the specified cursor, processing ANSI codes
1494 1494 if enabled.
1495 1495 """
1496 1496 cursor.beginEditBlock()
1497 1497 if self.ansi_codes:
1498 1498 for substring in self._ansi_processor.split_string(text):
1499 1499 for act in self._ansi_processor.actions:
1500 1500
1501 1501 # Unlike real terminal emulators, we don't distinguish
1502 1502 # between the screen and the scrollback buffer. A screen
1503 1503 # erase request clears everything.
1504 1504 if act.action == 'erase' and act.area == 'screen':
1505 1505 cursor.select(QtGui.QTextCursor.Document)
1506 1506 cursor.removeSelectedText()
1507 1507
1508 1508 # Simulate a form feed by scrolling just past the last line.
1509 1509 elif act.action == 'scroll' and act.unit == 'page':
1510 1510 cursor.insertText('\n')
1511 1511 cursor.endEditBlock()
1512 1512 self._set_top_cursor(cursor)
1513 1513 cursor.joinPreviousEditBlock()
1514 1514 cursor.deletePreviousChar()
1515 1515
1516 1516 elif act.action == 'carriage-return':
1517 1517 cursor.movePosition(
1518 1518 cursor.StartOfLine, cursor.KeepAnchor)
1519 1519
1520 1520 elif act.action == 'beep':
1521 1521 QtGui.qApp.beep()
1522 1522
1523 1523 format = self._ansi_processor.get_format()
1524 1524 cursor.insertText(substring, format)
1525 1525 else:
1526 1526 cursor.insertText(text)
1527 1527 cursor.endEditBlock()
1528 1528
1529 1529 def _insert_plain_text_into_buffer(self, cursor, text):
1530 1530 """ Inserts text into the input buffer using the specified cursor (which
1531 1531 must be in the input buffer), ensuring that continuation prompts are
1532 1532 inserted as necessary.
1533 1533 """
1534 1534 lines = text.splitlines(True)
1535 1535 if lines:
1536 1536 cursor.beginEditBlock()
1537 1537 cursor.insertText(lines[0])
1538 1538 for line in lines[1:]:
1539 1539 if self._continuation_prompt_html is None:
1540 1540 cursor.insertText(self._continuation_prompt)
1541 1541 else:
1542 1542 self._continuation_prompt = \
1543 1543 self._insert_html_fetching_plain_text(
1544 1544 cursor, self._continuation_prompt_html)
1545 1545 cursor.insertText(line)
1546 1546 cursor.endEditBlock()
1547 1547
1548 1548 def _in_buffer(self, position=None):
1549 1549 """ Returns whether the current cursor (or, if specified, a position) is
1550 1550 inside the editing region.
1551 1551 """
1552 1552 cursor = self._control.textCursor()
1553 1553 if position is None:
1554 1554 position = cursor.position()
1555 1555 else:
1556 1556 cursor.setPosition(position)
1557 1557 line = cursor.blockNumber()
1558 1558 prompt_line = self._get_prompt_cursor().blockNumber()
1559 1559 if line == prompt_line:
1560 1560 return position >= self._prompt_pos
1561 1561 elif line > prompt_line:
1562 1562 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1563 1563 prompt_pos = cursor.position() + len(self._continuation_prompt)
1564 1564 return position >= prompt_pos
1565 1565 return False
1566 1566
1567 1567 def _keep_cursor_in_buffer(self):
1568 1568 """ Ensures that the cursor is inside the editing region. Returns
1569 1569 whether the cursor was moved.
1570 1570 """
1571 1571 moved = not self._in_buffer()
1572 1572 if moved:
1573 1573 cursor = self._control.textCursor()
1574 1574 cursor.movePosition(QtGui.QTextCursor.End)
1575 1575 self._control.setTextCursor(cursor)
1576 1576 return moved
1577 1577
1578 1578 def _keyboard_quit(self):
1579 1579 """ Cancels the current editing task ala Ctrl-G in Emacs.
1580 1580 """
1581 1581 if self._text_completing_pos:
1582 1582 self._cancel_text_completion()
1583 1583 else:
1584 1584 self.input_buffer = ''
1585 1585
1586 1586 def _page(self, text, html=False):
1587 1587 """ Displays text using the pager if it exceeds the height of the
1588 1588 viewport.
1589 1589
1590 1590 Parameters:
1591 1591 -----------
1592 1592 html : bool, optional (default False)
1593 1593 If set, the text will be interpreted as HTML instead of plain text.
1594 1594 """
1595 1595 line_height = QtGui.QFontMetrics(self.font).height()
1596 1596 minlines = self._control.viewport().height() / line_height
1597 1597 if self.paging != 'none' and \
1598 1598 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1599 1599 if self.paging == 'custom':
1600 1600 self.custom_page_requested.emit(text)
1601 1601 else:
1602 1602 self._page_control.clear()
1603 1603 cursor = self._page_control.textCursor()
1604 1604 if html:
1605 1605 self._insert_html(cursor, text)
1606 1606 else:
1607 1607 self._insert_plain_text(cursor, text)
1608 1608 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1609 1609
1610 1610 self._page_control.viewport().resize(self._control.size())
1611 1611 if self._splitter:
1612 1612 self._page_control.show()
1613 1613 self._page_control.setFocus()
1614 1614 else:
1615 1615 self.layout().setCurrentWidget(self._page_control)
1616 1616 elif html:
1617 1617 self._append_plain_html(text)
1618 1618 else:
1619 1619 self._append_plain_text(text)
1620 1620
1621 1621 def _prompt_finished(self):
1622 1622 """ Called immediately after a prompt is finished, i.e. when some input
1623 1623 will be processed and a new prompt displayed.
1624 1624 """
1625 1625 self._control.setReadOnly(True)
1626 1626 self._prompt_finished_hook()
1627 1627
1628 1628 def _prompt_started(self):
1629 1629 """ Called immediately after a new prompt is displayed.
1630 1630 """
1631 1631 # Temporarily disable the maximum block count to permit undo/redo and
1632 1632 # to ensure that the prompt position does not change due to truncation.
1633 1633 self._control.document().setMaximumBlockCount(0)
1634 1634 self._control.setUndoRedoEnabled(True)
1635 1635
1636 1636 # Work around bug in QPlainTextEdit: input method is not re-enabled
1637 1637 # when read-only is disabled.
1638 1638 self._control.setReadOnly(False)
1639 1639 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1640 1640
1641 1641 if not self._reading:
1642 1642 self._executing = False
1643 1643 self._prompt_started_hook()
1644 1644
1645 1645 # If the input buffer has changed while executing, load it.
1646 1646 if self._input_buffer_pending:
1647 1647 self.input_buffer = self._input_buffer_pending
1648 1648 self._input_buffer_pending = ''
1649 1649
1650 1650 self._control.moveCursor(QtGui.QTextCursor.End)
1651 1651
1652 1652 def _readline(self, prompt='', callback=None):
1653 1653 """ Reads one line of input from the user.
1654 1654
1655 1655 Parameters
1656 1656 ----------
1657 1657 prompt : str, optional
1658 1658 The prompt to print before reading the line.
1659 1659
1660 1660 callback : callable, optional
1661 1661 A callback to execute with the read line. If not specified, input is
1662 1662 read *synchronously* and this method does not return until it has
1663 1663 been read.
1664 1664
1665 1665 Returns
1666 1666 -------
1667 1667 If a callback is specified, returns nothing. Otherwise, returns the
1668 1668 input string with the trailing newline stripped.
1669 1669 """
1670 1670 if self._reading:
1671 1671 raise RuntimeError('Cannot read a line. Widget is already reading.')
1672 1672
1673 1673 if not callback and not self.isVisible():
1674 1674 # If the user cannot see the widget, this function cannot return.
1675 1675 raise RuntimeError('Cannot synchronously read a line if the widget '
1676 1676 'is not visible!')
1677 1677
1678 1678 self._reading = True
1679 1679 self._show_prompt(prompt, newline=False)
1680 1680
1681 1681 if callback is None:
1682 1682 self._reading_callback = None
1683 1683 while self._reading:
1684 1684 QtCore.QCoreApplication.processEvents()
1685 1685 return self._get_input_buffer(force=True).rstrip('\n')
1686 1686
1687 1687 else:
1688 1688 self._reading_callback = lambda: \
1689 1689 callback(self._get_input_buffer(force=True).rstrip('\n'))
1690 1690
1691 1691 def _set_continuation_prompt(self, prompt, html=False):
1692 1692 """ Sets the continuation prompt.
1693 1693
1694 1694 Parameters
1695 1695 ----------
1696 1696 prompt : str
1697 1697 The prompt to show when more input is needed.
1698 1698
1699 1699 html : bool, optional (default False)
1700 1700 If set, the prompt will be inserted as formatted HTML. Otherwise,
1701 1701 the prompt will be treated as plain text, though ANSI color codes
1702 1702 will be handled.
1703 1703 """
1704 1704 if html:
1705 1705 self._continuation_prompt_html = prompt
1706 1706 else:
1707 1707 self._continuation_prompt = prompt
1708 1708 self._continuation_prompt_html = None
1709 1709
1710 1710 def _set_cursor(self, cursor):
1711 1711 """ Convenience method to set the current cursor.
1712 1712 """
1713 1713 self._control.setTextCursor(cursor)
1714 1714
1715 1715 def _set_top_cursor(self, cursor):
1716 1716 """ Scrolls the viewport so that the specified cursor is at the top.
1717 1717 """
1718 1718 scrollbar = self._control.verticalScrollBar()
1719 1719 scrollbar.setValue(scrollbar.maximum())
1720 1720 original_cursor = self._control.textCursor()
1721 1721 self._control.setTextCursor(cursor)
1722 1722 self._control.ensureCursorVisible()
1723 1723 self._control.setTextCursor(original_cursor)
1724 1724
1725 1725 def _show_prompt(self, prompt=None, html=False, newline=True):
1726 1726 """ Writes a new prompt at the end of the buffer.
1727 1727
1728 1728 Parameters
1729 1729 ----------
1730 1730 prompt : str, optional
1731 1731 The prompt to show. If not specified, the previous prompt is used.
1732 1732
1733 1733 html : bool, optional (default False)
1734 1734 Only relevant when a prompt is specified. If set, the prompt will
1735 1735 be inserted as formatted HTML. Otherwise, the prompt will be treated
1736 1736 as plain text, though ANSI color codes will be handled.
1737 1737
1738 1738 newline : bool, optional (default True)
1739 1739 If set, a new line will be written before showing the prompt if
1740 1740 there is not already a newline at the end of the buffer.
1741 1741 """
1742 1742 # Save the current end position to support _append*(before_prompt=True).
1743 1743 cursor = self._get_end_cursor()
1744 1744 self._append_before_prompt_pos = cursor.position()
1745 1745
1746 1746 # Insert a preliminary newline, if necessary.
1747 1747 if newline and cursor.position() > 0:
1748 1748 cursor.movePosition(QtGui.QTextCursor.Left,
1749 1749 QtGui.QTextCursor.KeepAnchor)
1750 1750 if cursor.selection().toPlainText() != '\n':
1751 1751 self._append_plain_text('\n')
1752 1752
1753 1753 # Write the prompt.
1754 1754 self._append_plain_text(self._prompt_sep)
1755 1755 if prompt is None:
1756 1756 if self._prompt_html is None:
1757 1757 self._append_plain_text(self._prompt)
1758 1758 else:
1759 1759 self._append_html(self._prompt_html)
1760 1760 else:
1761 1761 if html:
1762 1762 self._prompt = self._append_html_fetching_plain_text(prompt)
1763 1763 self._prompt_html = prompt
1764 1764 else:
1765 1765 self._append_plain_text(prompt)
1766 1766 self._prompt = prompt
1767 1767 self._prompt_html = None
1768 1768
1769 1769 self._prompt_pos = self._get_end_cursor().position()
1770 1770 self._prompt_started()
1771 1771
1772 1772 #------ Signal handlers ----------------------------------------------------
1773 1773
1774 1774 def _adjust_scrollbars(self):
1775 1775 """ Expands the vertical scrollbar beyond the range set by Qt.
1776 1776 """
1777 1777 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1778 1778 # and qtextedit.cpp.
1779 1779 document = self._control.document()
1780 1780 scrollbar = self._control.verticalScrollBar()
1781 1781 viewport_height = self._control.viewport().height()
1782 1782 if isinstance(self._control, QtGui.QPlainTextEdit):
1783 1783 maximum = max(0, document.lineCount() - 1)
1784 1784 step = viewport_height / self._control.fontMetrics().lineSpacing()
1785 1785 else:
1786 1786 # QTextEdit does not do line-based layout and blocks will not in
1787 1787 # general have the same height. Therefore it does not make sense to
1788 1788 # attempt to scroll in line height increments.
1789 1789 maximum = document.size().height()
1790 1790 step = viewport_height
1791 1791 diff = maximum - scrollbar.maximum()
1792 1792 scrollbar.setRange(0, maximum)
1793 1793 scrollbar.setPageStep(step)
1794 1794
1795 1795 # Compensate for undesirable scrolling that occurs automatically due to
1796 1796 # maximumBlockCount() text truncation.
1797 1797 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1798 1798 scrollbar.setValue(scrollbar.value() + diff)
1799 1799
1800 1800 def _cursor_position_changed(self):
1801 1801 """ Clears the temporary buffer based on the cursor position.
1802 1802 """
1803 1803 if self._text_completing_pos:
1804 1804 document = self._control.document()
1805 1805 if self._text_completing_pos < document.characterCount():
1806 1806 cursor = self._control.textCursor()
1807 1807 pos = cursor.position()
1808 1808 text_cursor = self._control.textCursor()
1809 1809 text_cursor.setPosition(self._text_completing_pos)
1810 1810 if pos < self._text_completing_pos or \
1811 1811 cursor.blockNumber() > text_cursor.blockNumber():
1812 1812 self._clear_temporary_buffer()
1813 1813 self._text_completing_pos = 0
1814 1814 else:
1815 1815 self._clear_temporary_buffer()
1816 1816 self._text_completing_pos = 0
1817 1817
1818 1818 def _custom_context_menu_requested(self, pos):
1819 1819 """ Shows a context menu at the given QPoint (in widget coordinates).
1820 1820 """
1821 1821 menu = self._context_menu_make(pos)
1822 1822 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,734 +1,742 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 _transform_prompt = staticmethod(transform_classic_prompt)
110 111 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
111 112 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
112 113 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
113 114 _input_splitter_class = InputSplitter
114 115 _local_kernel = False
115 116 _highlighter = Instance(FrontendHighlighter)
116 117
117 118 #---------------------------------------------------------------------------
118 119 # 'object' interface
119 120 #---------------------------------------------------------------------------
120 121
121 122 def __init__(self, *args, **kw):
122 123 super(FrontendWidget, self).__init__(*args, **kw)
123 124 # FIXME: remove this when PySide min version is updated past 1.0.7
124 125 # forcefully disable calltips if PySide is < 1.0.7, because they crash
125 126 if qt.QT_API == qt.QT_API_PYSIDE:
126 127 import PySide
127 128 if PySide.__version_info__ < (1,0,7):
128 129 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
129 130 self.enable_calltips = False
130 131
131 132 # FrontendWidget protected variables.
132 133 self._bracket_matcher = BracketMatcher(self._control)
133 134 self._call_tip_widget = CallTipWidget(self._control)
134 135 self._completion_lexer = CompletionLexer(PythonLexer())
135 136 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
136 137 self._hidden = False
137 138 self._highlighter = FrontendHighlighter(self)
138 139 self._input_splitter = self._input_splitter_class(input_mode='cell')
139 140 self._kernel_manager = None
140 141 self._request_info = {}
141 142 self._request_info['execute'] = {};
142 143 self._callback_dict = {}
143 144
144 145 # Configure the ConsoleWidget.
145 146 self.tab_width = 4
146 147 self._set_continuation_prompt('... ')
147 148
148 149 # Configure the CallTipWidget.
149 150 self._call_tip_widget.setFont(self.font)
150 151 self.font_changed.connect(self._call_tip_widget.setFont)
151 152
152 153 # Configure actions.
153 154 action = self._copy_raw_action
154 155 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
155 156 action.setEnabled(False)
156 157 action.setShortcut(QtGui.QKeySequence(key))
157 158 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
158 159 action.triggered.connect(self.copy_raw)
159 160 self.copy_available.connect(action.setEnabled)
160 161 self.addAction(action)
161 162
162 163 # Connect signal handlers.
163 164 document = self._control.document()
164 165 document.contentsChange.connect(self._document_contents_change)
165 166
166 167 # Set flag for whether we are connected via localhost.
167 168 self._local_kernel = kw.get('local_kernel',
168 169 FrontendWidget._local_kernel)
169 170
170 171 #---------------------------------------------------------------------------
171 172 # 'ConsoleWidget' public interface
172 173 #---------------------------------------------------------------------------
173 174
174 175 def copy(self):
175 176 """ Copy the currently selected text to the clipboard, removing prompts.
176 177 """
177 text = self._control.textCursor().selection().toPlainText()
178 if text:
179 lines = map(transform_classic_prompt, text.splitlines())
180 text = '\n'.join(lines)
181 QtGui.QApplication.clipboard().setText(text)
178 if self._page_control.hasFocus():
179 self._page_control.copy()
180 elif self._control.hasFocus():
181 text = self._control.textCursor().selection().toPlainText()
182 if text:
183 lines = map(self._transform_prompt, text.splitlines())
184 text = '\n'.join(lines)
185 QtGui.QApplication.clipboard().setText(text)
186 else:
187 self.log.debug("frontend widget : unknown copy target")
182 188
183 189 #---------------------------------------------------------------------------
184 190 # 'ConsoleWidget' abstract interface
185 191 #---------------------------------------------------------------------------
186 192
187 193 def _is_complete(self, source, interactive):
188 194 """ Returns whether 'source' can be completely processed and a new
189 195 prompt created. When triggered by an Enter/Return key press,
190 196 'interactive' is True; otherwise, it is False.
191 197 """
192 198 complete = self._input_splitter.push(source)
193 199 if interactive:
194 200 complete = not self._input_splitter.push_accepts_more()
195 201 return complete
196 202
197 203 def _execute(self, source, hidden):
198 204 """ Execute 'source'. If 'hidden', do not show any output.
199 205
200 206 See parent class :meth:`execute` docstring for full details.
201 207 """
202 208 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
203 209 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
204 210 self._hidden = hidden
205 211 if not hidden:
206 212 self.executing.emit(source)
207 213
208 214 def _prompt_started_hook(self):
209 215 """ Called immediately after a new prompt is displayed.
210 216 """
211 217 if not self._reading:
212 218 self._highlighter.highlighting_on = True
213 219
214 220 def _prompt_finished_hook(self):
215 221 """ Called immediately after a prompt is finished, i.e. when some input
216 222 will be processed and a new prompt displayed.
217 223 """
218 224 # Flush all state from the input splitter so the next round of
219 225 # reading input starts with a clean buffer.
220 226 self._input_splitter.reset()
221 227
222 228 if not self._reading:
223 229 self._highlighter.highlighting_on = False
224 230
225 231 def _tab_pressed(self):
226 232 """ Called when the tab key is pressed. Returns whether to continue
227 233 processing the event.
228 234 """
229 235 # Perform tab completion if:
230 236 # 1) The cursor is in the input buffer.
231 237 # 2) There is a non-whitespace character before the cursor.
232 238 text = self._get_input_buffer_cursor_line()
233 239 if text is None:
234 240 return False
235 241 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
236 242 if complete:
237 243 self._complete()
238 244 return not complete
239 245
240 246 #---------------------------------------------------------------------------
241 247 # 'ConsoleWidget' protected interface
242 248 #---------------------------------------------------------------------------
243 249
244 250 def _context_menu_make(self, pos):
245 251 """ Reimplemented to add an action for raw copy.
246 252 """
247 253 menu = super(FrontendWidget, self)._context_menu_make(pos)
248 254 for before_action in menu.actions():
249 255 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
250 256 QtGui.QKeySequence.ExactMatch:
251 257 menu.insertAction(before_action, self._copy_raw_action)
252 258 break
253 259 return menu
254 260
255 261 def request_interrupt_kernel(self):
256 262 if self._executing:
257 263 self.interrupt_kernel()
258 264
259 265 def request_restart_kernel(self):
260 266 message = 'Are you sure you want to restart the kernel?'
261 267 self.restart_kernel(message, now=False)
262 268
263 269 def _event_filter_console_keypress(self, event):
264 270 """ Reimplemented for execution interruption and smart backspace.
265 271 """
266 272 key = event.key()
267 273 if self._control_key_down(event.modifiers(), include_command=False):
268 274
269 275 if key == QtCore.Qt.Key_C and self._executing:
270 276 self.request_interrupt_kernel()
271 277 return True
272 278
273 279 elif key == QtCore.Qt.Key_Period:
274 280 self.request_restart_kernel()
275 281 return True
276 282
277 283 elif not event.modifiers() & QtCore.Qt.AltModifier:
278 284
279 285 # Smart backspace: remove four characters in one backspace if:
280 286 # 1) everything left of the cursor is whitespace
281 287 # 2) the four characters immediately left of the cursor are spaces
282 288 if key == QtCore.Qt.Key_Backspace:
283 289 col = self._get_input_buffer_cursor_column()
284 290 cursor = self._control.textCursor()
285 291 if col > 3 and not cursor.hasSelection():
286 292 text = self._get_input_buffer_cursor_line()[:col]
287 293 if text.endswith(' ') and not text.strip():
288 294 cursor.movePosition(QtGui.QTextCursor.Left,
289 295 QtGui.QTextCursor.KeepAnchor, 4)
290 296 cursor.removeSelectedText()
291 297 return True
292 298
293 299 return super(FrontendWidget, self)._event_filter_console_keypress(event)
294 300
295 301 def _insert_continuation_prompt(self, cursor):
296 302 """ Reimplemented for auto-indentation.
297 303 """
298 304 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
299 305 cursor.insertText(' ' * self._input_splitter.indent_spaces)
300 306
301 307 #---------------------------------------------------------------------------
302 308 # 'BaseFrontendMixin' abstract interface
303 309 #---------------------------------------------------------------------------
304 310
305 311 def _handle_complete_reply(self, rep):
306 312 """ Handle replies for tab completion.
307 313 """
308 314 self.log.debug("complete: %s", rep.get('content', ''))
309 315 cursor = self._get_cursor()
310 316 info = self._request_info.get('complete')
311 317 if info and info.id == rep['parent_header']['msg_id'] and \
312 318 info.pos == cursor.position():
313 319 text = '.'.join(self._get_context())
314 320 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
315 321 self._complete_with_items(cursor, rep['content']['matches'])
316 322
317 323 def _silent_exec_callback(self, expr, callback):
318 324 """Silently execute `expr` in the kernel and call `callback` with reply
319 325
320 326 the `expr` is evaluated silently in the kernel (without) output in
321 327 the frontend. Call `callback` with the
322 328 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
323 329
324 330 Parameters
325 331 ----------
326 332 expr : string
327 333 valid string to be executed by the kernel.
328 334 callback : function
329 335 function accepting one arguement, as a string. The string will be
330 336 the `repr` of the result of evaluating `expr`
331 337
332 338 The `callback` is called with the 'repr()' of the result of `expr` as
333 339 first argument. To get the object, do 'eval()' onthe passed value.
334 340
335 341 See Also
336 342 --------
337 343 _handle_exec_callback : private method, deal with calling callback with reply
338 344
339 345 """
340 346
341 347 # generate uuid, which would be used as a indication of wether or not
342 348 # the unique request originate from here (can use msg id ?)
343 349 local_uuid = str(uuid.uuid1())
344 350 msg_id = self.kernel_manager.shell_channel.execute('',
345 351 silent=True, user_expressions={ local_uuid:expr })
346 352 self._callback_dict[local_uuid] = callback
347 353 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
348 354
349 355 def _handle_exec_callback(self, msg):
350 356 """Execute `callback` corresonding to `msg` reply, after ``_silent_exec_callback``
351 357
352 358 Parameters
353 359 ----------
354 360 msg : raw message send by the kernel containing an `user_expressions`
355 361 and having a 'silent_exec_callback' kind.
356 362
357 363 Notes
358 364 -----
359 365 This fonction will look for a `callback` associated with the
360 366 corresponding message id. Association has been made by
361 367 `_silent_exec_callback`. `callback` is then called with the `repr()`
362 368 of the value of corresponding `user_expressions` as argument.
363 369 `callback` is then removed from the known list so that any message
364 370 coming again with the same id won't trigger it.
365 371
366 372 """
367 373
368 user_exp = msg['content']['user_expressions']
374 user_exp = msg['content'].get('user_expressions')
375 if not user_exp:
376 return
369 377 for expression in user_exp:
370 378 if expression in self._callback_dict:
371 379 self._callback_dict.pop(expression)(user_exp[expression])
372 380
373 381 def _handle_execute_reply(self, msg):
374 382 """ Handles replies for code execution.
375 383 """
376 384 self.log.debug("execute: %s", msg.get('content', ''))
377 385 msg_id = msg['parent_header']['msg_id']
378 386 info = self._request_info['execute'].get(msg_id)
379 387 # unset reading flag, because if execute finished, raw_input can't
380 388 # still be pending.
381 389 self._reading = False
382 390 if info and info.kind == 'user' and not self._hidden:
383 391 # Make sure that all output from the SUB channel has been processed
384 392 # before writing a new prompt.
385 393 self.kernel_manager.sub_channel.flush()
386 394
387 395 # Reset the ANSI style information to prevent bad text in stdout
388 396 # from messing up our colors. We're not a true terminal so we're
389 397 # allowed to do this.
390 398 if self.ansi_codes:
391 399 self._ansi_processor.reset_sgr()
392 400
393 401 content = msg['content']
394 402 status = content['status']
395 403 if status == 'ok':
396 404 self._process_execute_ok(msg)
397 405 elif status == 'error':
398 406 self._process_execute_error(msg)
399 407 elif status == 'aborted':
400 408 self._process_execute_abort(msg)
401 409
402 410 self._show_interpreter_prompt_for_reply(msg)
403 411 self.executed.emit(msg)
404 412 self._request_info['execute'].pop(msg_id)
405 413 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
406 414 self._handle_exec_callback(msg)
407 415 self._request_info['execute'].pop(msg_id)
408 416 else:
409 417 super(FrontendWidget, self)._handle_execute_reply(msg)
410 418
411 419 def _handle_input_request(self, msg):
412 420 """ Handle requests for raw_input.
413 421 """
414 422 self.log.debug("input: %s", msg.get('content', ''))
415 423 if self._hidden:
416 424 raise RuntimeError('Request for raw input during hidden execution.')
417 425
418 426 # Make sure that all output from the SUB channel has been processed
419 427 # before entering readline mode.
420 428 self.kernel_manager.sub_channel.flush()
421 429
422 430 def callback(line):
423 431 self.kernel_manager.stdin_channel.input(line)
424 432 if self._reading:
425 433 self.log.debug("Got second input request, assuming first was interrupted.")
426 434 self._reading = False
427 435 self._readline(msg['content']['prompt'], callback=callback)
428 436
429 437 def _handle_kernel_died(self, since_last_heartbeat):
430 438 """ Handle the kernel's death by asking if the user wants to restart.
431 439 """
432 440 self.log.debug("kernel died: %s", since_last_heartbeat)
433 441 if self.custom_restart:
434 442 self.custom_restart_kernel_died.emit(since_last_heartbeat)
435 443 else:
436 444 message = 'The kernel heartbeat has been inactive for %.2f ' \
437 445 'seconds. Do you want to restart the kernel? You may ' \
438 446 'first want to check the network connection.' % \
439 447 since_last_heartbeat
440 448 self.restart_kernel(message, now=True)
441 449
442 450 def _handle_object_info_reply(self, rep):
443 451 """ Handle replies for call tips.
444 452 """
445 453 self.log.debug("oinfo: %s", rep.get('content', ''))
446 454 cursor = self._get_cursor()
447 455 info = self._request_info.get('call_tip')
448 456 if info and info.id == rep['parent_header']['msg_id'] and \
449 457 info.pos == cursor.position():
450 458 # Get the information for a call tip. For now we format the call
451 459 # line as string, later we can pass False to format_call and
452 460 # syntax-highlight it ourselves for nicer formatting in the
453 461 # calltip.
454 462 content = rep['content']
455 463 # if this is from pykernel, 'docstring' will be the only key
456 464 if content.get('ismagic', False):
457 465 # Don't generate a call-tip for magics. Ideally, we should
458 466 # generate a tooltip, but not on ( like we do for actual
459 467 # callables.
460 468 call_info, doc = None, None
461 469 else:
462 470 call_info, doc = call_tip(content, format_call=True)
463 471 if call_info or doc:
464 472 self._call_tip_widget.show_call_info(call_info, doc)
465 473
466 474 def _handle_pyout(self, msg):
467 475 """ Handle display hook output.
468 476 """
469 477 self.log.debug("pyout: %s", msg.get('content', ''))
470 478 if not self._hidden and self._is_from_this_session(msg):
471 479 text = msg['content']['data']
472 480 self._append_plain_text(text + '\n', before_prompt=True)
473 481
474 482 def _handle_stream(self, msg):
475 483 """ Handle stdout, stderr, and stdin.
476 484 """
477 485 self.log.debug("stream: %s", msg.get('content', ''))
478 486 if not self._hidden and self._is_from_this_session(msg):
479 487 # Most consoles treat tabs as being 8 space characters. Convert tabs
480 488 # to spaces so that output looks as expected regardless of this
481 489 # widget's tab width.
482 490 text = msg['content']['data'].expandtabs(8)
483 491
484 492 self._append_plain_text(text, before_prompt=True)
485 493 self._control.moveCursor(QtGui.QTextCursor.End)
486 494
487 495 def _handle_shutdown_reply(self, msg):
488 496 """ Handle shutdown signal, only if from other console.
489 497 """
490 498 self.log.debug("shutdown: %s", msg.get('content', ''))
491 499 if not self._hidden and not self._is_from_this_session(msg):
492 500 if self._local_kernel:
493 501 if not msg['content']['restart']:
494 502 self.exit_requested.emit(self)
495 503 else:
496 504 # we just got notified of a restart!
497 505 time.sleep(0.25) # wait 1/4 sec to reset
498 506 # lest the request for a new prompt
499 507 # goes to the old kernel
500 508 self.reset()
501 509 else: # remote kernel, prompt on Kernel shutdown/reset
502 510 title = self.window().windowTitle()
503 511 if not msg['content']['restart']:
504 512 reply = QtGui.QMessageBox.question(self, title,
505 513 "Kernel has been shutdown permanently. "
506 514 "Close the Console?",
507 515 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
508 516 if reply == QtGui.QMessageBox.Yes:
509 517 self.exit_requested.emit(self)
510 518 else:
511 519 reply = QtGui.QMessageBox.question(self, title,
512 520 "Kernel has been reset. Clear the Console?",
513 521 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
514 522 if reply == QtGui.QMessageBox.Yes:
515 523 time.sleep(0.25) # wait 1/4 sec to reset
516 524 # lest the request for a new prompt
517 525 # goes to the old kernel
518 526 self.reset()
519 527
520 528 def _started_channels(self):
521 529 """ Called when the KernelManager channels have started listening or
522 530 when the frontend is assigned an already listening KernelManager.
523 531 """
524 532 self.reset()
525 533
526 534 #---------------------------------------------------------------------------
527 535 # 'FrontendWidget' public interface
528 536 #---------------------------------------------------------------------------
529 537
530 538 def copy_raw(self):
531 539 """ Copy the currently selected text to the clipboard without attempting
532 540 to remove prompts or otherwise alter the text.
533 541 """
534 542 self._control.copy()
535 543
536 544 def execute_file(self, path, hidden=False):
537 545 """ Attempts to execute file with 'path'. If 'hidden', no output is
538 546 shown.
539 547 """
540 548 self.execute('execfile(%r)' % path, hidden=hidden)
541 549
542 550 def interrupt_kernel(self):
543 551 """ Attempts to interrupt the running kernel.
544 552
545 553 Also unsets _reading flag, to avoid runtime errors
546 554 if raw_input is called again.
547 555 """
548 556 if self.custom_interrupt:
549 557 self._reading = False
550 558 self.custom_interrupt_requested.emit()
551 559 elif self.kernel_manager.has_kernel:
552 560 self._reading = False
553 561 self.kernel_manager.interrupt_kernel()
554 562 else:
555 563 self._append_plain_text('Kernel process is either remote or '
556 564 'unspecified. Cannot interrupt.\n')
557 565
558 566 def reset(self):
559 567 """ Resets the widget to its initial state. Similar to ``clear``, but
560 568 also re-writes the banner and aborts execution if necessary.
561 569 """
562 570 if self._executing:
563 571 self._executing = False
564 572 self._request_info['execute'] = {}
565 573 self._reading = False
566 574 self._highlighter.highlighting_on = False
567 575
568 576 self._control.clear()
569 577 self._append_plain_text(self.banner)
570 578 # update output marker for stdout/stderr, so that startup
571 579 # messages appear after banner:
572 580 self._append_before_prompt_pos = self._get_cursor().position()
573 581 self._show_interpreter_prompt()
574 582
575 583 def restart_kernel(self, message, now=False):
576 584 """ Attempts to restart the running kernel.
577 585 """
578 586 # FIXME: now should be configurable via a checkbox in the dialog. Right
579 587 # now at least the heartbeat path sets it to True and the manual restart
580 588 # to False. But those should just be the pre-selected states of a
581 589 # checkbox that the user could override if so desired. But I don't know
582 590 # enough Qt to go implementing the checkbox now.
583 591
584 592 if self.custom_restart:
585 593 self.custom_restart_requested.emit()
586 594
587 595 elif self.kernel_manager.has_kernel:
588 596 # Pause the heart beat channel to prevent further warnings.
589 597 self.kernel_manager.hb_channel.pause()
590 598
591 599 # Prompt the user to restart the kernel. Un-pause the heartbeat if
592 600 # they decline. (If they accept, the heartbeat will be un-paused
593 601 # automatically when the kernel is restarted.)
594 602 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
595 603 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
596 604 message, buttons)
597 605 if result == QtGui.QMessageBox.Yes:
598 606 try:
599 607 self.kernel_manager.restart_kernel(now=now)
600 608 except RuntimeError:
601 609 self._append_plain_text('Kernel started externally. '
602 610 'Cannot restart.\n',
603 611 before_prompt=True
604 612 )
605 613 else:
606 614 self.reset()
607 615 else:
608 616 self.kernel_manager.hb_channel.unpause()
609 617
610 618 else:
611 619 self._append_plain_text('Kernel process is either remote or '
612 620 'unspecified. Cannot restart.\n',
613 621 before_prompt=True
614 622 )
615 623
616 624 #---------------------------------------------------------------------------
617 625 # 'FrontendWidget' protected interface
618 626 #---------------------------------------------------------------------------
619 627
620 628 def _call_tip(self):
621 629 """ Shows a call tip, if appropriate, at the current cursor location.
622 630 """
623 631 # Decide if it makes sense to show a call tip
624 632 if not self.enable_calltips:
625 633 return False
626 634 cursor = self._get_cursor()
627 635 cursor.movePosition(QtGui.QTextCursor.Left)
628 636 if cursor.document().characterAt(cursor.position()) != '(':
629 637 return False
630 638 context = self._get_context(cursor)
631 639 if not context:
632 640 return False
633 641
634 642 # Send the metadata request to the kernel
635 643 name = '.'.join(context)
636 644 msg_id = self.kernel_manager.shell_channel.object_info(name)
637 645 pos = self._get_cursor().position()
638 646 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
639 647 return True
640 648
641 649 def _complete(self):
642 650 """ Performs completion at the current cursor location.
643 651 """
644 652 context = self._get_context()
645 653 if context:
646 654 # Send the completion request to the kernel
647 655 msg_id = self.kernel_manager.shell_channel.complete(
648 656 '.'.join(context), # text
649 657 self._get_input_buffer_cursor_line(), # line
650 658 self._get_input_buffer_cursor_column(), # cursor_pos
651 659 self.input_buffer) # block
652 660 pos = self._get_cursor().position()
653 661 info = self._CompletionRequest(msg_id, pos)
654 662 self._request_info['complete'] = info
655 663
656 664 def _get_context(self, cursor=None):
657 665 """ Gets the context for the specified cursor (or the current cursor
658 666 if none is specified).
659 667 """
660 668 if cursor is None:
661 669 cursor = self._get_cursor()
662 670 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
663 671 QtGui.QTextCursor.KeepAnchor)
664 672 text = cursor.selection().toPlainText()
665 673 return self._completion_lexer.get_context(text)
666 674
667 675 def _process_execute_abort(self, msg):
668 676 """ Process a reply for an aborted execution request.
669 677 """
670 678 self._append_plain_text("ERROR: execution aborted\n")
671 679
672 680 def _process_execute_error(self, msg):
673 681 """ Process a reply for an execution request that resulted in an error.
674 682 """
675 683 content = msg['content']
676 684 # If a SystemExit is passed along, this means exit() was called - also
677 685 # all the ipython %exit magic syntax of '-k' to be used to keep
678 686 # the kernel running
679 687 if content['ename']=='SystemExit':
680 688 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
681 689 self._keep_kernel_on_exit = keepkernel
682 690 self.exit_requested.emit(self)
683 691 else:
684 692 traceback = ''.join(content['traceback'])
685 693 self._append_plain_text(traceback)
686 694
687 695 def _process_execute_ok(self, msg):
688 696 """ Process a reply for a successful execution equest.
689 697 """
690 698 payload = msg['content']['payload']
691 699 for item in payload:
692 700 if not self._process_execute_payload(item):
693 701 warning = 'Warning: received unknown payload of type %s'
694 702 print(warning % repr(item['source']))
695 703
696 704 def _process_execute_payload(self, item):
697 705 """ Process a single payload item from the list of payload items in an
698 706 execution reply. Returns whether the payload was handled.
699 707 """
700 708 # The basic FrontendWidget doesn't handle payloads, as they are a
701 709 # mechanism for going beyond the standard Python interpreter model.
702 710 return False
703 711
704 712 def _show_interpreter_prompt(self):
705 713 """ Shows a prompt for the interpreter.
706 714 """
707 715 self._show_prompt('>>> ')
708 716
709 717 def _show_interpreter_prompt_for_reply(self, msg):
710 718 """ Shows a prompt for the interpreter given an 'execute_reply' message.
711 719 """
712 720 self._show_interpreter_prompt()
713 721
714 722 #------ Signal handlers ----------------------------------------------------
715 723
716 724 def _document_contents_change(self, position, removed, added):
717 725 """ Called whenever the document's content changes. Display a call tip
718 726 if appropriate.
719 727 """
720 728 # Calculate where the cursor should be *after* the change:
721 729 position += added
722 730
723 731 document = self._control.document()
724 732 if position == self._get_cursor().position():
725 733 self._call_tip()
726 734
727 735 #------ Trait default initializers -----------------------------------------
728 736
729 737 def _banner_default(self):
730 738 """ Returns the standard Python banner.
731 739 """
732 740 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
733 741 '"license" for more information.'
734 742 return banner % (sys.version, sys.platform)
@@ -1,563 +1,554 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 _transform_prompt = staticmethod(transform_ipy_prompt)
101 102
102 103 # IPythonWidget protected class variables.
103 104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 105 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 106 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 108 _payload_source_page = 'IPython.zmq.page.page'
108 109 _retrying_history_request = False
109 110
110 111 #---------------------------------------------------------------------------
111 112 # 'object' interface
112 113 #---------------------------------------------------------------------------
113 114
114 115 def __init__(self, *args, **kw):
115 116 super(IPythonWidget, self).__init__(*args, **kw)
116 117
117 118 # IPythonWidget protected variables.
118 119 self._payload_handlers = {
119 120 self._payload_source_edit : self._handle_payload_edit,
120 121 self._payload_source_exit : self._handle_payload_exit,
121 122 self._payload_source_page : self._handle_payload_page,
122 123 self._payload_source_next_input : self._handle_payload_next_input }
123 124 self._previous_prompt_obj = None
124 125 self._keep_kernel_on_exit = None
125 126
126 127 # Initialize widget styling.
127 128 if self.style_sheet:
128 129 self._style_sheet_changed()
129 130 self._syntax_style_changed()
130 131 else:
131 132 self.set_default_style()
132 133
133 134 #---------------------------------------------------------------------------
134 135 # 'BaseFrontendMixin' abstract interface
135 136 #---------------------------------------------------------------------------
136 137
137 138 def _handle_complete_reply(self, rep):
138 139 """ Reimplemented to support IPython's improved completion machinery.
139 140 """
140 141 self.log.debug("complete: %s", rep.get('content', ''))
141 142 cursor = self._get_cursor()
142 143 info = self._request_info.get('complete')
143 144 if info and info.id == rep['parent_header']['msg_id'] and \
144 145 info.pos == cursor.position():
145 146 matches = rep['content']['matches']
146 147 text = rep['content']['matched_text']
147 148 offset = len(text)
148 149
149 150 # Clean up matches with period and path separators if the matched
150 151 # text has not been transformed. This is done by truncating all
151 152 # but the last component and then suitably decreasing the offset
152 153 # between the current cursor position and the start of completion.
153 154 if len(matches) > 1 and matches[0][:offset] == text:
154 155 parts = re.split(r'[./\\]', text)
155 156 sep_count = len(parts) - 1
156 157 if sep_count:
157 158 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 159 matches = [ match[chop_length:] for match in matches ]
159 160 offset -= chop_length
160 161
161 162 # Move the cursor to the start of the match and complete.
162 163 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 164 self._complete_with_items(cursor, matches)
164 165
165 166 def _handle_execute_reply(self, msg):
166 167 """ Reimplemented to support prompt requests.
167 168 """
168 169 msg_id = msg['parent_header'].get('msg_id')
169 170 info = self._request_info['execute'].get(msg_id)
170 171 if info and info.kind == 'prompt':
171 172 number = msg['content']['execution_count'] + 1
172 173 self._show_interpreter_prompt(number)
173 174 self._request_info['execute'].pop(msg_id)
174 175 else:
175 176 super(IPythonWidget, self)._handle_execute_reply(msg)
176 177
177 178 def _handle_history_reply(self, msg):
178 179 """ Implemented to handle history tail replies, which are only supported
179 180 by the IPython kernel.
180 181 """
181 182 self.log.debug("history: %s", msg.get('content', ''))
182 183 content = msg['content']
183 184 if 'history' not in content:
184 185 self.log.error("History request failed: %r"%content)
185 186 if content.get('status', '') == 'aborted' and \
186 187 not self._retrying_history_request:
187 188 # a *different* action caused this request to be aborted, so
188 189 # we should try again.
189 190 self.log.error("Retrying aborted history request")
190 191 # prevent multiple retries of aborted requests:
191 192 self._retrying_history_request = True
192 193 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 194 time.sleep(0.25)
194 195 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 196 else:
196 197 self._retrying_history_request = False
197 198 return
198 199 # reset retry flag
199 200 self._retrying_history_request = False
200 201 history_items = content['history']
201 202 items = []
202 203 last_cell = u""
203 204 for _, _, cell in history_items:
204 205 cell = cell.rstrip()
205 206 if cell != last_cell:
206 207 items.append(cell)
207 208 last_cell = cell
208 209 self._set_history(items)
209 210
210 211 def _handle_pyout(self, msg):
211 212 """ Reimplemented for IPython-style "display hook".
212 213 """
213 214 self.log.debug("pyout: %s", msg.get('content', ''))
214 215 if not self._hidden and self._is_from_this_session(msg):
215 216 content = msg['content']
216 217 prompt_number = content['execution_count']
217 218 data = content['data']
218 219 if data.has_key('text/html'):
219 220 self._append_plain_text(self.output_sep, True)
220 221 self._append_html(self._make_out_prompt(prompt_number), True)
221 222 html = data['text/html']
222 223 self._append_plain_text('\n', True)
223 224 self._append_html(html + self.output_sep2, True)
224 225 elif data.has_key('text/plain'):
225 226 self._append_plain_text(self.output_sep, True)
226 227 self._append_html(self._make_out_prompt(prompt_number), True)
227 228 text = data['text/plain']
228 229 # If the repr is multiline, make sure we start on a new line,
229 230 # so that its lines are aligned.
230 231 if "\n" in text and not self.output_sep.endswith("\n"):
231 232 self._append_plain_text('\n', True)
232 233 self._append_plain_text(text + self.output_sep2, True)
233 234
234 235 def _handle_display_data(self, msg):
235 236 """ The base handler for the ``display_data`` message.
236 237 """
237 238 self.log.debug("display: %s", msg.get('content', ''))
238 239 # For now, we don't display data from other frontends, but we
239 240 # eventually will as this allows all frontends to monitor the display
240 241 # data. But we need to figure out how to handle this in the GUI.
241 242 if not self._hidden and self._is_from_this_session(msg):
242 243 source = msg['content']['source']
243 244 data = msg['content']['data']
244 245 metadata = msg['content']['metadata']
245 246 # In the regular IPythonWidget, we simply print the plain text
246 247 # representation.
247 248 if data.has_key('text/html'):
248 249 html = data['text/html']
249 250 self._append_html(html, True)
250 251 elif data.has_key('text/plain'):
251 252 text = data['text/plain']
252 253 self._append_plain_text(text, True)
253 254 # This newline seems to be needed for text and html output.
254 255 self._append_plain_text(u'\n', True)
255 256
256 257 def _started_channels(self):
257 258 """ Reimplemented to make a history request.
258 259 """
259 260 super(IPythonWidget, self)._started_channels()
260 261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
261 262 n=1000)
262 263 #---------------------------------------------------------------------------
263 264 # 'ConsoleWidget' public interface
264 265 #---------------------------------------------------------------------------
265 266
266 def copy(self):
267 """ Copy the currently selected text to the clipboard, removing prompts
268 if possible.
269 """
270 text = self._control.textCursor().selection().toPlainText()
271 if text:
272 lines = map(transform_ipy_prompt, text.splitlines())
273 text = '\n'.join(lines)
274 QtGui.QApplication.clipboard().setText(text)
275
276 267 #---------------------------------------------------------------------------
277 268 # 'FrontendWidget' public interface
278 269 #---------------------------------------------------------------------------
279 270
280 271 def execute_file(self, path, hidden=False):
281 272 """ Reimplemented to use the 'run' magic.
282 273 """
283 274 # Use forward slashes on Windows to avoid escaping each separator.
284 275 if sys.platform == 'win32':
285 276 path = os.path.normpath(path).replace('\\', '/')
286 277
287 278 # Perhaps we should not be using %run directly, but while we
288 279 # are, it is necessary to quote filenames containing spaces or quotes.
289 280 # Escaping quotes in filename in %run seems tricky and inconsistent,
290 281 # so not trying it at present.
291 282 if '"' in path:
292 283 if "'" in path:
293 284 raise ValueError("Can't run filename containing both single "
294 285 "and double quotes: %s" % path)
295 286 path = "'%s'" % path
296 287 elif ' ' in path or "'" in path:
297 288 path = '"%s"' % path
298 289
299 290 self.execute('%%run %s' % path, hidden=hidden)
300 291
301 292 #---------------------------------------------------------------------------
302 293 # 'FrontendWidget' protected interface
303 294 #---------------------------------------------------------------------------
304 295
305 296 def _complete(self):
306 297 """ Reimplemented to support IPython's improved completion machinery.
307 298 """
308 299 # We let the kernel split the input line, so we *always* send an empty
309 300 # text field. Readline-based frontends do get a real text field which
310 301 # they can use.
311 302 text = ''
312 303
313 304 # Send the completion request to the kernel
314 305 msg_id = self.kernel_manager.shell_channel.complete(
315 306 text, # text
316 307 self._get_input_buffer_cursor_line(), # line
317 308 self._get_input_buffer_cursor_column(), # cursor_pos
318 309 self.input_buffer) # block
319 310 pos = self._get_cursor().position()
320 311 info = self._CompletionRequest(msg_id, pos)
321 312 self._request_info['complete'] = info
322 313
323 314 def _process_execute_error(self, msg):
324 315 """ Reimplemented for IPython-style traceback formatting.
325 316 """
326 317 content = msg['content']
327 318 traceback = '\n'.join(content['traceback']) + '\n'
328 319 if False:
329 320 # FIXME: For now, tracebacks come as plain text, so we can't use
330 321 # the html renderer yet. Once we refactor ultratb to produce
331 322 # properly styled tracebacks, this branch should be the default
332 323 traceback = traceback.replace(' ', '&nbsp;')
333 324 traceback = traceback.replace('\n', '<br/>')
334 325
335 326 ename = content['ename']
336 327 ename_styled = '<span class="error">%s</span>' % ename
337 328 traceback = traceback.replace(ename, ename_styled)
338 329
339 330 self._append_html(traceback)
340 331 else:
341 332 # This is the fallback for now, using plain text with ansi escapes
342 333 self._append_plain_text(traceback)
343 334
344 335 def _process_execute_payload(self, item):
345 336 """ Reimplemented to dispatch payloads to handler methods.
346 337 """
347 338 handler = self._payload_handlers.get(item['source'])
348 339 if handler is None:
349 340 # We have no handler for this type of payload, simply ignore it
350 341 return False
351 342 else:
352 343 handler(item)
353 344 return True
354 345
355 346 def _show_interpreter_prompt(self, number=None):
356 347 """ Reimplemented for IPython-style prompts.
357 348 """
358 349 # If a number was not specified, make a prompt number request.
359 350 if number is None:
360 351 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
361 352 info = self._ExecutionRequest(msg_id, 'prompt')
362 353 self._request_info['execute'][msg_id] = info
363 354 return
364 355
365 356 # Show a new prompt and save information about it so that it can be
366 357 # updated later if the prompt number turns out to be wrong.
367 358 self._prompt_sep = self.input_sep
368 359 self._show_prompt(self._make_in_prompt(number), html=True)
369 360 block = self._control.document().lastBlock()
370 361 length = len(self._prompt)
371 362 self._previous_prompt_obj = self._PromptBlock(block, length, number)
372 363
373 364 # Update continuation prompt to reflect (possibly) new prompt length.
374 365 self._set_continuation_prompt(
375 366 self._make_continuation_prompt(self._prompt), html=True)
376 367
377 368 def _show_interpreter_prompt_for_reply(self, msg):
378 369 """ Reimplemented for IPython-style prompts.
379 370 """
380 371 # Update the old prompt number if necessary.
381 372 content = msg['content']
382 373 # abort replies do not have any keys:
383 374 if content['status'] == 'aborted':
384 375 if self._previous_prompt_obj:
385 376 previous_prompt_number = self._previous_prompt_obj.number
386 377 else:
387 378 previous_prompt_number = 0
388 379 else:
389 380 previous_prompt_number = content['execution_count']
390 381 if self._previous_prompt_obj and \
391 382 self._previous_prompt_obj.number != previous_prompt_number:
392 383 block = self._previous_prompt_obj.block
393 384
394 385 # Make sure the prompt block has not been erased.
395 386 if block.isValid() and block.text():
396 387
397 388 # Remove the old prompt and insert a new prompt.
398 389 cursor = QtGui.QTextCursor(block)
399 390 cursor.movePosition(QtGui.QTextCursor.Right,
400 391 QtGui.QTextCursor.KeepAnchor,
401 392 self._previous_prompt_obj.length)
402 393 prompt = self._make_in_prompt(previous_prompt_number)
403 394 self._prompt = self._insert_html_fetching_plain_text(
404 395 cursor, prompt)
405 396
406 397 # When the HTML is inserted, Qt blows away the syntax
407 398 # highlighting for the line, so we need to rehighlight it.
408 399 self._highlighter.rehighlightBlock(cursor.block())
409 400
410 401 self._previous_prompt_obj = None
411 402
412 403 # Show a new prompt with the kernel's estimated prompt number.
413 404 self._show_interpreter_prompt(previous_prompt_number + 1)
414 405
415 406 #---------------------------------------------------------------------------
416 407 # 'IPythonWidget' interface
417 408 #---------------------------------------------------------------------------
418 409
419 410 def set_default_style(self, colors='lightbg'):
420 411 """ Sets the widget style to the class defaults.
421 412
422 413 Parameters:
423 414 -----------
424 415 colors : str, optional (default lightbg)
425 416 Whether to use the default IPython light background or dark
426 417 background or B&W style.
427 418 """
428 419 colors = colors.lower()
429 420 if colors=='lightbg':
430 421 self.style_sheet = styles.default_light_style_sheet
431 422 self.syntax_style = styles.default_light_syntax_style
432 423 elif colors=='linux':
433 424 self.style_sheet = styles.default_dark_style_sheet
434 425 self.syntax_style = styles.default_dark_syntax_style
435 426 elif colors=='nocolor':
436 427 self.style_sheet = styles.default_bw_style_sheet
437 428 self.syntax_style = styles.default_bw_syntax_style
438 429 else:
439 430 raise KeyError("No such color scheme: %s"%colors)
440 431
441 432 #---------------------------------------------------------------------------
442 433 # 'IPythonWidget' protected interface
443 434 #---------------------------------------------------------------------------
444 435
445 436 def _edit(self, filename, line=None):
446 437 """ Opens a Python script for editing.
447 438
448 439 Parameters:
449 440 -----------
450 441 filename : str
451 442 A path to a local system file.
452 443
453 444 line : int, optional
454 445 A line of interest in the file.
455 446 """
456 447 if self.custom_edit:
457 448 self.custom_edit_requested.emit(filename, line)
458 449 elif not self.editor:
459 450 self._append_plain_text('No default editor available.\n'
460 451 'Specify a GUI text editor in the `IPythonWidget.editor` '
461 452 'configurable to enable the %edit magic')
462 453 else:
463 454 try:
464 455 filename = '"%s"' % filename
465 456 if line and self.editor_line:
466 457 command = self.editor_line.format(filename=filename,
467 458 line=line)
468 459 else:
469 460 try:
470 461 command = self.editor.format()
471 462 except KeyError:
472 463 command = self.editor.format(filename=filename)
473 464 else:
474 465 command += ' ' + filename
475 466 except KeyError:
476 467 self._append_plain_text('Invalid editor command.\n')
477 468 else:
478 469 try:
479 470 Popen(command, shell=True)
480 471 except OSError:
481 472 msg = 'Opening editor with command "%s" failed.\n'
482 473 self._append_plain_text(msg % command)
483 474
484 475 def _make_in_prompt(self, number):
485 476 """ Given a prompt number, returns an HTML In prompt.
486 477 """
487 478 try:
488 479 body = self.in_prompt % number
489 480 except TypeError:
490 481 # allow in_prompt to leave out number, e.g. '>>> '
491 482 body = self.in_prompt
492 483 return '<span class="in-prompt">%s</span>' % body
493 484
494 485 def _make_continuation_prompt(self, prompt):
495 486 """ Given a plain text version of an In prompt, returns an HTML
496 487 continuation prompt.
497 488 """
498 489 end_chars = '...: '
499 490 space_count = len(prompt.lstrip('\n')) - len(end_chars)
500 491 body = '&nbsp;' * space_count + end_chars
501 492 return '<span class="in-prompt">%s</span>' % body
502 493
503 494 def _make_out_prompt(self, number):
504 495 """ Given a prompt number, returns an HTML Out prompt.
505 496 """
506 497 body = self.out_prompt % number
507 498 return '<span class="out-prompt">%s</span>' % body
508 499
509 500 #------ Payload handlers --------------------------------------------------
510 501
511 502 # Payload handlers with a generic interface: each takes the opaque payload
512 503 # dict, unpacks it and calls the underlying functions with the necessary
513 504 # arguments.
514 505
515 506 def _handle_payload_edit(self, item):
516 507 self._edit(item['filename'], item['line_number'])
517 508
518 509 def _handle_payload_exit(self, item):
519 510 self._keep_kernel_on_exit = item['keepkernel']
520 511 self.exit_requested.emit(self)
521 512
522 513 def _handle_payload_next_input(self, item):
523 514 self.input_buffer = dedent(item['text'].rstrip())
524 515
525 516 def _handle_payload_page(self, item):
526 517 # Since the plain text widget supports only a very small subset of HTML
527 518 # and we have no control over the HTML source, we only page HTML
528 519 # payloads in the rich text widget.
529 520 if item['html'] and self.kind == 'rich':
530 521 self._page(item['html'], html=True)
531 522 else:
532 523 self._page(item['text'], html=False)
533 524
534 525 #------ Trait change handlers --------------------------------------------
535 526
536 527 def _style_sheet_changed(self):
537 528 """ Set the style sheets of the underlying widgets.
538 529 """
539 530 self.setStyleSheet(self.style_sheet)
540 531 self._control.document().setDefaultStyleSheet(self.style_sheet)
541 532 if self._page_control:
542 533 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
543 534
544 535 bg_color = self._control.palette().window().color()
545 536 self._ansi_processor.set_background_color(bg_color)
546 537
547 538
548 539 def _syntax_style_changed(self):
549 540 """ Set the style for the syntax highlighter.
550 541 """
551 542 if self._highlighter is None:
552 543 # ignore premature calls
553 544 return
554 545 if self.syntax_style:
555 546 self._highlighter.set_style(self.syntax_style)
556 547 else:
557 548 self._highlighter.set_style_sheet(self.style_sheet)
558 549
559 550 #------ Trait default initializers -----------------------------------------
560 551
561 552 def _banner_default(self):
562 553 from IPython.core.usage import default_gui_banner
563 554 return default_gui_banner
@@ -1,909 +1,908 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 629 self.pop = QtGui.QAction("&Update All Magic Menu ",
630 630 self, triggered=self.update_all_magic_menu)
631 631 self.add_menu_action(self.all_magic_menu, self.pop)
632 632 # we need to populate the 'Magic Menu' once the kernel has answer at
633 633 # least once let's do it immedialy, but it's assured to works
634 634 self.pop.trigger()
635 635
636 636 self.reset_action = QtGui.QAction("&Reset",
637 637 self,
638 638 statusTip="Clear all varible from workspace",
639 639 triggered=self.reset_magic_active_frontend)
640 640 self.add_menu_action(self.magic_menu, self.reset_action)
641 641
642 642 self.history_action = QtGui.QAction("&History",
643 643 self,
644 644 statusTip="show command history",
645 645 triggered=self.history_magic_active_frontend)
646 646 self.add_menu_action(self.magic_menu, self.history_action)
647 647
648 648 self.save_action = QtGui.QAction("E&xport History ",
649 649 self,
650 650 statusTip="Export History as Python File",
651 651 triggered=self.save_magic_active_frontend)
652 652 self.add_menu_action(self.magic_menu, self.save_action)
653 653
654 654 self.who_action = QtGui.QAction("&Who",
655 655 self,
656 656 statusTip="List interactive variable",
657 657 triggered=self.who_magic_active_frontend)
658 658 self.add_menu_action(self.magic_menu, self.who_action)
659 659
660 660 self.who_ls_action = QtGui.QAction("Wh&o ls",
661 661 self,
662 662 statusTip="Return a list of interactive variable",
663 663 triggered=self.who_ls_magic_active_frontend)
664 664 self.add_menu_action(self.magic_menu, self.who_ls_action)
665 665
666 666 self.whos_action = QtGui.QAction("Who&s",
667 667 self,
668 668 statusTip="List interactive variable with detail",
669 669 triggered=self.whos_magic_active_frontend)
670 670 self.add_menu_action(self.magic_menu, self.whos_action)
671 671
672 672 def init_window_menu(self):
673 673 self.window_menu = self.menuBar().addMenu("&Window")
674 674 if sys.platform == 'darwin':
675 675 # add min/maximize actions to OSX, which lacks default bindings.
676 676 self.minimizeAct = QtGui.QAction("Mini&mize",
677 677 self,
678 678 shortcut="Ctrl+m",
679 679 statusTip="Minimize the window/Restore Normal Size",
680 680 triggered=self.toggleMinimized)
681 681 # maximize is called 'Zoom' on OSX for some reason
682 682 self.maximizeAct = QtGui.QAction("&Zoom",
683 683 self,
684 684 shortcut="Ctrl+Shift+M",
685 685 statusTip="Maximize the window/Restore Normal Size",
686 686 triggered=self.toggleMaximized)
687 687
688 688 self.add_menu_action(self.window_menu, self.minimizeAct)
689 689 self.add_menu_action(self.window_menu, self.maximizeAct)
690 690 self.window_menu.addSeparator()
691 691
692 692 prev_key = "Ctrl+Shift+Left" if sys.platform == 'darwin' else "Ctrl+PgUp"
693 693 self.prev_tab_act = QtGui.QAction("Pre&vious Tab",
694 694 self,
695 695 shortcut=prev_key,
696 696 statusTip="Select previous tab",
697 697 triggered=self.prev_tab)
698 698 self.add_menu_action(self.window_menu, self.prev_tab_act)
699 699
700 700 next_key = "Ctrl+Shift+Right" if sys.platform == 'darwin' else "Ctrl+PgDown"
701 701 self.next_tab_act = QtGui.QAction("Ne&xt Tab",
702 702 self,
703 703 shortcut=next_key,
704 704 statusTip="Select next tab",
705 705 triggered=self.next_tab)
706 706 self.add_menu_action(self.window_menu, self.next_tab_act)
707 707
708 708 def init_help_menu(self):
709 709 # please keep the Help menu in Mac Os even if empty. It will
710 710 # automatically contain a search field to search inside menus and
711 711 # please keep it spelled in English, as long as Qt Doesn't support
712 712 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
713 713 # this search field fonctionality
714 714
715 715 self.help_menu = self.menuBar().addMenu("&Help")
716 716
717 717
718 718 # Help Menu
719 719
720 720 self.intro_active_frontend_action = QtGui.QAction("&Intro to IPython",
721 721 self,
722 722 triggered=self.intro_active_frontend
723 723 )
724 724 self.add_menu_action(self.help_menu, self.intro_active_frontend_action)
725 725
726 726 self.quickref_active_frontend_action = QtGui.QAction("IPython &Cheat Sheet",
727 727 self,
728 728 triggered=self.quickref_active_frontend
729 729 )
730 730 self.add_menu_action(self.help_menu, self.quickref_active_frontend_action)
731 731
732 732 self.guiref_active_frontend_action = QtGui.QAction("&Qt Console",
733 733 self,
734 734 triggered=self.guiref_active_frontend
735 735 )
736 736 self.add_menu_action(self.help_menu, self.guiref_active_frontend_action)
737 737
738 738 self.onlineHelpAct = QtGui.QAction("Open Online &Help",
739 739 self,
740 740 triggered=self._open_online_help)
741 741 self.add_menu_action(self.help_menu, self.onlineHelpAct)
742 742
743 743 # minimize/maximize/fullscreen actions:
744 744
745 745 def toggle_menu_bar(self):
746 746 menu_bar = self.menuBar()
747 747 if menu_bar.isVisible():
748 748 menu_bar.setVisible(False)
749 749 else:
750 750 menu_bar.setVisible(True)
751 751
752 752 def toggleMinimized(self):
753 753 if not self.isMinimized():
754 754 self.showMinimized()
755 755 else:
756 756 self.showNormal()
757 757
758 758 def _open_online_help(self):
759 759 filename="http://ipython.org/ipython-doc/stable/index.html"
760 760 webbrowser.open(filename, new=1, autoraise=True)
761 761
762 762 def toggleMaximized(self):
763 763 if not self.isMaximized():
764 764 self.showMaximized()
765 765 else:
766 766 self.showNormal()
767 767
768 768 # Min/Max imizing while in full screen give a bug
769 769 # when going out of full screen, at least on OSX
770 770 def toggleFullScreen(self):
771 771 if not self.isFullScreen():
772 772 self.showFullScreen()
773 773 if sys.platform == 'darwin':
774 774 self.maximizeAct.setEnabled(False)
775 775 self.minimizeAct.setEnabled(False)
776 776 else:
777 777 self.showNormal()
778 778 if sys.platform == 'darwin':
779 779 self.maximizeAct.setEnabled(True)
780 780 self.minimizeAct.setEnabled(True)
781 781
782 782 def close_active_frontend(self):
783 783 self.close_tab(self.active_frontend)
784 784
785 785 def restart_kernel_active_frontend(self):
786 786 self.active_frontend.request_restart_kernel()
787 787
788 788 def interrupt_kernel_active_frontend(self):
789 789 self.active_frontend.request_interrupt_kernel()
790 790
791 791 def cut_active_frontend(self):
792 792 widget = self.active_frontend
793 793 if widget.can_cut():
794 794 widget.cut()
795 795
796 796 def copy_active_frontend(self):
797 797 widget = self.active_frontend
798 if widget.can_copy():
799 widget.copy()
798 widget.copy()
800 799
801 800 def copy_raw_active_frontend(self):
802 801 self.active_frontend._copy_raw_action.trigger()
803 802
804 803 def paste_active_frontend(self):
805 804 widget = self.active_frontend
806 805 if widget.can_paste():
807 806 widget.paste()
808 807
809 808 def undo_active_frontend(self):
810 809 self.active_frontend.undo()
811 810
812 811 def redo_active_frontend(self):
813 812 self.active_frontend.redo()
814 813
815 814 def reset_magic_active_frontend(self):
816 815 self.active_frontend.execute("%reset")
817 816
818 817 def history_magic_active_frontend(self):
819 818 self.active_frontend.execute("%history")
820 819
821 820 def save_magic_active_frontend(self):
822 821 self.active_frontend.save_magic()
823 822
824 823 def clear_magic_active_frontend(self):
825 824 self.active_frontend.execute("%clear")
826 825
827 826 def who_magic_active_frontend(self):
828 827 self.active_frontend.execute("%who")
829 828
830 829 def who_ls_magic_active_frontend(self):
831 830 self.active_frontend.execute("%who_ls")
832 831
833 832 def whos_magic_active_frontend(self):
834 833 self.active_frontend.execute("%whos")
835 834
836 835 def print_action_active_frontend(self):
837 836 self.active_frontend.print_action.trigger()
838 837
839 838 def export_action_active_frontend(self):
840 839 self.active_frontend.export_action.trigger()
841 840
842 841 def select_all_active_frontend(self):
843 842 self.active_frontend.select_all_action.trigger()
844 843
845 844 def increase_font_size_active_frontend(self):
846 845 self.active_frontend.increase_font_size.trigger()
847 846
848 847 def decrease_font_size_active_frontend(self):
849 848 self.active_frontend.decrease_font_size.trigger()
850 849
851 850 def reset_font_size_active_frontend(self):
852 851 self.active_frontend.reset_font_size.trigger()
853 852
854 853 def guiref_active_frontend(self):
855 854 self.active_frontend.execute("%guiref")
856 855
857 856 def intro_active_frontend(self):
858 857 self.active_frontend.execute("?")
859 858
860 859 def quickref_active_frontend(self):
861 860 self.active_frontend.execute("%quickref")
862 861 #---------------------------------------------------------------------------
863 862 # QWidget interface
864 863 #---------------------------------------------------------------------------
865 864
866 865 def closeEvent(self, event):
867 866 """ Forward the close event to every tabs contained by the windows
868 867 """
869 868 if self.tab_widget.count() == 0:
870 869 # no tabs, just close
871 870 event.accept()
872 871 return
873 872 # Do Not loop on the widget count as it change while closing
874 873 title = self.window().windowTitle()
875 874 cancel = QtGui.QMessageBox.Cancel
876 875 okay = QtGui.QMessageBox.Ok
877 876
878 877 if self.confirm_exit:
879 878 if self.tab_widget.count() > 1:
880 879 msg = "Close all tabs, stop all kernels, and Quit?"
881 880 else:
882 881 msg = "Close console, stop kernel, and Quit?"
883 882 info = "Kernels not started here (e.g. notebooks) will be left alone."
884 883 closeall = QtGui.QPushButton("&Yes, quit everything", self)
885 884 closeall.setShortcut('Y')
886 885 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
887 886 title, msg)
888 887 box.setInformativeText(info)
889 888 box.addButton(cancel)
890 889 box.addButton(closeall, QtGui.QMessageBox.YesRole)
891 890 box.setDefaultButton(closeall)
892 891 box.setEscapeButton(cancel)
893 892 pixmap = QtGui.QPixmap(self._app.icon.pixmap(QtCore.QSize(64,64)))
894 893 box.setIconPixmap(pixmap)
895 894 reply = box.exec_()
896 895 else:
897 896 reply = okay
898 897
899 898 if reply == cancel:
900 899 event.ignore()
901 900 return
902 901 if reply == okay:
903 902 while self.tab_widget.count() >= 1:
904 903 # prevent further confirmations:
905 904 widget = self.active_frontend
906 905 widget._confirm_exit = False
907 906 self.close_tab(widget)
908 907 event.accept()
909 908
General Comments 0
You need to be logged in to leave comments. Login now