##// END OF EJS Templates
Fix all imports for Qt console.
Fernando Perez -
Show More
@@ -1,2009 +1,2009 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.path
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12 from unicodedata import category
13 13 import webbrowser
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.core.inputsplitter import ESC_SEQUENCES
21 from IPython.frontend.qt.rich_text import HtmlExporter
22 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.qt.rich_text import HtmlExporter
22 from IPython.qt.util import MetaQObjectHasTraits, get_font
23 23 from IPython.utils.text import columnize
24 24 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
25 25 from ansi_code_processor import QtAnsiCodeProcessor
26 26 from completion_widget import CompletionWidget
27 27 from completion_html import CompletionHtml
28 28 from completion_plain import CompletionPlain
29 29 from kill_ring import QtKillRing
30 30
31 31
32 32 #-----------------------------------------------------------------------------
33 33 # Functions
34 34 #-----------------------------------------------------------------------------
35 35
36 36 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
37 37 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
38 38
39 39 def commonprefix(items):
40 40 """Get common prefix for completions
41 41
42 42 Return the longest common prefix of a list of strings, but with special
43 43 treatment of escape characters that might precede commands in IPython,
44 44 such as %magic functions. Used in tab completion.
45 45
46 46 For a more general function, see os.path.commonprefix
47 47 """
48 48 # the last item will always have the least leading % symbol
49 49 # min / max are first/last in alphabetical order
50 50 first_match = ESCAPE_RE.match(min(items))
51 51 last_match = ESCAPE_RE.match(max(items))
52 52 # common suffix is (common prefix of reversed items) reversed
53 53 if first_match and last_match:
54 54 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
55 55 else:
56 56 prefix = ''
57 57
58 58 items = [s.lstrip(ESCAPE_CHARS) for s in items]
59 59 return prefix+os.path.commonprefix(items)
60 60
61 61 def is_letter_or_number(char):
62 62 """ Returns whether the specified unicode character is a letter or a number.
63 63 """
64 64 cat = category(char)
65 65 return cat.startswith('L') or cat.startswith('N')
66 66
67 67 #-----------------------------------------------------------------------------
68 68 # Classes
69 69 #-----------------------------------------------------------------------------
70 70
71 71 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
72 72 """ An abstract base class for console-type widgets. This class has
73 73 functionality for:
74 74
75 75 * Maintaining a prompt and editing region
76 76 * Providing the traditional Unix-style console keyboard shortcuts
77 77 * Performing tab completion
78 78 * Paging text
79 79 * Handling ANSI escape codes
80 80
81 81 ConsoleWidget also provides a number of utility methods that will be
82 82 convenient to implementors of a console-style widget.
83 83 """
84 84 __metaclass__ = MetaQObjectHasTraits
85 85
86 86 #------ Configuration ------------------------------------------------------
87 87
88 88 ansi_codes = Bool(True, config=True,
89 89 help="Whether to process ANSI escape codes."
90 90 )
91 91 buffer_size = Integer(500, config=True,
92 92 help="""
93 93 The maximum number of lines of text before truncation. Specifying a
94 94 non-positive number disables text truncation (not recommended).
95 95 """
96 96 )
97 97 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
98 98 default_value = 'ncurses',
99 99 help="""
100 100 The type of completer to use. Valid values are:
101 101
102 102 'plain' : Show the availlable completion as a text list
103 103 Below the editting area.
104 104 'droplist': Show the completion in a drop down list navigable
105 105 by the arrow keys, and from which you can select
106 106 completion by pressing Return.
107 107 'ncurses' : Show the completion as a text list which is navigable by
108 108 `tab` and arrow keys.
109 109 """
110 110 )
111 111 # NOTE: this value can only be specified during initialization.
112 112 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
113 113 help="""
114 114 The type of underlying text widget to use. Valid values are 'plain',
115 115 which specifies a QPlainTextEdit, and 'rich', which specifies a
116 116 QTextEdit.
117 117 """
118 118 )
119 119 # NOTE: this value can only be specified during initialization.
120 120 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
121 121 default_value='inside', config=True,
122 122 help="""
123 123 The type of paging to use. Valid values are:
124 124
125 125 'inside' : The widget pages like a traditional terminal.
126 126 'hsplit' : When paging is requested, the widget is split
127 127 horizontally. The top pane contains the console, and the
128 128 bottom pane contains the paged text.
129 129 'vsplit' : Similar to 'hsplit', except that a vertical splitter
130 130 used.
131 131 'custom' : No action is taken by the widget beyond emitting a
132 132 'custom_page_requested(str)' signal.
133 133 'none' : The text is written directly to the console.
134 134 """)
135 135
136 136 font_family = Unicode(config=True,
137 137 help="""The font family to use for the console.
138 138 On OSX this defaults to Monaco, on Windows the default is
139 139 Consolas with fallback of Courier, and on other platforms
140 140 the default is Monospace.
141 141 """)
142 142 def _font_family_default(self):
143 143 if sys.platform == 'win32':
144 144 # Consolas ships with Vista/Win7, fallback to Courier if needed
145 145 return 'Consolas'
146 146 elif sys.platform == 'darwin':
147 147 # OSX always has Monaco, no need for a fallback
148 148 return 'Monaco'
149 149 else:
150 150 # Monospace should always exist, no need for a fallback
151 151 return 'Monospace'
152 152
153 153 font_size = Integer(config=True,
154 154 help="""The font size. If unconfigured, Qt will be entrusted
155 155 with the size of the font.
156 156 """)
157 157
158 158 width = Integer(81, config=True,
159 159 help="""The width of the console at start time in number
160 160 of characters (will double with `hsplit` paging)
161 161 """)
162 162
163 163 height = Integer(25, config=True,
164 164 help="""The height of the console at start time in number
165 165 of characters (will double with `vsplit` paging)
166 166 """)
167 167
168 168 # Whether to override ShortcutEvents for the keybindings defined by this
169 169 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
170 170 # priority (when it has focus) over, e.g., window-level menu shortcuts.
171 171 override_shortcuts = Bool(False)
172 172
173 173 # ------ Custom Qt Widgets -------------------------------------------------
174 174
175 175 # For other projects to easily override the Qt widgets used by the console
176 176 # (e.g. Spyder)
177 177 custom_control = None
178 178 custom_page_control = None
179 179
180 180 #------ Signals ------------------------------------------------------------
181 181
182 182 # Signals that indicate ConsoleWidget state.
183 183 copy_available = QtCore.Signal(bool)
184 184 redo_available = QtCore.Signal(bool)
185 185 undo_available = QtCore.Signal(bool)
186 186
187 187 # Signal emitted when paging is needed and the paging style has been
188 188 # specified as 'custom'.
189 189 custom_page_requested = QtCore.Signal(object)
190 190
191 191 # Signal emitted when the font is changed.
192 192 font_changed = QtCore.Signal(QtGui.QFont)
193 193
194 194 #------ Protected class variables ------------------------------------------
195 195
196 196 # control handles
197 197 _control = None
198 198 _page_control = None
199 199 _splitter = None
200 200
201 201 # When the control key is down, these keys are mapped.
202 202 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
203 203 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
204 204 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
205 205 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
206 206 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
207 207 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
208 208 if not sys.platform == 'darwin':
209 209 # On OS X, Ctrl-E already does the right thing, whereas End moves the
210 210 # cursor to the bottom of the buffer.
211 211 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
212 212
213 213 # The shortcuts defined by this widget. We need to keep track of these to
214 214 # support 'override_shortcuts' above.
215 215 _shortcuts = set(_ctrl_down_remap.keys() +
216 216 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
217 217 QtCore.Qt.Key_V ])
218 218
219 219 _temp_buffer_filled = False
220 220
221 221 #---------------------------------------------------------------------------
222 222 # 'QObject' interface
223 223 #---------------------------------------------------------------------------
224 224
225 225 def __init__(self, parent=None, **kw):
226 226 """ Create a ConsoleWidget.
227 227
228 228 Parameters:
229 229 -----------
230 230 parent : QWidget, optional [default None]
231 231 The parent for this widget.
232 232 """
233 233 QtGui.QWidget.__init__(self, parent)
234 234 LoggingConfigurable.__init__(self, **kw)
235 235
236 236 # While scrolling the pager on Mac OS X, it tears badly. The
237 237 # NativeGesture is platform and perhaps build-specific hence
238 238 # we take adequate precautions here.
239 239 self._pager_scroll_events = [QtCore.QEvent.Wheel]
240 240 if hasattr(QtCore.QEvent, 'NativeGesture'):
241 241 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
242 242
243 243 # Create the layout and underlying text widget.
244 244 layout = QtGui.QStackedLayout(self)
245 245 layout.setContentsMargins(0, 0, 0, 0)
246 246 self._control = self._create_control()
247 247 if self.paging in ('hsplit', 'vsplit'):
248 248 self._splitter = QtGui.QSplitter()
249 249 if self.paging == 'hsplit':
250 250 self._splitter.setOrientation(QtCore.Qt.Horizontal)
251 251 else:
252 252 self._splitter.setOrientation(QtCore.Qt.Vertical)
253 253 self._splitter.addWidget(self._control)
254 254 layout.addWidget(self._splitter)
255 255 else:
256 256 layout.addWidget(self._control)
257 257
258 258 # Create the paging widget, if necessary.
259 259 if self.paging in ('inside', 'hsplit', 'vsplit'):
260 260 self._page_control = self._create_page_control()
261 261 if self._splitter:
262 262 self._page_control.hide()
263 263 self._splitter.addWidget(self._page_control)
264 264 else:
265 265 layout.addWidget(self._page_control)
266 266
267 267 # Initialize protected variables. Some variables contain useful state
268 268 # information for subclasses; they should be considered read-only.
269 269 self._append_before_prompt_pos = 0
270 270 self._ansi_processor = QtAnsiCodeProcessor()
271 271 if self.gui_completion == 'ncurses':
272 272 self._completion_widget = CompletionHtml(self)
273 273 elif self.gui_completion == 'droplist':
274 274 self._completion_widget = CompletionWidget(self)
275 275 elif self.gui_completion == 'plain':
276 276 self._completion_widget = CompletionPlain(self)
277 277
278 278 self._continuation_prompt = '> '
279 279 self._continuation_prompt_html = None
280 280 self._executing = False
281 281 self._filter_resize = False
282 282 self._html_exporter = HtmlExporter(self._control)
283 283 self._input_buffer_executing = ''
284 284 self._input_buffer_pending = ''
285 285 self._kill_ring = QtKillRing(self._control)
286 286 self._prompt = ''
287 287 self._prompt_html = None
288 288 self._prompt_pos = 0
289 289 self._prompt_sep = ''
290 290 self._reading = False
291 291 self._reading_callback = None
292 292 self._tab_width = 8
293 293
294 294 # Set a monospaced font.
295 295 self.reset_font()
296 296
297 297 # Configure actions.
298 298 action = QtGui.QAction('Print', None)
299 299 action.setEnabled(True)
300 300 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
301 301 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
302 302 # Only override the default if there is a collision.
303 303 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
304 304 printkey = "Ctrl+Shift+P"
305 305 action.setShortcut(printkey)
306 306 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
307 307 action.triggered.connect(self.print_)
308 308 self.addAction(action)
309 309 self.print_action = action
310 310
311 311 action = QtGui.QAction('Save as HTML/XML', None)
312 312 action.setShortcut(QtGui.QKeySequence.Save)
313 313 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
314 314 action.triggered.connect(self.export_html)
315 315 self.addAction(action)
316 316 self.export_action = action
317 317
318 318 action = QtGui.QAction('Select All', None)
319 319 action.setEnabled(True)
320 320 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
321 321 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
322 322 # Only override the default if there is a collision.
323 323 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
324 324 selectall = "Ctrl+Shift+A"
325 325 action.setShortcut(selectall)
326 326 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
327 327 action.triggered.connect(self.select_all)
328 328 self.addAction(action)
329 329 self.select_all_action = action
330 330
331 331 self.increase_font_size = QtGui.QAction("Bigger Font",
332 332 self,
333 333 shortcut=QtGui.QKeySequence.ZoomIn,
334 334 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
335 335 statusTip="Increase the font size by one point",
336 336 triggered=self._increase_font_size)
337 337 self.addAction(self.increase_font_size)
338 338
339 339 self.decrease_font_size = QtGui.QAction("Smaller Font",
340 340 self,
341 341 shortcut=QtGui.QKeySequence.ZoomOut,
342 342 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
343 343 statusTip="Decrease the font size by one point",
344 344 triggered=self._decrease_font_size)
345 345 self.addAction(self.decrease_font_size)
346 346
347 347 self.reset_font_size = QtGui.QAction("Normal Font",
348 348 self,
349 349 shortcut="Ctrl+0",
350 350 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
351 351 statusTip="Restore the Normal font size",
352 352 triggered=self.reset_font)
353 353 self.addAction(self.reset_font_size)
354 354
355 355 # Accept drag and drop events here. Drops were already turned off
356 356 # in self._control when that widget was created.
357 357 self.setAcceptDrops(True)
358 358
359 359 #---------------------------------------------------------------------------
360 360 # Drag and drop support
361 361 #---------------------------------------------------------------------------
362 362
363 363 def dragEnterEvent(self, e):
364 364 if e.mimeData().hasUrls():
365 365 # The link action should indicate to that the drop will insert
366 366 # the file anme.
367 367 e.setDropAction(QtCore.Qt.LinkAction)
368 368 e.accept()
369 369 elif e.mimeData().hasText():
370 370 # By changing the action to copy we don't need to worry about
371 371 # the user accidentally moving text around in the widget.
372 372 e.setDropAction(QtCore.Qt.CopyAction)
373 373 e.accept()
374 374
375 375 def dragMoveEvent(self, e):
376 376 if e.mimeData().hasUrls():
377 377 pass
378 378 elif e.mimeData().hasText():
379 379 cursor = self._control.cursorForPosition(e.pos())
380 380 if self._in_buffer(cursor.position()):
381 381 e.setDropAction(QtCore.Qt.CopyAction)
382 382 self._control.setTextCursor(cursor)
383 383 else:
384 384 e.setDropAction(QtCore.Qt.IgnoreAction)
385 385 e.accept()
386 386
387 387 def dropEvent(self, e):
388 388 if e.mimeData().hasUrls():
389 389 self._keep_cursor_in_buffer()
390 390 cursor = self._control.textCursor()
391 391 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
392 392 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
393 393 for f in filenames)
394 394 self._insert_plain_text_into_buffer(cursor, text)
395 395 elif e.mimeData().hasText():
396 396 cursor = self._control.cursorForPosition(e.pos())
397 397 if self._in_buffer(cursor.position()):
398 398 text = e.mimeData().text()
399 399 self._insert_plain_text_into_buffer(cursor, text)
400 400
401 401 def eventFilter(self, obj, event):
402 402 """ Reimplemented to ensure a console-like behavior in the underlying
403 403 text widgets.
404 404 """
405 405 etype = event.type()
406 406 if etype == QtCore.QEvent.KeyPress:
407 407
408 408 # Re-map keys for all filtered widgets.
409 409 key = event.key()
410 410 if self._control_key_down(event.modifiers()) and \
411 411 key in self._ctrl_down_remap:
412 412 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
413 413 self._ctrl_down_remap[key],
414 414 QtCore.Qt.NoModifier)
415 415 QtGui.qApp.sendEvent(obj, new_event)
416 416 return True
417 417
418 418 elif obj == self._control:
419 419 return self._event_filter_console_keypress(event)
420 420
421 421 elif obj == self._page_control:
422 422 return self._event_filter_page_keypress(event)
423 423
424 424 # Make middle-click paste safe.
425 425 elif etype == QtCore.QEvent.MouseButtonRelease and \
426 426 event.button() == QtCore.Qt.MidButton and \
427 427 obj == self._control.viewport():
428 428 cursor = self._control.cursorForPosition(event.pos())
429 429 self._control.setTextCursor(cursor)
430 430 self.paste(QtGui.QClipboard.Selection)
431 431 return True
432 432
433 433 # Manually adjust the scrollbars *after* a resize event is dispatched.
434 434 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
435 435 self._filter_resize = True
436 436 QtGui.qApp.sendEvent(obj, event)
437 437 self._adjust_scrollbars()
438 438 self._filter_resize = False
439 439 return True
440 440
441 441 # Override shortcuts for all filtered widgets.
442 442 elif etype == QtCore.QEvent.ShortcutOverride and \
443 443 self.override_shortcuts and \
444 444 self._control_key_down(event.modifiers()) and \
445 445 event.key() in self._shortcuts:
446 446 event.accept()
447 447
448 448 # Handle scrolling of the vsplit pager. This hack attempts to solve
449 449 # problems with tearing of the help text inside the pager window. This
450 450 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
451 451 # perfect but makes the pager more usable.
452 452 elif etype in self._pager_scroll_events and \
453 453 obj == self._page_control:
454 454 self._page_control.repaint()
455 455 return True
456 456
457 457 elif etype == QtCore.QEvent.MouseMove:
458 458 anchor = self._control.anchorAt(event.pos())
459 459 QtGui.QToolTip.showText(event.globalPos(), anchor)
460 460
461 461 return super(ConsoleWidget, self).eventFilter(obj, event)
462 462
463 463 #---------------------------------------------------------------------------
464 464 # 'QWidget' interface
465 465 #---------------------------------------------------------------------------
466 466
467 467 def sizeHint(self):
468 468 """ Reimplemented to suggest a size that is 80 characters wide and
469 469 25 lines high.
470 470 """
471 471 font_metrics = QtGui.QFontMetrics(self.font)
472 472 margin = (self._control.frameWidth() +
473 473 self._control.document().documentMargin()) * 2
474 474 style = self.style()
475 475 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
476 476
477 477 # Note 1: Despite my best efforts to take the various margins into
478 478 # account, the width is still coming out a bit too small, so we include
479 479 # a fudge factor of one character here.
480 480 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
481 481 # to a Qt bug on certain Mac OS systems where it returns 0.
482 482 width = font_metrics.width(' ') * self.width + margin
483 483 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
484 484 if self.paging == 'hsplit':
485 485 width = width * 2 + splitwidth
486 486
487 487 height = font_metrics.height() * self.height + margin
488 488 if self.paging == 'vsplit':
489 489 height = height * 2 + splitwidth
490 490
491 491 return QtCore.QSize(width, height)
492 492
493 493 #---------------------------------------------------------------------------
494 494 # 'ConsoleWidget' public interface
495 495 #---------------------------------------------------------------------------
496 496
497 497 def can_copy(self):
498 498 """ Returns whether text can be copied to the clipboard.
499 499 """
500 500 return self._control.textCursor().hasSelection()
501 501
502 502 def can_cut(self):
503 503 """ Returns whether text can be cut to the clipboard.
504 504 """
505 505 cursor = self._control.textCursor()
506 506 return (cursor.hasSelection() and
507 507 self._in_buffer(cursor.anchor()) and
508 508 self._in_buffer(cursor.position()))
509 509
510 510 def can_paste(self):
511 511 """ Returns whether text can be pasted from the clipboard.
512 512 """
513 513 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
514 514 return bool(QtGui.QApplication.clipboard().text())
515 515 return False
516 516
517 517 def clear(self, keep_input=True):
518 518 """ Clear the console.
519 519
520 520 Parameters:
521 521 -----------
522 522 keep_input : bool, optional (default True)
523 523 If set, restores the old input buffer if a new prompt is written.
524 524 """
525 525 if self._executing:
526 526 self._control.clear()
527 527 else:
528 528 if keep_input:
529 529 input_buffer = self.input_buffer
530 530 self._control.clear()
531 531 self._show_prompt()
532 532 if keep_input:
533 533 self.input_buffer = input_buffer
534 534
535 535 def copy(self):
536 536 """ Copy the currently selected text to the clipboard.
537 537 """
538 538 self.layout().currentWidget().copy()
539 539
540 540 def copy_anchor(self, anchor):
541 541 """ Copy anchor text to the clipboard
542 542 """
543 543 QtGui.QApplication.clipboard().setText(anchor)
544 544
545 545 def cut(self):
546 546 """ Copy the currently selected text to the clipboard and delete it
547 547 if it's inside the input buffer.
548 548 """
549 549 self.copy()
550 550 if self.can_cut():
551 551 self._control.textCursor().removeSelectedText()
552 552
553 553 def execute(self, source=None, hidden=False, interactive=False):
554 554 """ Executes source or the input buffer, possibly prompting for more
555 555 input.
556 556
557 557 Parameters:
558 558 -----------
559 559 source : str, optional
560 560
561 561 The source to execute. If not specified, the input buffer will be
562 562 used. If specified and 'hidden' is False, the input buffer will be
563 563 replaced with the source before execution.
564 564
565 565 hidden : bool, optional (default False)
566 566
567 567 If set, no output will be shown and the prompt will not be modified.
568 568 In other words, it will be completely invisible to the user that
569 569 an execution has occurred.
570 570
571 571 interactive : bool, optional (default False)
572 572
573 573 Whether the console is to treat the source as having been manually
574 574 entered by the user. The effect of this parameter depends on the
575 575 subclass implementation.
576 576
577 577 Raises:
578 578 -------
579 579 RuntimeError
580 580 If incomplete input is given and 'hidden' is True. In this case,
581 581 it is not possible to prompt for more input.
582 582
583 583 Returns:
584 584 --------
585 585 A boolean indicating whether the source was executed.
586 586 """
587 587 # WARNING: The order in which things happen here is very particular, in
588 588 # large part because our syntax highlighting is fragile. If you change
589 589 # something, test carefully!
590 590
591 591 # Decide what to execute.
592 592 if source is None:
593 593 source = self.input_buffer
594 594 if not hidden:
595 595 # A newline is appended later, but it should be considered part
596 596 # of the input buffer.
597 597 source += '\n'
598 598 elif not hidden:
599 599 self.input_buffer = source
600 600
601 601 # Execute the source or show a continuation prompt if it is incomplete.
602 602 complete = self._is_complete(source, interactive)
603 603 if hidden:
604 604 if complete:
605 605 self._execute(source, hidden)
606 606 else:
607 607 error = 'Incomplete noninteractive input: "%s"'
608 608 raise RuntimeError(error % source)
609 609 else:
610 610 if complete:
611 611 self._append_plain_text('\n')
612 612 self._input_buffer_executing = self.input_buffer
613 613 self._executing = True
614 614 self._prompt_finished()
615 615
616 616 # The maximum block count is only in effect during execution.
617 617 # This ensures that _prompt_pos does not become invalid due to
618 618 # text truncation.
619 619 self._control.document().setMaximumBlockCount(self.buffer_size)
620 620
621 621 # Setting a positive maximum block count will automatically
622 622 # disable the undo/redo history, but just to be safe:
623 623 self._control.setUndoRedoEnabled(False)
624 624
625 625 # Perform actual execution.
626 626 self._execute(source, hidden)
627 627
628 628 else:
629 629 # Do this inside an edit block so continuation prompts are
630 630 # removed seamlessly via undo/redo.
631 631 cursor = self._get_end_cursor()
632 632 cursor.beginEditBlock()
633 633 cursor.insertText('\n')
634 634 self._insert_continuation_prompt(cursor)
635 635 cursor.endEditBlock()
636 636
637 637 # Do not do this inside the edit block. It works as expected
638 638 # when using a QPlainTextEdit control, but does not have an
639 639 # effect when using a QTextEdit. I believe this is a Qt bug.
640 640 self._control.moveCursor(QtGui.QTextCursor.End)
641 641
642 642 return complete
643 643
644 644 def export_html(self):
645 645 """ Shows a dialog to export HTML/XML in various formats.
646 646 """
647 647 self._html_exporter.export()
648 648
649 649 def _get_input_buffer(self, force=False):
650 650 """ The text that the user has entered entered at the current prompt.
651 651
652 652 If the console is currently executing, the text that is executing will
653 653 always be returned.
654 654 """
655 655 # If we're executing, the input buffer may not even exist anymore due to
656 656 # the limit imposed by 'buffer_size'. Therefore, we store it.
657 657 if self._executing and not force:
658 658 return self._input_buffer_executing
659 659
660 660 cursor = self._get_end_cursor()
661 661 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
662 662 input_buffer = cursor.selection().toPlainText()
663 663
664 664 # Strip out continuation prompts.
665 665 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
666 666
667 667 def _set_input_buffer(self, string):
668 668 """ Sets the text in the input buffer.
669 669
670 670 If the console is currently executing, this call has no *immediate*
671 671 effect. When the execution is finished, the input buffer will be updated
672 672 appropriately.
673 673 """
674 674 # If we're executing, store the text for later.
675 675 if self._executing:
676 676 self._input_buffer_pending = string
677 677 return
678 678
679 679 # Remove old text.
680 680 cursor = self._get_end_cursor()
681 681 cursor.beginEditBlock()
682 682 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
683 683 cursor.removeSelectedText()
684 684
685 685 # Insert new text with continuation prompts.
686 686 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
687 687 cursor.endEditBlock()
688 688 self._control.moveCursor(QtGui.QTextCursor.End)
689 689
690 690 input_buffer = property(_get_input_buffer, _set_input_buffer)
691 691
692 692 def _get_font(self):
693 693 """ The base font being used by the ConsoleWidget.
694 694 """
695 695 return self._control.document().defaultFont()
696 696
697 697 def _set_font(self, font):
698 698 """ Sets the base font for the ConsoleWidget to the specified QFont.
699 699 """
700 700 font_metrics = QtGui.QFontMetrics(font)
701 701 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
702 702
703 703 self._completion_widget.setFont(font)
704 704 self._control.document().setDefaultFont(font)
705 705 if self._page_control:
706 706 self._page_control.document().setDefaultFont(font)
707 707
708 708 self.font_changed.emit(font)
709 709
710 710 font = property(_get_font, _set_font)
711 711
712 712 def open_anchor(self, anchor):
713 713 """ Open selected anchor in the default webbrowser
714 714 """
715 715 webbrowser.open( anchor )
716 716
717 717 def paste(self, mode=QtGui.QClipboard.Clipboard):
718 718 """ Paste the contents of the clipboard into the input region.
719 719
720 720 Parameters:
721 721 -----------
722 722 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
723 723
724 724 Controls which part of the system clipboard is used. This can be
725 725 used to access the selection clipboard in X11 and the Find buffer
726 726 in Mac OS. By default, the regular clipboard is used.
727 727 """
728 728 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
729 729 # Make sure the paste is safe.
730 730 self._keep_cursor_in_buffer()
731 731 cursor = self._control.textCursor()
732 732
733 733 # Remove any trailing newline, which confuses the GUI and forces the
734 734 # user to backspace.
735 735 text = QtGui.QApplication.clipboard().text(mode).rstrip()
736 736 self._insert_plain_text_into_buffer(cursor, dedent(text))
737 737
738 738 def print_(self, printer = None):
739 739 """ Print the contents of the ConsoleWidget to the specified QPrinter.
740 740 """
741 741 if (not printer):
742 742 printer = QtGui.QPrinter()
743 743 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
744 744 return
745 745 self._control.print_(printer)
746 746
747 747 def prompt_to_top(self):
748 748 """ Moves the prompt to the top of the viewport.
749 749 """
750 750 if not self._executing:
751 751 prompt_cursor = self._get_prompt_cursor()
752 752 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
753 753 self._set_cursor(prompt_cursor)
754 754 self._set_top_cursor(prompt_cursor)
755 755
756 756 def redo(self):
757 757 """ Redo the last operation. If there is no operation to redo, nothing
758 758 happens.
759 759 """
760 760 self._control.redo()
761 761
762 762 def reset_font(self):
763 763 """ Sets the font to the default fixed-width font for this platform.
764 764 """
765 765 if sys.platform == 'win32':
766 766 # Consolas ships with Vista/Win7, fallback to Courier if needed
767 767 fallback = 'Courier'
768 768 elif sys.platform == 'darwin':
769 769 # OSX always has Monaco
770 770 fallback = 'Monaco'
771 771 else:
772 772 # Monospace should always exist
773 773 fallback = 'Monospace'
774 774 font = get_font(self.font_family, fallback)
775 775 if self.font_size:
776 776 font.setPointSize(self.font_size)
777 777 else:
778 778 font.setPointSize(QtGui.qApp.font().pointSize())
779 779 font.setStyleHint(QtGui.QFont.TypeWriter)
780 780 self._set_font(font)
781 781
782 782 def change_font_size(self, delta):
783 783 """Change the font size by the specified amount (in points).
784 784 """
785 785 font = self.font
786 786 size = max(font.pointSize() + delta, 1) # minimum 1 point
787 787 font.setPointSize(size)
788 788 self._set_font(font)
789 789
790 790 def _increase_font_size(self):
791 791 self.change_font_size(1)
792 792
793 793 def _decrease_font_size(self):
794 794 self.change_font_size(-1)
795 795
796 796 def select_all(self):
797 797 """ Selects all the text in the buffer.
798 798 """
799 799 self._control.selectAll()
800 800
801 801 def _get_tab_width(self):
802 802 """ The width (in terms of space characters) for tab characters.
803 803 """
804 804 return self._tab_width
805 805
806 806 def _set_tab_width(self, tab_width):
807 807 """ Sets the width (in terms of space characters) for tab characters.
808 808 """
809 809 font_metrics = QtGui.QFontMetrics(self.font)
810 810 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
811 811
812 812 self._tab_width = tab_width
813 813
814 814 tab_width = property(_get_tab_width, _set_tab_width)
815 815
816 816 def undo(self):
817 817 """ Undo the last operation. If there is no operation to undo, nothing
818 818 happens.
819 819 """
820 820 self._control.undo()
821 821
822 822 #---------------------------------------------------------------------------
823 823 # 'ConsoleWidget' abstract interface
824 824 #---------------------------------------------------------------------------
825 825
826 826 def _is_complete(self, source, interactive):
827 827 """ Returns whether 'source' can be executed. When triggered by an
828 828 Enter/Return key press, 'interactive' is True; otherwise, it is
829 829 False.
830 830 """
831 831 raise NotImplementedError
832 832
833 833 def _execute(self, source, hidden):
834 834 """ Execute 'source'. If 'hidden', do not show any output.
835 835 """
836 836 raise NotImplementedError
837 837
838 838 def _prompt_started_hook(self):
839 839 """ Called immediately after a new prompt is displayed.
840 840 """
841 841 pass
842 842
843 843 def _prompt_finished_hook(self):
844 844 """ Called immediately after a prompt is finished, i.e. when some input
845 845 will be processed and a new prompt displayed.
846 846 """
847 847 pass
848 848
849 849 def _up_pressed(self, shift_modifier):
850 850 """ Called when the up key is pressed. Returns whether to continue
851 851 processing the event.
852 852 """
853 853 return True
854 854
855 855 def _down_pressed(self, shift_modifier):
856 856 """ Called when the down key is pressed. Returns whether to continue
857 857 processing the event.
858 858 """
859 859 return True
860 860
861 861 def _tab_pressed(self):
862 862 """ Called when the tab key is pressed. Returns whether to continue
863 863 processing the event.
864 864 """
865 865 return False
866 866
867 867 #--------------------------------------------------------------------------
868 868 # 'ConsoleWidget' protected interface
869 869 #--------------------------------------------------------------------------
870 870
871 871 def _append_custom(self, insert, input, before_prompt=False):
872 872 """ A low-level method for appending content to the end of the buffer.
873 873
874 874 If 'before_prompt' is enabled, the content will be inserted before the
875 875 current prompt, if there is one.
876 876 """
877 877 # Determine where to insert the content.
878 878 cursor = self._control.textCursor()
879 879 if before_prompt and (self._reading or not self._executing):
880 880 cursor.setPosition(self._append_before_prompt_pos)
881 881 else:
882 882 cursor.movePosition(QtGui.QTextCursor.End)
883 883 start_pos = cursor.position()
884 884
885 885 # Perform the insertion.
886 886 result = insert(cursor, input)
887 887
888 888 # Adjust the prompt position if we have inserted before it. This is safe
889 889 # because buffer truncation is disabled when not executing.
890 890 if before_prompt and not self._executing:
891 891 diff = cursor.position() - start_pos
892 892 self._append_before_prompt_pos += diff
893 893 self._prompt_pos += diff
894 894
895 895 return result
896 896
897 897 def _append_block(self, block_format=None, before_prompt=False):
898 898 """ Appends an new QTextBlock to the end of the console buffer.
899 899 """
900 900 self._append_custom(self._insert_block, block_format, before_prompt)
901 901
902 902 def _append_html(self, html, before_prompt=False):
903 903 """ Appends HTML at the end of the console buffer.
904 904 """
905 905 self._append_custom(self._insert_html, html, before_prompt)
906 906
907 907 def _append_html_fetching_plain_text(self, html, before_prompt=False):
908 908 """ Appends HTML, then returns the plain text version of it.
909 909 """
910 910 return self._append_custom(self._insert_html_fetching_plain_text,
911 911 html, before_prompt)
912 912
913 913 def _append_plain_text(self, text, before_prompt=False):
914 914 """ Appends plain text, processing ANSI codes if enabled.
915 915 """
916 916 self._append_custom(self._insert_plain_text, text, before_prompt)
917 917
918 918 def _cancel_completion(self):
919 919 """ If text completion is progress, cancel it.
920 920 """
921 921 self._completion_widget.cancel_completion()
922 922
923 923 def _clear_temporary_buffer(self):
924 924 """ Clears the "temporary text" buffer, i.e. all the text following
925 925 the prompt region.
926 926 """
927 927 # Select and remove all text below the input buffer.
928 928 cursor = self._get_prompt_cursor()
929 929 prompt = self._continuation_prompt.lstrip()
930 930 if(self._temp_buffer_filled):
931 931 self._temp_buffer_filled = False
932 932 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
933 933 temp_cursor = QtGui.QTextCursor(cursor)
934 934 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
935 935 text = temp_cursor.selection().toPlainText().lstrip()
936 936 if not text.startswith(prompt):
937 937 break
938 938 else:
939 939 # We've reached the end of the input buffer and no text follows.
940 940 return
941 941 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
942 942 cursor.movePosition(QtGui.QTextCursor.End,
943 943 QtGui.QTextCursor.KeepAnchor)
944 944 cursor.removeSelectedText()
945 945
946 946 # After doing this, we have no choice but to clear the undo/redo
947 947 # history. Otherwise, the text is not "temporary" at all, because it
948 948 # can be recalled with undo/redo. Unfortunately, Qt does not expose
949 949 # fine-grained control to the undo/redo system.
950 950 if self._control.isUndoRedoEnabled():
951 951 self._control.setUndoRedoEnabled(False)
952 952 self._control.setUndoRedoEnabled(True)
953 953
954 954 def _complete_with_items(self, cursor, items):
955 955 """ Performs completion with 'items' at the specified cursor location.
956 956 """
957 957 self._cancel_completion()
958 958
959 959 if len(items) == 1:
960 960 cursor.setPosition(self._control.textCursor().position(),
961 961 QtGui.QTextCursor.KeepAnchor)
962 962 cursor.insertText(items[0])
963 963
964 964 elif len(items) > 1:
965 965 current_pos = self._control.textCursor().position()
966 966 prefix = commonprefix(items)
967 967 if prefix:
968 968 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
969 969 cursor.insertText(prefix)
970 970 current_pos = cursor.position()
971 971
972 972 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
973 973 self._completion_widget.show_items(cursor, items)
974 974
975 975
976 976 def _fill_temporary_buffer(self, cursor, text, html=False):
977 977 """fill the area below the active editting zone with text"""
978 978
979 979 current_pos = self._control.textCursor().position()
980 980
981 981 cursor.beginEditBlock()
982 982 self._append_plain_text('\n')
983 983 self._page(text, html=html)
984 984 cursor.endEditBlock()
985 985
986 986 cursor.setPosition(current_pos)
987 987 self._control.moveCursor(QtGui.QTextCursor.End)
988 988 self._control.setTextCursor(cursor)
989 989
990 990 self._temp_buffer_filled = True
991 991
992 992
993 993 def _context_menu_make(self, pos):
994 994 """ Creates a context menu for the given QPoint (in widget coordinates).
995 995 """
996 996 menu = QtGui.QMenu(self)
997 997
998 998 self.cut_action = menu.addAction('Cut', self.cut)
999 999 self.cut_action.setEnabled(self.can_cut())
1000 1000 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1001 1001
1002 1002 self.copy_action = menu.addAction('Copy', self.copy)
1003 1003 self.copy_action.setEnabled(self.can_copy())
1004 1004 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1005 1005
1006 1006 self.paste_action = menu.addAction('Paste', self.paste)
1007 1007 self.paste_action.setEnabled(self.can_paste())
1008 1008 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1009 1009
1010 1010 anchor = self._control.anchorAt(pos)
1011 1011 if anchor:
1012 1012 menu.addSeparator()
1013 1013 self.copy_link_action = menu.addAction(
1014 1014 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1015 1015 self.open_link_action = menu.addAction(
1016 1016 'Open Link', lambda: self.open_anchor(anchor=anchor))
1017 1017
1018 1018 menu.addSeparator()
1019 1019 menu.addAction(self.select_all_action)
1020 1020
1021 1021 menu.addSeparator()
1022 1022 menu.addAction(self.export_action)
1023 1023 menu.addAction(self.print_action)
1024 1024
1025 1025 return menu
1026 1026
1027 1027 def _control_key_down(self, modifiers, include_command=False):
1028 1028 """ Given a KeyboardModifiers flags object, return whether the Control
1029 1029 key is down.
1030 1030
1031 1031 Parameters:
1032 1032 -----------
1033 1033 include_command : bool, optional (default True)
1034 1034 Whether to treat the Command key as a (mutually exclusive) synonym
1035 1035 for Control when in Mac OS.
1036 1036 """
1037 1037 # Note that on Mac OS, ControlModifier corresponds to the Command key
1038 1038 # while MetaModifier corresponds to the Control key.
1039 1039 if sys.platform == 'darwin':
1040 1040 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1041 1041 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1042 1042 else:
1043 1043 return bool(modifiers & QtCore.Qt.ControlModifier)
1044 1044
1045 1045 def _create_control(self):
1046 1046 """ Creates and connects the underlying text widget.
1047 1047 """
1048 1048 # Create the underlying control.
1049 1049 if self.custom_control:
1050 1050 control = self.custom_control()
1051 1051 elif self.kind == 'plain':
1052 1052 control = QtGui.QPlainTextEdit()
1053 1053 elif self.kind == 'rich':
1054 1054 control = QtGui.QTextEdit()
1055 1055 control.setAcceptRichText(False)
1056 1056 control.setMouseTracking(True)
1057 1057
1058 1058 # Prevent the widget from handling drops, as we already provide
1059 1059 # the logic in this class.
1060 1060 control.setAcceptDrops(False)
1061 1061
1062 1062 # Install event filters. The filter on the viewport is needed for
1063 1063 # mouse events.
1064 1064 control.installEventFilter(self)
1065 1065 control.viewport().installEventFilter(self)
1066 1066
1067 1067 # Connect signals.
1068 1068 control.customContextMenuRequested.connect(
1069 1069 self._custom_context_menu_requested)
1070 1070 control.copyAvailable.connect(self.copy_available)
1071 1071 control.redoAvailable.connect(self.redo_available)
1072 1072 control.undoAvailable.connect(self.undo_available)
1073 1073
1074 1074 # Hijack the document size change signal to prevent Qt from adjusting
1075 1075 # the viewport's scrollbar. We are relying on an implementation detail
1076 1076 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1077 1077 # this functionality we cannot create a nice terminal interface.
1078 1078 layout = control.document().documentLayout()
1079 1079 layout.documentSizeChanged.disconnect()
1080 1080 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1081 1081
1082 1082 # Configure the control.
1083 1083 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1084 1084 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1085 1085 control.setReadOnly(True)
1086 1086 control.setUndoRedoEnabled(False)
1087 1087 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1088 1088 return control
1089 1089
1090 1090 def _create_page_control(self):
1091 1091 """ Creates and connects the underlying paging widget.
1092 1092 """
1093 1093 if self.custom_page_control:
1094 1094 control = self.custom_page_control()
1095 1095 elif self.kind == 'plain':
1096 1096 control = QtGui.QPlainTextEdit()
1097 1097 elif self.kind == 'rich':
1098 1098 control = QtGui.QTextEdit()
1099 1099 control.installEventFilter(self)
1100 1100 viewport = control.viewport()
1101 1101 viewport.installEventFilter(self)
1102 1102 control.setReadOnly(True)
1103 1103 control.setUndoRedoEnabled(False)
1104 1104 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1105 1105 return control
1106 1106
1107 1107 def _event_filter_console_keypress(self, event):
1108 1108 """ Filter key events for the underlying text widget to create a
1109 1109 console-like interface.
1110 1110 """
1111 1111 intercepted = False
1112 1112 cursor = self._control.textCursor()
1113 1113 position = cursor.position()
1114 1114 key = event.key()
1115 1115 ctrl_down = self._control_key_down(event.modifiers())
1116 1116 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1117 1117 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1118 1118
1119 1119 #------ Special sequences ----------------------------------------------
1120 1120
1121 1121 if event.matches(QtGui.QKeySequence.Copy):
1122 1122 self.copy()
1123 1123 intercepted = True
1124 1124
1125 1125 elif event.matches(QtGui.QKeySequence.Cut):
1126 1126 self.cut()
1127 1127 intercepted = True
1128 1128
1129 1129 elif event.matches(QtGui.QKeySequence.Paste):
1130 1130 self.paste()
1131 1131 intercepted = True
1132 1132
1133 1133 #------ Special modifier logic -----------------------------------------
1134 1134
1135 1135 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1136 1136 intercepted = True
1137 1137
1138 1138 # Special handling when tab completing in text mode.
1139 1139 self._cancel_completion()
1140 1140
1141 1141 if self._in_buffer(position):
1142 1142 # Special handling when a reading a line of raw input.
1143 1143 if self._reading:
1144 1144 self._append_plain_text('\n')
1145 1145 self._reading = False
1146 1146 if self._reading_callback:
1147 1147 self._reading_callback()
1148 1148
1149 1149 # If the input buffer is a single line or there is only
1150 1150 # whitespace after the cursor, execute. Otherwise, split the
1151 1151 # line with a continuation prompt.
1152 1152 elif not self._executing:
1153 1153 cursor.movePosition(QtGui.QTextCursor.End,
1154 1154 QtGui.QTextCursor.KeepAnchor)
1155 1155 at_end = len(cursor.selectedText().strip()) == 0
1156 1156 single_line = (self._get_end_cursor().blockNumber() ==
1157 1157 self._get_prompt_cursor().blockNumber())
1158 1158 if (at_end or shift_down or single_line) and not ctrl_down:
1159 1159 self.execute(interactive = not shift_down)
1160 1160 else:
1161 1161 # Do this inside an edit block for clean undo/redo.
1162 1162 cursor.beginEditBlock()
1163 1163 cursor.setPosition(position)
1164 1164 cursor.insertText('\n')
1165 1165 self._insert_continuation_prompt(cursor)
1166 1166 cursor.endEditBlock()
1167 1167
1168 1168 # Ensure that the whole input buffer is visible.
1169 1169 # FIXME: This will not be usable if the input buffer is
1170 1170 # taller than the console widget.
1171 1171 self._control.moveCursor(QtGui.QTextCursor.End)
1172 1172 self._control.setTextCursor(cursor)
1173 1173
1174 1174 #------ Control/Cmd modifier -------------------------------------------
1175 1175
1176 1176 elif ctrl_down:
1177 1177 if key == QtCore.Qt.Key_G:
1178 1178 self._keyboard_quit()
1179 1179 intercepted = True
1180 1180
1181 1181 elif key == QtCore.Qt.Key_K:
1182 1182 if self._in_buffer(position):
1183 1183 cursor.clearSelection()
1184 1184 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1185 1185 QtGui.QTextCursor.KeepAnchor)
1186 1186 if not cursor.hasSelection():
1187 1187 # Line deletion (remove continuation prompt)
1188 1188 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1189 1189 QtGui.QTextCursor.KeepAnchor)
1190 1190 cursor.movePosition(QtGui.QTextCursor.Right,
1191 1191 QtGui.QTextCursor.KeepAnchor,
1192 1192 len(self._continuation_prompt))
1193 1193 self._kill_ring.kill_cursor(cursor)
1194 1194 self._set_cursor(cursor)
1195 1195 intercepted = True
1196 1196
1197 1197 elif key == QtCore.Qt.Key_L:
1198 1198 self.prompt_to_top()
1199 1199 intercepted = True
1200 1200
1201 1201 elif key == QtCore.Qt.Key_O:
1202 1202 if self._page_control and self._page_control.isVisible():
1203 1203 self._page_control.setFocus()
1204 1204 intercepted = True
1205 1205
1206 1206 elif key == QtCore.Qt.Key_U:
1207 1207 if self._in_buffer(position):
1208 1208 cursor.clearSelection()
1209 1209 start_line = cursor.blockNumber()
1210 1210 if start_line == self._get_prompt_cursor().blockNumber():
1211 1211 offset = len(self._prompt)
1212 1212 else:
1213 1213 offset = len(self._continuation_prompt)
1214 1214 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1215 1215 QtGui.QTextCursor.KeepAnchor)
1216 1216 cursor.movePosition(QtGui.QTextCursor.Right,
1217 1217 QtGui.QTextCursor.KeepAnchor, offset)
1218 1218 self._kill_ring.kill_cursor(cursor)
1219 1219 self._set_cursor(cursor)
1220 1220 intercepted = True
1221 1221
1222 1222 elif key == QtCore.Qt.Key_Y:
1223 1223 self._keep_cursor_in_buffer()
1224 1224 self._kill_ring.yank()
1225 1225 intercepted = True
1226 1226
1227 1227 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1228 1228 if key == QtCore.Qt.Key_Backspace:
1229 1229 cursor = self._get_word_start_cursor(position)
1230 1230 else: # key == QtCore.Qt.Key_Delete
1231 1231 cursor = self._get_word_end_cursor(position)
1232 1232 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1233 1233 self._kill_ring.kill_cursor(cursor)
1234 1234 intercepted = True
1235 1235
1236 1236 elif key == QtCore.Qt.Key_D:
1237 1237 if len(self.input_buffer) == 0:
1238 1238 self.exit_requested.emit(self)
1239 1239 else:
1240 1240 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1241 1241 QtCore.Qt.Key_Delete,
1242 1242 QtCore.Qt.NoModifier)
1243 1243 QtGui.qApp.sendEvent(self._control, new_event)
1244 1244 intercepted = True
1245 1245
1246 1246 #------ Alt modifier ---------------------------------------------------
1247 1247
1248 1248 elif alt_down:
1249 1249 if key == QtCore.Qt.Key_B:
1250 1250 self._set_cursor(self._get_word_start_cursor(position))
1251 1251 intercepted = True
1252 1252
1253 1253 elif key == QtCore.Qt.Key_F:
1254 1254 self._set_cursor(self._get_word_end_cursor(position))
1255 1255 intercepted = True
1256 1256
1257 1257 elif key == QtCore.Qt.Key_Y:
1258 1258 self._kill_ring.rotate()
1259 1259 intercepted = True
1260 1260
1261 1261 elif key == QtCore.Qt.Key_Backspace:
1262 1262 cursor = self._get_word_start_cursor(position)
1263 1263 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1264 1264 self._kill_ring.kill_cursor(cursor)
1265 1265 intercepted = True
1266 1266
1267 1267 elif key == QtCore.Qt.Key_D:
1268 1268 cursor = self._get_word_end_cursor(position)
1269 1269 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1270 1270 self._kill_ring.kill_cursor(cursor)
1271 1271 intercepted = True
1272 1272
1273 1273 elif key == QtCore.Qt.Key_Delete:
1274 1274 intercepted = True
1275 1275
1276 1276 elif key == QtCore.Qt.Key_Greater:
1277 1277 self._control.moveCursor(QtGui.QTextCursor.End)
1278 1278 intercepted = True
1279 1279
1280 1280 elif key == QtCore.Qt.Key_Less:
1281 1281 self._control.setTextCursor(self._get_prompt_cursor())
1282 1282 intercepted = True
1283 1283
1284 1284 #------ No modifiers ---------------------------------------------------
1285 1285
1286 1286 else:
1287 1287 if shift_down:
1288 1288 anchormode = QtGui.QTextCursor.KeepAnchor
1289 1289 else:
1290 1290 anchormode = QtGui.QTextCursor.MoveAnchor
1291 1291
1292 1292 if key == QtCore.Qt.Key_Escape:
1293 1293 self._keyboard_quit()
1294 1294 intercepted = True
1295 1295
1296 1296 elif key == QtCore.Qt.Key_Up:
1297 1297 if self._reading or not self._up_pressed(shift_down):
1298 1298 intercepted = True
1299 1299 else:
1300 1300 prompt_line = self._get_prompt_cursor().blockNumber()
1301 1301 intercepted = cursor.blockNumber() <= prompt_line
1302 1302
1303 1303 elif key == QtCore.Qt.Key_Down:
1304 1304 if self._reading or not self._down_pressed(shift_down):
1305 1305 intercepted = True
1306 1306 else:
1307 1307 end_line = self._get_end_cursor().blockNumber()
1308 1308 intercepted = cursor.blockNumber() == end_line
1309 1309
1310 1310 elif key == QtCore.Qt.Key_Tab:
1311 1311 if not self._reading:
1312 1312 if self._tab_pressed():
1313 1313 # real tab-key, insert four spaces
1314 1314 cursor.insertText(' '*4)
1315 1315 intercepted = True
1316 1316
1317 1317 elif key == QtCore.Qt.Key_Left:
1318 1318
1319 1319 # Move to the previous line
1320 1320 line, col = cursor.blockNumber(), cursor.columnNumber()
1321 1321 if line > self._get_prompt_cursor().blockNumber() and \
1322 1322 col == len(self._continuation_prompt):
1323 1323 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1324 1324 mode=anchormode)
1325 1325 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1326 1326 mode=anchormode)
1327 1327 intercepted = True
1328 1328
1329 1329 # Regular left movement
1330 1330 else:
1331 1331 intercepted = not self._in_buffer(position - 1)
1332 1332
1333 1333 elif key == QtCore.Qt.Key_Right:
1334 1334 original_block_number = cursor.blockNumber()
1335 1335 cursor.movePosition(QtGui.QTextCursor.Right,
1336 1336 mode=anchormode)
1337 1337 if cursor.blockNumber() != original_block_number:
1338 1338 cursor.movePosition(QtGui.QTextCursor.Right,
1339 1339 n=len(self._continuation_prompt),
1340 1340 mode=anchormode)
1341 1341 self._set_cursor(cursor)
1342 1342 intercepted = True
1343 1343
1344 1344 elif key == QtCore.Qt.Key_Home:
1345 1345 start_line = cursor.blockNumber()
1346 1346 if start_line == self._get_prompt_cursor().blockNumber():
1347 1347 start_pos = self._prompt_pos
1348 1348 else:
1349 1349 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1350 1350 QtGui.QTextCursor.KeepAnchor)
1351 1351 start_pos = cursor.position()
1352 1352 start_pos += len(self._continuation_prompt)
1353 1353 cursor.setPosition(position)
1354 1354 if shift_down and self._in_buffer(position):
1355 1355 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1356 1356 else:
1357 1357 cursor.setPosition(start_pos)
1358 1358 self._set_cursor(cursor)
1359 1359 intercepted = True
1360 1360
1361 1361 elif key == QtCore.Qt.Key_Backspace:
1362 1362
1363 1363 # Line deletion (remove continuation prompt)
1364 1364 line, col = cursor.blockNumber(), cursor.columnNumber()
1365 1365 if not self._reading and \
1366 1366 col == len(self._continuation_prompt) and \
1367 1367 line > self._get_prompt_cursor().blockNumber():
1368 1368 cursor.beginEditBlock()
1369 1369 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1370 1370 QtGui.QTextCursor.KeepAnchor)
1371 1371 cursor.removeSelectedText()
1372 1372 cursor.deletePreviousChar()
1373 1373 cursor.endEditBlock()
1374 1374 intercepted = True
1375 1375
1376 1376 # Regular backwards deletion
1377 1377 else:
1378 1378 anchor = cursor.anchor()
1379 1379 if anchor == position:
1380 1380 intercepted = not self._in_buffer(position - 1)
1381 1381 else:
1382 1382 intercepted = not self._in_buffer(min(anchor, position))
1383 1383
1384 1384 elif key == QtCore.Qt.Key_Delete:
1385 1385
1386 1386 # Line deletion (remove continuation prompt)
1387 1387 if not self._reading and self._in_buffer(position) and \
1388 1388 cursor.atBlockEnd() and not cursor.hasSelection():
1389 1389 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1390 1390 QtGui.QTextCursor.KeepAnchor)
1391 1391 cursor.movePosition(QtGui.QTextCursor.Right,
1392 1392 QtGui.QTextCursor.KeepAnchor,
1393 1393 len(self._continuation_prompt))
1394 1394 cursor.removeSelectedText()
1395 1395 intercepted = True
1396 1396
1397 1397 # Regular forwards deletion:
1398 1398 else:
1399 1399 anchor = cursor.anchor()
1400 1400 intercepted = (not self._in_buffer(anchor) or
1401 1401 not self._in_buffer(position))
1402 1402
1403 1403 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1404 1404 # using the keyboard in any part of the buffer. Also, permit scrolling
1405 1405 # with Page Up/Down keys. Finally, if we're executing, don't move the
1406 1406 # cursor (if even this made sense, we can't guarantee that the prompt
1407 1407 # position is still valid due to text truncation).
1408 1408 if not (self._control_key_down(event.modifiers(), include_command=True)
1409 1409 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1410 1410 or (self._executing and not self._reading)):
1411 1411 self._keep_cursor_in_buffer()
1412 1412
1413 1413 return intercepted
1414 1414
1415 1415 def _event_filter_page_keypress(self, event):
1416 1416 """ Filter key events for the paging widget to create console-like
1417 1417 interface.
1418 1418 """
1419 1419 key = event.key()
1420 1420 ctrl_down = self._control_key_down(event.modifiers())
1421 1421 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1422 1422
1423 1423 if ctrl_down:
1424 1424 if key == QtCore.Qt.Key_O:
1425 1425 self._control.setFocus()
1426 1426 intercept = True
1427 1427
1428 1428 elif alt_down:
1429 1429 if key == QtCore.Qt.Key_Greater:
1430 1430 self._page_control.moveCursor(QtGui.QTextCursor.End)
1431 1431 intercepted = True
1432 1432
1433 1433 elif key == QtCore.Qt.Key_Less:
1434 1434 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1435 1435 intercepted = True
1436 1436
1437 1437 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1438 1438 if self._splitter:
1439 1439 self._page_control.hide()
1440 1440 self._control.setFocus()
1441 1441 else:
1442 1442 self.layout().setCurrentWidget(self._control)
1443 1443 return True
1444 1444
1445 1445 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1446 1446 QtCore.Qt.Key_Tab):
1447 1447 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1448 1448 QtCore.Qt.Key_PageDown,
1449 1449 QtCore.Qt.NoModifier)
1450 1450 QtGui.qApp.sendEvent(self._page_control, new_event)
1451 1451 return True
1452 1452
1453 1453 elif key == QtCore.Qt.Key_Backspace:
1454 1454 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1455 1455 QtCore.Qt.Key_PageUp,
1456 1456 QtCore.Qt.NoModifier)
1457 1457 QtGui.qApp.sendEvent(self._page_control, new_event)
1458 1458 return True
1459 1459
1460 1460 return False
1461 1461
1462 1462 def _format_as_columns(self, items, separator=' '):
1463 1463 """ Transform a list of strings into a single string with columns.
1464 1464
1465 1465 Parameters
1466 1466 ----------
1467 1467 items : sequence of strings
1468 1468 The strings to process.
1469 1469
1470 1470 separator : str, optional [default is two spaces]
1471 1471 The string that separates columns.
1472 1472
1473 1473 Returns
1474 1474 -------
1475 1475 The formatted string.
1476 1476 """
1477 1477 # Calculate the number of characters available.
1478 1478 width = self._control.viewport().width()
1479 1479 char_width = QtGui.QFontMetrics(self.font).width(' ')
1480 1480 displaywidth = max(10, (width / char_width) - 1)
1481 1481
1482 1482 return columnize(items, separator, displaywidth)
1483 1483
1484 1484 def _get_block_plain_text(self, block):
1485 1485 """ Given a QTextBlock, return its unformatted text.
1486 1486 """
1487 1487 cursor = QtGui.QTextCursor(block)
1488 1488 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1489 1489 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1490 1490 QtGui.QTextCursor.KeepAnchor)
1491 1491 return cursor.selection().toPlainText()
1492 1492
1493 1493 def _get_cursor(self):
1494 1494 """ Convenience method that returns a cursor for the current position.
1495 1495 """
1496 1496 return self._control.textCursor()
1497 1497
1498 1498 def _get_end_cursor(self):
1499 1499 """ Convenience method that returns a cursor for the last character.
1500 1500 """
1501 1501 cursor = self._control.textCursor()
1502 1502 cursor.movePosition(QtGui.QTextCursor.End)
1503 1503 return cursor
1504 1504
1505 1505 def _get_input_buffer_cursor_column(self):
1506 1506 """ Returns the column of the cursor in the input buffer, excluding the
1507 1507 contribution by the prompt, or -1 if there is no such column.
1508 1508 """
1509 1509 prompt = self._get_input_buffer_cursor_prompt()
1510 1510 if prompt is None:
1511 1511 return -1
1512 1512 else:
1513 1513 cursor = self._control.textCursor()
1514 1514 return cursor.columnNumber() - len(prompt)
1515 1515
1516 1516 def _get_input_buffer_cursor_line(self):
1517 1517 """ Returns the text of the line of the input buffer that contains the
1518 1518 cursor, or None if there is no such line.
1519 1519 """
1520 1520 prompt = self._get_input_buffer_cursor_prompt()
1521 1521 if prompt is None:
1522 1522 return None
1523 1523 else:
1524 1524 cursor = self._control.textCursor()
1525 1525 text = self._get_block_plain_text(cursor.block())
1526 1526 return text[len(prompt):]
1527 1527
1528 1528 def _get_input_buffer_cursor_prompt(self):
1529 1529 """ Returns the (plain text) prompt for line of the input buffer that
1530 1530 contains the cursor, or None if there is no such line.
1531 1531 """
1532 1532 if self._executing:
1533 1533 return None
1534 1534 cursor = self._control.textCursor()
1535 1535 if cursor.position() >= self._prompt_pos:
1536 1536 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1537 1537 return self._prompt
1538 1538 else:
1539 1539 return self._continuation_prompt
1540 1540 else:
1541 1541 return None
1542 1542
1543 1543 def _get_prompt_cursor(self):
1544 1544 """ Convenience method that returns a cursor for the prompt position.
1545 1545 """
1546 1546 cursor = self._control.textCursor()
1547 1547 cursor.setPosition(self._prompt_pos)
1548 1548 return cursor
1549 1549
1550 1550 def _get_selection_cursor(self, start, end):
1551 1551 """ Convenience method that returns a cursor with text selected between
1552 1552 the positions 'start' and 'end'.
1553 1553 """
1554 1554 cursor = self._control.textCursor()
1555 1555 cursor.setPosition(start)
1556 1556 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1557 1557 return cursor
1558 1558
1559 1559 def _get_word_start_cursor(self, position):
1560 1560 """ Find the start of the word to the left the given position. If a
1561 1561 sequence of non-word characters precedes the first word, skip over
1562 1562 them. (This emulates the behavior of bash, emacs, etc.)
1563 1563 """
1564 1564 document = self._control.document()
1565 1565 position -= 1
1566 1566 while position >= self._prompt_pos and \
1567 1567 not is_letter_or_number(document.characterAt(position)):
1568 1568 position -= 1
1569 1569 while position >= self._prompt_pos and \
1570 1570 is_letter_or_number(document.characterAt(position)):
1571 1571 position -= 1
1572 1572 cursor = self._control.textCursor()
1573 1573 cursor.setPosition(position + 1)
1574 1574 return cursor
1575 1575
1576 1576 def _get_word_end_cursor(self, position):
1577 1577 """ Find the end of the word to the right the given position. If a
1578 1578 sequence of non-word characters precedes the first word, skip over
1579 1579 them. (This emulates the behavior of bash, emacs, etc.)
1580 1580 """
1581 1581 document = self._control.document()
1582 1582 end = self._get_end_cursor().position()
1583 1583 while position < end and \
1584 1584 not is_letter_or_number(document.characterAt(position)):
1585 1585 position += 1
1586 1586 while position < end and \
1587 1587 is_letter_or_number(document.characterAt(position)):
1588 1588 position += 1
1589 1589 cursor = self._control.textCursor()
1590 1590 cursor.setPosition(position)
1591 1591 return cursor
1592 1592
1593 1593 def _insert_continuation_prompt(self, cursor):
1594 1594 """ Inserts new continuation prompt using the specified cursor.
1595 1595 """
1596 1596 if self._continuation_prompt_html is None:
1597 1597 self._insert_plain_text(cursor, self._continuation_prompt)
1598 1598 else:
1599 1599 self._continuation_prompt = self._insert_html_fetching_plain_text(
1600 1600 cursor, self._continuation_prompt_html)
1601 1601
1602 1602 def _insert_block(self, cursor, block_format=None):
1603 1603 """ Inserts an empty QTextBlock using the specified cursor.
1604 1604 """
1605 1605 if block_format is None:
1606 1606 block_format = QtGui.QTextBlockFormat()
1607 1607 cursor.insertBlock(block_format)
1608 1608
1609 1609 def _insert_html(self, cursor, html):
1610 1610 """ Inserts HTML using the specified cursor in such a way that future
1611 1611 formatting is unaffected.
1612 1612 """
1613 1613 cursor.beginEditBlock()
1614 1614 cursor.insertHtml(html)
1615 1615
1616 1616 # After inserting HTML, the text document "remembers" it's in "html
1617 1617 # mode", which means that subsequent calls adding plain text will result
1618 1618 # in unwanted formatting, lost tab characters, etc. The following code
1619 1619 # hacks around this behavior, which I consider to be a bug in Qt, by
1620 1620 # (crudely) resetting the document's style state.
1621 1621 cursor.movePosition(QtGui.QTextCursor.Left,
1622 1622 QtGui.QTextCursor.KeepAnchor)
1623 1623 if cursor.selection().toPlainText() == ' ':
1624 1624 cursor.removeSelectedText()
1625 1625 else:
1626 1626 cursor.movePosition(QtGui.QTextCursor.Right)
1627 1627 cursor.insertText(' ', QtGui.QTextCharFormat())
1628 1628 cursor.endEditBlock()
1629 1629
1630 1630 def _insert_html_fetching_plain_text(self, cursor, html):
1631 1631 """ Inserts HTML using the specified cursor, then returns its plain text
1632 1632 version.
1633 1633 """
1634 1634 cursor.beginEditBlock()
1635 1635 cursor.removeSelectedText()
1636 1636
1637 1637 start = cursor.position()
1638 1638 self._insert_html(cursor, html)
1639 1639 end = cursor.position()
1640 1640 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1641 1641 text = cursor.selection().toPlainText()
1642 1642
1643 1643 cursor.setPosition(end)
1644 1644 cursor.endEditBlock()
1645 1645 return text
1646 1646
1647 1647 def _insert_plain_text(self, cursor, text):
1648 1648 """ Inserts plain text using the specified cursor, processing ANSI codes
1649 1649 if enabled.
1650 1650 """
1651 1651 cursor.beginEditBlock()
1652 1652 if self.ansi_codes:
1653 1653 for substring in self._ansi_processor.split_string(text):
1654 1654 for act in self._ansi_processor.actions:
1655 1655
1656 1656 # Unlike real terminal emulators, we don't distinguish
1657 1657 # between the screen and the scrollback buffer. A screen
1658 1658 # erase request clears everything.
1659 1659 if act.action == 'erase' and act.area == 'screen':
1660 1660 cursor.select(QtGui.QTextCursor.Document)
1661 1661 cursor.removeSelectedText()
1662 1662
1663 1663 # Simulate a form feed by scrolling just past the last line.
1664 1664 elif act.action == 'scroll' and act.unit == 'page':
1665 1665 cursor.insertText('\n')
1666 1666 cursor.endEditBlock()
1667 1667 self._set_top_cursor(cursor)
1668 1668 cursor.joinPreviousEditBlock()
1669 1669 cursor.deletePreviousChar()
1670 1670
1671 1671 elif act.action == 'carriage-return':
1672 1672 cursor.movePosition(
1673 1673 cursor.StartOfLine, cursor.KeepAnchor)
1674 1674
1675 1675 elif act.action == 'beep':
1676 1676 QtGui.qApp.beep()
1677 1677
1678 1678 elif act.action == 'backspace':
1679 1679 if not cursor.atBlockStart():
1680 1680 cursor.movePosition(
1681 1681 cursor.PreviousCharacter, cursor.KeepAnchor)
1682 1682
1683 1683 elif act.action == 'newline':
1684 1684 cursor.movePosition(cursor.EndOfLine)
1685 1685
1686 1686 format = self._ansi_processor.get_format()
1687 1687
1688 1688 selection = cursor.selectedText()
1689 1689 if len(selection) == 0:
1690 1690 cursor.insertText(substring, format)
1691 1691 elif substring is not None:
1692 1692 # BS and CR are treated as a change in print
1693 1693 # position, rather than a backwards character
1694 1694 # deletion for output equivalence with (I)Python
1695 1695 # terminal.
1696 1696 if len(substring) >= len(selection):
1697 1697 cursor.insertText(substring, format)
1698 1698 else:
1699 1699 old_text = selection[len(substring):]
1700 1700 cursor.insertText(substring + old_text, format)
1701 1701 cursor.movePosition(cursor.PreviousCharacter,
1702 1702 cursor.KeepAnchor, len(old_text))
1703 1703 else:
1704 1704 cursor.insertText(text)
1705 1705 cursor.endEditBlock()
1706 1706
1707 1707 def _insert_plain_text_into_buffer(self, cursor, text):
1708 1708 """ Inserts text into the input buffer using the specified cursor (which
1709 1709 must be in the input buffer), ensuring that continuation prompts are
1710 1710 inserted as necessary.
1711 1711 """
1712 1712 lines = text.splitlines(True)
1713 1713 if lines:
1714 1714 cursor.beginEditBlock()
1715 1715 cursor.insertText(lines[0])
1716 1716 for line in lines[1:]:
1717 1717 if self._continuation_prompt_html is None:
1718 1718 cursor.insertText(self._continuation_prompt)
1719 1719 else:
1720 1720 self._continuation_prompt = \
1721 1721 self._insert_html_fetching_plain_text(
1722 1722 cursor, self._continuation_prompt_html)
1723 1723 cursor.insertText(line)
1724 1724 cursor.endEditBlock()
1725 1725
1726 1726 def _in_buffer(self, position=None):
1727 1727 """ Returns whether the current cursor (or, if specified, a position) is
1728 1728 inside the editing region.
1729 1729 """
1730 1730 cursor = self._control.textCursor()
1731 1731 if position is None:
1732 1732 position = cursor.position()
1733 1733 else:
1734 1734 cursor.setPosition(position)
1735 1735 line = cursor.blockNumber()
1736 1736 prompt_line = self._get_prompt_cursor().blockNumber()
1737 1737 if line == prompt_line:
1738 1738 return position >= self._prompt_pos
1739 1739 elif line > prompt_line:
1740 1740 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1741 1741 prompt_pos = cursor.position() + len(self._continuation_prompt)
1742 1742 return position >= prompt_pos
1743 1743 return False
1744 1744
1745 1745 def _keep_cursor_in_buffer(self):
1746 1746 """ Ensures that the cursor is inside the editing region. Returns
1747 1747 whether the cursor was moved.
1748 1748 """
1749 1749 moved = not self._in_buffer()
1750 1750 if moved:
1751 1751 cursor = self._control.textCursor()
1752 1752 cursor.movePosition(QtGui.QTextCursor.End)
1753 1753 self._control.setTextCursor(cursor)
1754 1754 return moved
1755 1755
1756 1756 def _keyboard_quit(self):
1757 1757 """ Cancels the current editing task ala Ctrl-G in Emacs.
1758 1758 """
1759 1759 if self._temp_buffer_filled :
1760 1760 self._cancel_completion()
1761 1761 self._clear_temporary_buffer()
1762 1762 else:
1763 1763 self.input_buffer = ''
1764 1764
1765 1765 def _page(self, text, html=False):
1766 1766 """ Displays text using the pager if it exceeds the height of the
1767 1767 viewport.
1768 1768
1769 1769 Parameters:
1770 1770 -----------
1771 1771 html : bool, optional (default False)
1772 1772 If set, the text will be interpreted as HTML instead of plain text.
1773 1773 """
1774 1774 line_height = QtGui.QFontMetrics(self.font).height()
1775 1775 minlines = self._control.viewport().height() / line_height
1776 1776 if self.paging != 'none' and \
1777 1777 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1778 1778 if self.paging == 'custom':
1779 1779 self.custom_page_requested.emit(text)
1780 1780 else:
1781 1781 self._page_control.clear()
1782 1782 cursor = self._page_control.textCursor()
1783 1783 if html:
1784 1784 self._insert_html(cursor, text)
1785 1785 else:
1786 1786 self._insert_plain_text(cursor, text)
1787 1787 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1788 1788
1789 1789 self._page_control.viewport().resize(self._control.size())
1790 1790 if self._splitter:
1791 1791 self._page_control.show()
1792 1792 self._page_control.setFocus()
1793 1793 else:
1794 1794 self.layout().setCurrentWidget(self._page_control)
1795 1795 elif html:
1796 1796 self._append_html(text)
1797 1797 else:
1798 1798 self._append_plain_text(text)
1799 1799
1800 1800 def _set_paging(self, paging):
1801 1801 """
1802 1802 Change the pager to `paging` style.
1803 1803
1804 1804 XXX: currently, this is limited to switching between 'hsplit' and
1805 1805 'vsplit'.
1806 1806
1807 1807 Parameters:
1808 1808 -----------
1809 1809 paging : string
1810 1810 Either "hsplit", "vsplit", or "inside"
1811 1811 """
1812 1812 if self._splitter is None:
1813 1813 raise NotImplementedError("""can only switch if --paging=hsplit or
1814 1814 --paging=vsplit is used.""")
1815 1815 if paging == 'hsplit':
1816 1816 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1817 1817 elif paging == 'vsplit':
1818 1818 self._splitter.setOrientation(QtCore.Qt.Vertical)
1819 1819 elif paging == 'inside':
1820 1820 raise NotImplementedError("""switching to 'inside' paging not
1821 1821 supported yet.""")
1822 1822 else:
1823 1823 raise ValueError("unknown paging method '%s'" % paging)
1824 1824 self.paging = paging
1825 1825
1826 1826 def _prompt_finished(self):
1827 1827 """ Called immediately after a prompt is finished, i.e. when some input
1828 1828 will be processed and a new prompt displayed.
1829 1829 """
1830 1830 self._control.setReadOnly(True)
1831 1831 self._prompt_finished_hook()
1832 1832
1833 1833 def _prompt_started(self):
1834 1834 """ Called immediately after a new prompt is displayed.
1835 1835 """
1836 1836 # Temporarily disable the maximum block count to permit undo/redo and
1837 1837 # to ensure that the prompt position does not change due to truncation.
1838 1838 self._control.document().setMaximumBlockCount(0)
1839 1839 self._control.setUndoRedoEnabled(True)
1840 1840
1841 1841 # Work around bug in QPlainTextEdit: input method is not re-enabled
1842 1842 # when read-only is disabled.
1843 1843 self._control.setReadOnly(False)
1844 1844 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1845 1845
1846 1846 if not self._reading:
1847 1847 self._executing = False
1848 1848 self._prompt_started_hook()
1849 1849
1850 1850 # If the input buffer has changed while executing, load it.
1851 1851 if self._input_buffer_pending:
1852 1852 self.input_buffer = self._input_buffer_pending
1853 1853 self._input_buffer_pending = ''
1854 1854
1855 1855 self._control.moveCursor(QtGui.QTextCursor.End)
1856 1856
1857 1857 def _readline(self, prompt='', callback=None):
1858 1858 """ Reads one line of input from the user.
1859 1859
1860 1860 Parameters
1861 1861 ----------
1862 1862 prompt : str, optional
1863 1863 The prompt to print before reading the line.
1864 1864
1865 1865 callback : callable, optional
1866 1866 A callback to execute with the read line. If not specified, input is
1867 1867 read *synchronously* and this method does not return until it has
1868 1868 been read.
1869 1869
1870 1870 Returns
1871 1871 -------
1872 1872 If a callback is specified, returns nothing. Otherwise, returns the
1873 1873 input string with the trailing newline stripped.
1874 1874 """
1875 1875 if self._reading:
1876 1876 raise RuntimeError('Cannot read a line. Widget is already reading.')
1877 1877
1878 1878 if not callback and not self.isVisible():
1879 1879 # If the user cannot see the widget, this function cannot return.
1880 1880 raise RuntimeError('Cannot synchronously read a line if the widget '
1881 1881 'is not visible!')
1882 1882
1883 1883 self._reading = True
1884 1884 self._show_prompt(prompt, newline=False)
1885 1885
1886 1886 if callback is None:
1887 1887 self._reading_callback = None
1888 1888 while self._reading:
1889 1889 QtCore.QCoreApplication.processEvents()
1890 1890 return self._get_input_buffer(force=True).rstrip('\n')
1891 1891
1892 1892 else:
1893 1893 self._reading_callback = lambda: \
1894 1894 callback(self._get_input_buffer(force=True).rstrip('\n'))
1895 1895
1896 1896 def _set_continuation_prompt(self, prompt, html=False):
1897 1897 """ Sets the continuation prompt.
1898 1898
1899 1899 Parameters
1900 1900 ----------
1901 1901 prompt : str
1902 1902 The prompt to show when more input is needed.
1903 1903
1904 1904 html : bool, optional (default False)
1905 1905 If set, the prompt will be inserted as formatted HTML. Otherwise,
1906 1906 the prompt will be treated as plain text, though ANSI color codes
1907 1907 will be handled.
1908 1908 """
1909 1909 if html:
1910 1910 self._continuation_prompt_html = prompt
1911 1911 else:
1912 1912 self._continuation_prompt = prompt
1913 1913 self._continuation_prompt_html = None
1914 1914
1915 1915 def _set_cursor(self, cursor):
1916 1916 """ Convenience method to set the current cursor.
1917 1917 """
1918 1918 self._control.setTextCursor(cursor)
1919 1919
1920 1920 def _set_top_cursor(self, cursor):
1921 1921 """ Scrolls the viewport so that the specified cursor is at the top.
1922 1922 """
1923 1923 scrollbar = self._control.verticalScrollBar()
1924 1924 scrollbar.setValue(scrollbar.maximum())
1925 1925 original_cursor = self._control.textCursor()
1926 1926 self._control.setTextCursor(cursor)
1927 1927 self._control.ensureCursorVisible()
1928 1928 self._control.setTextCursor(original_cursor)
1929 1929
1930 1930 def _show_prompt(self, prompt=None, html=False, newline=True):
1931 1931 """ Writes a new prompt at the end of the buffer.
1932 1932
1933 1933 Parameters
1934 1934 ----------
1935 1935 prompt : str, optional
1936 1936 The prompt to show. If not specified, the previous prompt is used.
1937 1937
1938 1938 html : bool, optional (default False)
1939 1939 Only relevant when a prompt is specified. If set, the prompt will
1940 1940 be inserted as formatted HTML. Otherwise, the prompt will be treated
1941 1941 as plain text, though ANSI color codes will be handled.
1942 1942
1943 1943 newline : bool, optional (default True)
1944 1944 If set, a new line will be written before showing the prompt if
1945 1945 there is not already a newline at the end of the buffer.
1946 1946 """
1947 1947 # Save the current end position to support _append*(before_prompt=True).
1948 1948 cursor = self._get_end_cursor()
1949 1949 self._append_before_prompt_pos = cursor.position()
1950 1950
1951 1951 # Insert a preliminary newline, if necessary.
1952 1952 if newline and cursor.position() > 0:
1953 1953 cursor.movePosition(QtGui.QTextCursor.Left,
1954 1954 QtGui.QTextCursor.KeepAnchor)
1955 1955 if cursor.selection().toPlainText() != '\n':
1956 1956 self._append_block()
1957 1957
1958 1958 # Write the prompt.
1959 1959 self._append_plain_text(self._prompt_sep)
1960 1960 if prompt is None:
1961 1961 if self._prompt_html is None:
1962 1962 self._append_plain_text(self._prompt)
1963 1963 else:
1964 1964 self._append_html(self._prompt_html)
1965 1965 else:
1966 1966 if html:
1967 1967 self._prompt = self._append_html_fetching_plain_text(prompt)
1968 1968 self._prompt_html = prompt
1969 1969 else:
1970 1970 self._append_plain_text(prompt)
1971 1971 self._prompt = prompt
1972 1972 self._prompt_html = None
1973 1973
1974 1974 self._prompt_pos = self._get_end_cursor().position()
1975 1975 self._prompt_started()
1976 1976
1977 1977 #------ Signal handlers ----------------------------------------------------
1978 1978
1979 1979 def _adjust_scrollbars(self):
1980 1980 """ Expands the vertical scrollbar beyond the range set by Qt.
1981 1981 """
1982 1982 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1983 1983 # and qtextedit.cpp.
1984 1984 document = self._control.document()
1985 1985 scrollbar = self._control.verticalScrollBar()
1986 1986 viewport_height = self._control.viewport().height()
1987 1987 if isinstance(self._control, QtGui.QPlainTextEdit):
1988 1988 maximum = max(0, document.lineCount() - 1)
1989 1989 step = viewport_height / self._control.fontMetrics().lineSpacing()
1990 1990 else:
1991 1991 # QTextEdit does not do line-based layout and blocks will not in
1992 1992 # general have the same height. Therefore it does not make sense to
1993 1993 # attempt to scroll in line height increments.
1994 1994 maximum = document.size().height()
1995 1995 step = viewport_height
1996 1996 diff = maximum - scrollbar.maximum()
1997 1997 scrollbar.setRange(0, maximum)
1998 1998 scrollbar.setPageStep(step)
1999 1999
2000 2000 # Compensate for undesirable scrolling that occurs automatically due to
2001 2001 # maximumBlockCount() text truncation.
2002 2002 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2003 2003 scrollbar.setValue(scrollbar.value() + diff)
2004 2004
2005 2005 def _custom_context_menu_requested(self, pos):
2006 2006 """ Shows a context menu at the given QPoint (in widget coordinates).
2007 2007 """
2008 2008 menu = self._context_menu_make(pos)
2009 2009 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,785 +1,785 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, IPythonInputSplitter
16 16 from IPython.core.inputtransformer import classic_prompt
17 17 from IPython.core.oinspect import call_tip
18 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
19 19 from IPython.utils.traitlets import Bool, Instance, Unicode
20 20 from bracket_matcher import BracketMatcher
21 21 from call_tip_widget import CallTipWidget
22 22 from completion_lexer import CompletionLexer
23 23 from history_console_widget import HistoryConsoleWidget
24 24 from pygments_highlighter import PygmentsHighlighter
25 25
26 26
27 27 class FrontendHighlighter(PygmentsHighlighter):
28 28 """ A PygmentsHighlighter that understands and ignores prompts.
29 29 """
30 30
31 31 def __init__(self, frontend):
32 32 super(FrontendHighlighter, self).__init__(frontend._control.document())
33 33 self._current_offset = 0
34 34 self._frontend = frontend
35 35 self.highlighting_on = False
36 36
37 37 def highlightBlock(self, string):
38 38 """ Highlight a block of text. Reimplemented to highlight selectively.
39 39 """
40 40 if not self.highlighting_on:
41 41 return
42 42
43 43 # The input to this function is a unicode string that may contain
44 44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
45 45 # the string as plain text so we can compare it.
46 46 current_block = self.currentBlock()
47 47 string = self._frontend._get_block_plain_text(current_block)
48 48
49 49 # Decide whether to check for the regular or continuation prompt.
50 50 if current_block.contains(self._frontend._prompt_pos):
51 51 prompt = self._frontend._prompt
52 52 else:
53 53 prompt = self._frontend._continuation_prompt
54 54
55 55 # Only highlight if we can identify a prompt, but make sure not to
56 56 # highlight the prompt.
57 57 if string.startswith(prompt):
58 58 self._current_offset = len(prompt)
59 59 string = string[len(prompt):]
60 60 super(FrontendHighlighter, self).highlightBlock(string)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 super(FrontendHighlighter, self).setFormat(start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # The text to show when the kernel is (re)started.
82 82 banner = Unicode()
83 83
84 84 # An option and corresponding signal for overriding the default kernel
85 85 # interrupt behavior.
86 86 custom_interrupt = Bool(False)
87 87 custom_interrupt_requested = QtCore.Signal()
88 88
89 89 # An option and corresponding signals for overriding the default kernel
90 90 # restart behavior.
91 91 custom_restart = Bool(False)
92 92 custom_restart_kernel_died = QtCore.Signal(float)
93 93 custom_restart_requested = QtCore.Signal()
94 94
95 95 # Whether to automatically show calltips on open-parentheses.
96 96 enable_calltips = Bool(True, config=True,
97 97 help="Whether to draw information calltips on open-parentheses.")
98 98
99 99 clear_on_kernel_restart = Bool(True, config=True,
100 100 help="Whether to clear the console when the kernel is restarted")
101 101
102 102 confirm_restart = Bool(True, config=True,
103 103 help="Whether to ask for user confirmation when restarting kernel")
104 104
105 105 # Emitted when a user visible 'execute_request' has been submitted to the
106 106 # kernel from the FrontendWidget. Contains the code to be executed.
107 107 executing = QtCore.Signal(object)
108 108
109 109 # Emitted when a user-visible 'execute_reply' has been received from the
110 110 # kernel and processed by the FrontendWidget. Contains the response message.
111 111 executed = QtCore.Signal(object)
112 112
113 113 # Emitted when an exit request has been received from the kernel.
114 114 exit_requested = QtCore.Signal(object)
115 115
116 116 # Protected class variables.
117 117 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
118 118 logical_line_transforms=[],
119 119 python_line_transforms=[],
120 120 )
121 121 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
122 122 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
123 123 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
124 124 _input_splitter_class = InputSplitter
125 125 _local_kernel = False
126 126 _highlighter = Instance(FrontendHighlighter)
127 127
128 128 #---------------------------------------------------------------------------
129 129 # 'object' interface
130 130 #---------------------------------------------------------------------------
131 131
132 132 def __init__(self, *args, **kw):
133 133 super(FrontendWidget, self).__init__(*args, **kw)
134 134 # FIXME: remove this when PySide min version is updated past 1.0.7
135 135 # forcefully disable calltips if PySide is < 1.0.7, because they crash
136 136 if qt.QT_API == qt.QT_API_PYSIDE:
137 137 import PySide
138 138 if PySide.__version_info__ < (1,0,7):
139 139 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
140 140 self.enable_calltips = False
141 141
142 142 # FrontendWidget protected variables.
143 143 self._bracket_matcher = BracketMatcher(self._control)
144 144 self._call_tip_widget = CallTipWidget(self._control)
145 145 self._completion_lexer = CompletionLexer(PythonLexer())
146 146 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
147 147 self._hidden = False
148 148 self._highlighter = FrontendHighlighter(self)
149 149 self._input_splitter = self._input_splitter_class()
150 150 self._kernel_manager = None
151 151 self._kernel_client = None
152 152 self._request_info = {}
153 153 self._request_info['execute'] = {};
154 154 self._callback_dict = {}
155 155
156 156 # Configure the ConsoleWidget.
157 157 self.tab_width = 4
158 158 self._set_continuation_prompt('... ')
159 159
160 160 # Configure the CallTipWidget.
161 161 self._call_tip_widget.setFont(self.font)
162 162 self.font_changed.connect(self._call_tip_widget.setFont)
163 163
164 164 # Configure actions.
165 165 action = self._copy_raw_action
166 166 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
167 167 action.setEnabled(False)
168 168 action.setShortcut(QtGui.QKeySequence(key))
169 169 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
170 170 action.triggered.connect(self.copy_raw)
171 171 self.copy_available.connect(action.setEnabled)
172 172 self.addAction(action)
173 173
174 174 # Connect signal handlers.
175 175 document = self._control.document()
176 176 document.contentsChange.connect(self._document_contents_change)
177 177
178 178 # Set flag for whether we are connected via localhost.
179 179 self._local_kernel = kw.get('local_kernel',
180 180 FrontendWidget._local_kernel)
181 181
182 182 #---------------------------------------------------------------------------
183 183 # 'ConsoleWidget' public interface
184 184 #---------------------------------------------------------------------------
185 185
186 186 def copy(self):
187 187 """ Copy the currently selected text to the clipboard, removing prompts.
188 188 """
189 189 if self._page_control is not None and self._page_control.hasFocus():
190 190 self._page_control.copy()
191 191 elif self._control.hasFocus():
192 192 text = self._control.textCursor().selection().toPlainText()
193 193 if text:
194 194 text = self._prompt_transformer.transform_cell(text)
195 195 QtGui.QApplication.clipboard().setText(text)
196 196 else:
197 197 self.log.debug("frontend widget : unknown copy target")
198 198
199 199 #---------------------------------------------------------------------------
200 200 # 'ConsoleWidget' abstract interface
201 201 #---------------------------------------------------------------------------
202 202
203 203 def _is_complete(self, source, interactive):
204 204 """ Returns whether 'source' can be completely processed and a new
205 205 prompt created. When triggered by an Enter/Return key press,
206 206 'interactive' is True; otherwise, it is False.
207 207 """
208 208 self._input_splitter.reset()
209 209 complete = self._input_splitter.push(source)
210 210 if interactive:
211 211 complete = not self._input_splitter.push_accepts_more()
212 212 return complete
213 213
214 214 def _execute(self, source, hidden):
215 215 """ Execute 'source'. If 'hidden', do not show any output.
216 216
217 217 See parent class :meth:`execute` docstring for full details.
218 218 """
219 219 msg_id = self.kernel_client.execute(source, hidden)
220 220 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
221 221 self._hidden = hidden
222 222 if not hidden:
223 223 self.executing.emit(source)
224 224
225 225 def _prompt_started_hook(self):
226 226 """ Called immediately after a new prompt is displayed.
227 227 """
228 228 if not self._reading:
229 229 self._highlighter.highlighting_on = True
230 230
231 231 def _prompt_finished_hook(self):
232 232 """ Called immediately after a prompt is finished, i.e. when some input
233 233 will be processed and a new prompt displayed.
234 234 """
235 235 # Flush all state from the input splitter so the next round of
236 236 # reading input starts with a clean buffer.
237 237 self._input_splitter.reset()
238 238
239 239 if not self._reading:
240 240 self._highlighter.highlighting_on = False
241 241
242 242 def _tab_pressed(self):
243 243 """ Called when the tab key is pressed. Returns whether to continue
244 244 processing the event.
245 245 """
246 246 # Perform tab completion if:
247 247 # 1) The cursor is in the input buffer.
248 248 # 2) There is a non-whitespace character before the cursor.
249 249 text = self._get_input_buffer_cursor_line()
250 250 if text is None:
251 251 return False
252 252 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
253 253 if complete:
254 254 self._complete()
255 255 return not complete
256 256
257 257 #---------------------------------------------------------------------------
258 258 # 'ConsoleWidget' protected interface
259 259 #---------------------------------------------------------------------------
260 260
261 261 def _context_menu_make(self, pos):
262 262 """ Reimplemented to add an action for raw copy.
263 263 """
264 264 menu = super(FrontendWidget, self)._context_menu_make(pos)
265 265 for before_action in menu.actions():
266 266 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
267 267 QtGui.QKeySequence.ExactMatch:
268 268 menu.insertAction(before_action, self._copy_raw_action)
269 269 break
270 270 return menu
271 271
272 272 def request_interrupt_kernel(self):
273 273 if self._executing:
274 274 self.interrupt_kernel()
275 275
276 276 def request_restart_kernel(self):
277 277 message = 'Are you sure you want to restart the kernel?'
278 278 self.restart_kernel(message, now=False)
279 279
280 280 def _event_filter_console_keypress(self, event):
281 281 """ Reimplemented for execution interruption and smart backspace.
282 282 """
283 283 key = event.key()
284 284 if self._control_key_down(event.modifiers(), include_command=False):
285 285
286 286 if key == QtCore.Qt.Key_C and self._executing:
287 287 self.request_interrupt_kernel()
288 288 return True
289 289
290 290 elif key == QtCore.Qt.Key_Period:
291 291 self.request_restart_kernel()
292 292 return True
293 293
294 294 elif not event.modifiers() & QtCore.Qt.AltModifier:
295 295
296 296 # Smart backspace: remove four characters in one backspace if:
297 297 # 1) everything left of the cursor is whitespace
298 298 # 2) the four characters immediately left of the cursor are spaces
299 299 if key == QtCore.Qt.Key_Backspace:
300 300 col = self._get_input_buffer_cursor_column()
301 301 cursor = self._control.textCursor()
302 302 if col > 3 and not cursor.hasSelection():
303 303 text = self._get_input_buffer_cursor_line()[:col]
304 304 if text.endswith(' ') and not text.strip():
305 305 cursor.movePosition(QtGui.QTextCursor.Left,
306 306 QtGui.QTextCursor.KeepAnchor, 4)
307 307 cursor.removeSelectedText()
308 308 return True
309 309
310 310 return super(FrontendWidget, self)._event_filter_console_keypress(event)
311 311
312 312 def _insert_continuation_prompt(self, cursor):
313 313 """ Reimplemented for auto-indentation.
314 314 """
315 315 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
316 316 cursor.insertText(' ' * self._input_splitter.indent_spaces)
317 317
318 318 #---------------------------------------------------------------------------
319 319 # 'BaseFrontendMixin' abstract interface
320 320 #---------------------------------------------------------------------------
321 321
322 322 def _handle_complete_reply(self, rep):
323 323 """ Handle replies for tab completion.
324 324 """
325 325 self.log.debug("complete: %s", rep.get('content', ''))
326 326 cursor = self._get_cursor()
327 327 info = self._request_info.get('complete')
328 328 if info and info.id == rep['parent_header']['msg_id'] and \
329 329 info.pos == cursor.position():
330 330 text = '.'.join(self._get_context())
331 331 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
332 332 self._complete_with_items(cursor, rep['content']['matches'])
333 333
334 334 def _silent_exec_callback(self, expr, callback):
335 335 """Silently execute `expr` in the kernel and call `callback` with reply
336 336
337 337 the `expr` is evaluated silently in the kernel (without) output in
338 338 the frontend. Call `callback` with the
339 339 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
340 340
341 341 Parameters
342 342 ----------
343 343 expr : string
344 344 valid string to be executed by the kernel.
345 345 callback : function
346 346 function accepting one argument, as a string. The string will be
347 347 the `repr` of the result of evaluating `expr`
348 348
349 349 The `callback` is called with the `repr()` of the result of `expr` as
350 350 first argument. To get the object, do `eval()` on the passed value.
351 351
352 352 See Also
353 353 --------
354 354 _handle_exec_callback : private method, deal with calling callback with reply
355 355
356 356 """
357 357
358 358 # generate uuid, which would be used as an indication of whether or
359 359 # not the unique request originated from here (can use msg id ?)
360 360 local_uuid = str(uuid.uuid1())
361 361 msg_id = self.kernel_client.execute('',
362 362 silent=True, user_expressions={ local_uuid:expr })
363 363 self._callback_dict[local_uuid] = callback
364 364 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
365 365
366 366 def _handle_exec_callback(self, msg):
367 367 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
368 368
369 369 Parameters
370 370 ----------
371 371 msg : raw message send by the kernel containing an `user_expressions`
372 372 and having a 'silent_exec_callback' kind.
373 373
374 374 Notes
375 375 -----
376 376 This function will look for a `callback` associated with the
377 377 corresponding message id. Association has been made by
378 378 `_silent_exec_callback`. `callback` is then called with the `repr()`
379 379 of the value of corresponding `user_expressions` as argument.
380 380 `callback` is then removed from the known list so that any message
381 381 coming again with the same id won't trigger it.
382 382
383 383 """
384 384
385 385 user_exp = msg['content'].get('user_expressions')
386 386 if not user_exp:
387 387 return
388 388 for expression in user_exp:
389 389 if expression in self._callback_dict:
390 390 self._callback_dict.pop(expression)(user_exp[expression])
391 391
392 392 def _handle_execute_reply(self, msg):
393 393 """ Handles replies for code execution.
394 394 """
395 395 self.log.debug("execute: %s", msg.get('content', ''))
396 396 msg_id = msg['parent_header']['msg_id']
397 397 info = self._request_info['execute'].get(msg_id)
398 398 # unset reading flag, because if execute finished, raw_input can't
399 399 # still be pending.
400 400 self._reading = False
401 401 if info and info.kind == 'user' and not self._hidden:
402 402 # Make sure that all output from the SUB channel has been processed
403 403 # before writing a new prompt.
404 404 self.kernel_client.iopub_channel.flush()
405 405
406 406 # Reset the ANSI style information to prevent bad text in stdout
407 407 # from messing up our colors. We're not a true terminal so we're
408 408 # allowed to do this.
409 409 if self.ansi_codes:
410 410 self._ansi_processor.reset_sgr()
411 411
412 412 content = msg['content']
413 413 status = content['status']
414 414 if status == 'ok':
415 415 self._process_execute_ok(msg)
416 416 elif status == 'error':
417 417 self._process_execute_error(msg)
418 418 elif status == 'aborted':
419 419 self._process_execute_abort(msg)
420 420
421 421 self._show_interpreter_prompt_for_reply(msg)
422 422 self.executed.emit(msg)
423 423 self._request_info['execute'].pop(msg_id)
424 424 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
425 425 self._handle_exec_callback(msg)
426 426 self._request_info['execute'].pop(msg_id)
427 427 else:
428 428 super(FrontendWidget, self)._handle_execute_reply(msg)
429 429
430 430 def _handle_input_request(self, msg):
431 431 """ Handle requests for raw_input.
432 432 """
433 433 self.log.debug("input: %s", msg.get('content', ''))
434 434 if self._hidden:
435 435 raise RuntimeError('Request for raw input during hidden execution.')
436 436
437 437 # Make sure that all output from the SUB channel has been processed
438 438 # before entering readline mode.
439 439 self.kernel_client.iopub_channel.flush()
440 440
441 441 def callback(line):
442 442 self.kernel_client.stdin_channel.input(line)
443 443 if self._reading:
444 444 self.log.debug("Got second input request, assuming first was interrupted.")
445 445 self._reading = False
446 446 self._readline(msg['content']['prompt'], callback=callback)
447 447
448 448 def _kernel_restarted_message(self, died=True):
449 449 msg = "Kernel died, restarting" if died else "Kernel restarting"
450 450 self._append_html("<br>%s<hr><br>" % msg,
451 451 before_prompt=False
452 452 )
453 453
454 454 def _handle_kernel_died(self, since_last_heartbeat):
455 455 """Handle the kernel's death (if we do not own the kernel).
456 456 """
457 457 self.log.warn("kernel died: %s", since_last_heartbeat)
458 458 if self.custom_restart:
459 459 self.custom_restart_kernel_died.emit(since_last_heartbeat)
460 460 else:
461 461 self._kernel_restarted_message(died=True)
462 462 self.reset()
463 463
464 464 def _handle_kernel_restarted(self, died=True):
465 465 """Notice that the autorestarter restarted the kernel.
466 466
467 467 There's nothing to do but show a message.
468 468 """
469 469 self.log.warn("kernel restarted")
470 470 self._kernel_restarted_message(died=died)
471 471 self.reset()
472 472
473 473 def _handle_object_info_reply(self, rep):
474 474 """ Handle replies for call tips.
475 475 """
476 476 self.log.debug("oinfo: %s", rep.get('content', ''))
477 477 cursor = self._get_cursor()
478 478 info = self._request_info.get('call_tip')
479 479 if info and info.id == rep['parent_header']['msg_id'] and \
480 480 info.pos == cursor.position():
481 481 # Get the information for a call tip. For now we format the call
482 482 # line as string, later we can pass False to format_call and
483 483 # syntax-highlight it ourselves for nicer formatting in the
484 484 # calltip.
485 485 content = rep['content']
486 486 # if this is from pykernel, 'docstring' will be the only key
487 487 if content.get('ismagic', False):
488 488 # Don't generate a call-tip for magics. Ideally, we should
489 489 # generate a tooltip, but not on ( like we do for actual
490 490 # callables.
491 491 call_info, doc = None, None
492 492 else:
493 493 call_info, doc = call_tip(content, format_call=True)
494 494 if call_info or doc:
495 495 self._call_tip_widget.show_call_info(call_info, doc)
496 496
497 497 def _handle_pyout(self, msg):
498 498 """ Handle display hook output.
499 499 """
500 500 self.log.debug("pyout: %s", msg.get('content', ''))
501 501 if not self._hidden and self._is_from_this_session(msg):
502 502 text = msg['content']['data']
503 503 self._append_plain_text(text + '\n', before_prompt=True)
504 504
505 505 def _handle_stream(self, msg):
506 506 """ Handle stdout, stderr, and stdin.
507 507 """
508 508 self.log.debug("stream: %s", msg.get('content', ''))
509 509 if not self._hidden and self._is_from_this_session(msg):
510 510 # Most consoles treat tabs as being 8 space characters. Convert tabs
511 511 # to spaces so that output looks as expected regardless of this
512 512 # widget's tab width.
513 513 text = msg['content']['data'].expandtabs(8)
514 514
515 515 self._append_plain_text(text, before_prompt=True)
516 516 self._control.moveCursor(QtGui.QTextCursor.End)
517 517
518 518 def _handle_shutdown_reply(self, msg):
519 519 """ Handle shutdown signal, only if from other console.
520 520 """
521 521 self.log.warn("shutdown: %s", msg.get('content', ''))
522 522 restart = msg.get('content', {}).get('restart', False)
523 523 if not self._hidden and not self._is_from_this_session(msg):
524 524 # got shutdown reply, request came from session other than ours
525 525 if restart:
526 526 # someone restarted the kernel, handle it
527 527 self._handle_kernel_restarted(died=False)
528 528 else:
529 529 # kernel was shutdown permanently
530 530 # this triggers exit_requested if the kernel was local,
531 531 # and a dialog if the kernel was remote,
532 532 # so we don't suddenly clear the qtconsole without asking.
533 533 if self._local_kernel:
534 534 self.exit_requested.emit(self)
535 535 else:
536 536 title = self.window().windowTitle()
537 537 reply = QtGui.QMessageBox.question(self, title,
538 538 "Kernel has been shutdown permanently. "
539 539 "Close the Console?",
540 540 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
541 541 if reply == QtGui.QMessageBox.Yes:
542 542 self.exit_requested.emit(self)
543 543
544 544 def _handle_status(self, msg):
545 545 """Handle status message"""
546 546 # This is where a busy/idle indicator would be triggered,
547 547 # when we make one.
548 548 state = msg['content'].get('execution_state', '')
549 549 if state == 'starting':
550 550 # kernel started while we were running
551 551 if self._executing:
552 552 self._handle_kernel_restarted(died=True)
553 553 elif state == 'idle':
554 554 pass
555 555 elif state == 'busy':
556 556 pass
557 557
558 558 def _started_channels(self):
559 559 """ Called when the KernelManager channels have started listening or
560 560 when the frontend is assigned an already listening KernelManager.
561 561 """
562 562 self.reset(clear=True)
563 563
564 564 #---------------------------------------------------------------------------
565 565 # 'FrontendWidget' public interface
566 566 #---------------------------------------------------------------------------
567 567
568 568 def copy_raw(self):
569 569 """ Copy the currently selected text to the clipboard without attempting
570 570 to remove prompts or otherwise alter the text.
571 571 """
572 572 self._control.copy()
573 573
574 574 def execute_file(self, path, hidden=False):
575 575 """ Attempts to execute file with 'path'. If 'hidden', no output is
576 576 shown.
577 577 """
578 578 self.execute('execfile(%r)' % path, hidden=hidden)
579 579
580 580 def interrupt_kernel(self):
581 581 """ Attempts to interrupt the running kernel.
582 582
583 583 Also unsets _reading flag, to avoid runtime errors
584 584 if raw_input is called again.
585 585 """
586 586 if self.custom_interrupt:
587 587 self._reading = False
588 588 self.custom_interrupt_requested.emit()
589 589 elif self.kernel_manager:
590 590 self._reading = False
591 591 self.kernel_manager.interrupt_kernel()
592 592 else:
593 593 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
594 594
595 595 def reset(self, clear=False):
596 596 """ Resets the widget to its initial state if ``clear`` parameter
597 597 is True, otherwise
598 598 prints a visual indication of the fact that the kernel restarted, but
599 599 does not clear the traces from previous usage of the kernel before it
600 600 was restarted. With ``clear=True``, it is similar to ``%clear``, but
601 601 also re-writes the banner and aborts execution if necessary.
602 602 """
603 603 if self._executing:
604 604 self._executing = False
605 605 self._request_info['execute'] = {}
606 606 self._reading = False
607 607 self._highlighter.highlighting_on = False
608 608
609 609 if clear:
610 610 self._control.clear()
611 611 self._append_plain_text(self.banner)
612 612 # update output marker for stdout/stderr, so that startup
613 613 # messages appear after banner:
614 614 self._append_before_prompt_pos = self._get_cursor().position()
615 615 self._show_interpreter_prompt()
616 616
617 617 def restart_kernel(self, message, now=False):
618 618 """ Attempts to restart the running kernel.
619 619 """
620 620 # FIXME: now should be configurable via a checkbox in the dialog. Right
621 621 # now at least the heartbeat path sets it to True and the manual restart
622 622 # to False. But those should just be the pre-selected states of a
623 623 # checkbox that the user could override if so desired. But I don't know
624 624 # enough Qt to go implementing the checkbox now.
625 625
626 626 if self.custom_restart:
627 627 self.custom_restart_requested.emit()
628 628 return
629 629
630 630 if self.kernel_manager:
631 631 # Pause the heart beat channel to prevent further warnings.
632 632 self.kernel_client.hb_channel.pause()
633 633
634 634 # Prompt the user to restart the kernel. Un-pause the heartbeat if
635 635 # they decline. (If they accept, the heartbeat will be un-paused
636 636 # automatically when the kernel is restarted.)
637 637 if self.confirm_restart:
638 638 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
639 639 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
640 640 message, buttons)
641 641 do_restart = result == QtGui.QMessageBox.Yes
642 642 else:
643 643 # confirm_restart is False, so we don't need to ask user
644 644 # anything, just do the restart
645 645 do_restart = True
646 646 if do_restart:
647 647 try:
648 648 self.kernel_manager.restart_kernel(now=now)
649 649 except RuntimeError as e:
650 650 self._append_plain_text(
651 651 'Error restarting kernel: %s\n' % e,
652 652 before_prompt=True
653 653 )
654 654 else:
655 655 self._append_html("<br>Restarting kernel...\n<hr><br>",
656 656 before_prompt=True,
657 657 )
658 658 else:
659 659 self.kernel_client.hb_channel.unpause()
660 660
661 661 else:
662 662 self._append_plain_text(
663 663 'Cannot restart a Kernel I did not start\n',
664 664 before_prompt=True
665 665 )
666 666
667 667 #---------------------------------------------------------------------------
668 668 # 'FrontendWidget' protected interface
669 669 #---------------------------------------------------------------------------
670 670
671 671 def _call_tip(self):
672 672 """ Shows a call tip, if appropriate, at the current cursor location.
673 673 """
674 674 # Decide if it makes sense to show a call tip
675 675 if not self.enable_calltips:
676 676 return False
677 677 cursor = self._get_cursor()
678 678 cursor.movePosition(QtGui.QTextCursor.Left)
679 679 if cursor.document().characterAt(cursor.position()) != '(':
680 680 return False
681 681 context = self._get_context(cursor)
682 682 if not context:
683 683 return False
684 684
685 685 # Send the metadata request to the kernel
686 686 name = '.'.join(context)
687 687 msg_id = self.kernel_client.object_info(name)
688 688 pos = self._get_cursor().position()
689 689 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
690 690 return True
691 691
692 692 def _complete(self):
693 693 """ Performs completion at the current cursor location.
694 694 """
695 695 context = self._get_context()
696 696 if context:
697 697 # Send the completion request to the kernel
698 698 msg_id = self.kernel_client.complete(
699 699 '.'.join(context), # text
700 700 self._get_input_buffer_cursor_line(), # line
701 701 self._get_input_buffer_cursor_column(), # cursor_pos
702 702 self.input_buffer) # block
703 703 pos = self._get_cursor().position()
704 704 info = self._CompletionRequest(msg_id, pos)
705 705 self._request_info['complete'] = info
706 706
707 707 def _get_context(self, cursor=None):
708 708 """ Gets the context for the specified cursor (or the current cursor
709 709 if none is specified).
710 710 """
711 711 if cursor is None:
712 712 cursor = self._get_cursor()
713 713 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
714 714 QtGui.QTextCursor.KeepAnchor)
715 715 text = cursor.selection().toPlainText()
716 716 return self._completion_lexer.get_context(text)
717 717
718 718 def _process_execute_abort(self, msg):
719 719 """ Process a reply for an aborted execution request.
720 720 """
721 721 self._append_plain_text("ERROR: execution aborted\n")
722 722
723 723 def _process_execute_error(self, msg):
724 724 """ Process a reply for an execution request that resulted in an error.
725 725 """
726 726 content = msg['content']
727 727 # If a SystemExit is passed along, this means exit() was called - also
728 728 # all the ipython %exit magic syntax of '-k' to be used to keep
729 729 # the kernel running
730 730 if content['ename']=='SystemExit':
731 731 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
732 732 self._keep_kernel_on_exit = keepkernel
733 733 self.exit_requested.emit(self)
734 734 else:
735 735 traceback = ''.join(content['traceback'])
736 736 self._append_plain_text(traceback)
737 737
738 738 def _process_execute_ok(self, msg):
739 739 """ Process a reply for a successful execution request.
740 740 """
741 741 payload = msg['content']['payload']
742 742 for item in payload:
743 743 if not self._process_execute_payload(item):
744 744 warning = 'Warning: received unknown payload of type %s'
745 745 print(warning % repr(item['source']))
746 746
747 747 def _process_execute_payload(self, item):
748 748 """ Process a single payload item from the list of payload items in an
749 749 execution reply. Returns whether the payload was handled.
750 750 """
751 751 # The basic FrontendWidget doesn't handle payloads, as they are a
752 752 # mechanism for going beyond the standard Python interpreter model.
753 753 return False
754 754
755 755 def _show_interpreter_prompt(self):
756 756 """ Shows a prompt for the interpreter.
757 757 """
758 758 self._show_prompt('>>> ')
759 759
760 760 def _show_interpreter_prompt_for_reply(self, msg):
761 761 """ Shows a prompt for the interpreter given an 'execute_reply' message.
762 762 """
763 763 self._show_interpreter_prompt()
764 764
765 765 #------ Signal handlers ----------------------------------------------------
766 766
767 767 def _document_contents_change(self, position, removed, added):
768 768 """ Called whenever the document's content changes. Display a call tip
769 769 if appropriate.
770 770 """
771 771 # Calculate where the cursor should be *after* the change:
772 772 position += added
773 773
774 774 document = self._control.document()
775 775 if position == self._get_cursor().position():
776 776 self._call_tip()
777 777
778 778 #------ Trait default initializers -----------------------------------------
779 779
780 780 def _banner_default(self):
781 781 """ Returns the standard Python banner.
782 782 """
783 783 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
784 784 '"license" for more information.'
785 785 return banner % (sys.version, sys.platform)
@@ -1,388 +1,388 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14 * Paul Ivanov
15 15
16 16 """
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # stdlib imports
23 23 import os
24 24 import signal
25 25 import sys
26 26
27 27 # If run on Windows, install an exception hook which pops up a
28 28 # message box. Pythonw.exe hides the console, so without this
29 29 # the application silently fails to load.
30 30 #
31 31 # We always install this handler, because the expectation is for
32 32 # qtconsole to bring up a GUI even if called from the console.
33 33 # The old handler is called, so the exception is printed as well.
34 34 # If desired, check for pythonw with an additional condition
35 35 # (sys.executable.lower().find('pythonw.exe') >= 0).
36 36 if os.name == 'nt':
37 37 old_excepthook = sys.excepthook
38 38
39 39 def gui_excepthook(exctype, value, tb):
40 40 try:
41 41 import ctypes, traceback
42 42 MB_ICONERROR = 0x00000010L
43 43 title = u'Error starting IPython QtConsole'
44 44 msg = u''.join(traceback.format_exception(exctype, value, tb))
45 45 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
46 46 finally:
47 47 # Also call the old exception hook to let it do
48 48 # its thing too.
49 49 old_excepthook(exctype, value, tb)
50 50
51 51 sys.excepthook = gui_excepthook
52 52
53 53 # System library imports
54 54 from IPython.external.qt import QtCore, QtGui
55 55
56 56 # Local imports
57 57 from IPython.config.application import boolean_flag, catch_config_error
58 58 from IPython.core.application import BaseIPythonApplication
59 59 from IPython.core.profiledir import ProfileDir
60 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
61 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
62 from IPython.frontend.qt.console import styles
63 from IPython.frontend.qt.console.mainwindow import MainWindow
64 from IPython.frontend.qt.client import QtKernelClient
65 from IPython.frontend.qt.manager import QtKernelManager
60 from IPython.qt.console.ipython_widget import IPythonWidget
61 from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
62 from IPython.qt.console import styles
63 from IPython.qt.console.mainwindow import MainWindow
64 from IPython.qt.client import QtKernelClient
65 from IPython.qt.manager import QtKernelManager
66 66 from IPython.kernel import tunnel_to_kernel, find_connection_file
67 67 from IPython.utils.traitlets import (
68 68 Dict, List, Unicode, CBool, Any
69 69 )
70 70 from IPython.kernel.zmq.session import default_secure
71 71
72 from IPython.frontend.consoleapp import (
72 from IPython.consoleapp import (
73 73 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
74 74 )
75 75
76 76 #-----------------------------------------------------------------------------
77 77 # Network Constants
78 78 #-----------------------------------------------------------------------------
79 79
80 80 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
81 81
82 82 #-----------------------------------------------------------------------------
83 83 # Globals
84 84 #-----------------------------------------------------------------------------
85 85
86 86 _examples = """
87 87 ipython qtconsole # start the qtconsole
88 88 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
89 89 """
90 90
91 91 #-----------------------------------------------------------------------------
92 92 # Aliases and Flags
93 93 #-----------------------------------------------------------------------------
94 94
95 95 # start with copy of flags
96 96 flags = dict(flags)
97 97 qt_flags = {
98 98 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
99 99 "Disable rich text support."),
100 100 }
101 101
102 102 # and app_flags from the Console Mixin
103 103 qt_flags.update(app_flags)
104 104 # add frontend flags to the full set
105 105 flags.update(qt_flags)
106 106
107 107 # start with copy of front&backend aliases list
108 108 aliases = dict(aliases)
109 109 qt_aliases = dict(
110 110 style = 'IPythonWidget.syntax_style',
111 111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 112 colors = 'ZMQInteractiveShell.colors',
113 113
114 114 editor = 'IPythonWidget.editor',
115 115 paging = 'ConsoleWidget.paging',
116 116 )
117 117 # and app_aliases from the Console Mixin
118 118 qt_aliases.update(app_aliases)
119 119 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
120 120 # add frontend aliases to the full set
121 121 aliases.update(qt_aliases)
122 122
123 123 # get flags&aliases into sets, and remove a couple that
124 124 # shouldn't be scrubbed from backend flags:
125 125 qt_aliases = set(qt_aliases.keys())
126 126 qt_aliases.remove('colors')
127 127 qt_flags = set(qt_flags.keys())
128 128
129 129 #-----------------------------------------------------------------------------
130 130 # Classes
131 131 #-----------------------------------------------------------------------------
132 132
133 133 #-----------------------------------------------------------------------------
134 134 # IPythonQtConsole
135 135 #-----------------------------------------------------------------------------
136 136
137 137
138 138 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
139 139 name = 'ipython-qtconsole'
140 140
141 141 description = """
142 142 The IPython QtConsole.
143 143
144 144 This launches a Console-style application using Qt. It is not a full
145 145 console, in that launched terminal subprocesses will not be able to accept
146 146 input.
147 147
148 148 The QtConsole supports various extra features beyond the Terminal IPython
149 149 shell, such as inline plotting with matplotlib, via:
150 150
151 151 ipython qtconsole --pylab=inline
152 152
153 153 as well as saving your session as HTML, and printing the output.
154 154
155 155 """
156 156 examples = _examples
157 157
158 158 classes = [IPythonWidget] + IPythonConsoleApp.classes
159 159 flags = Dict(flags)
160 160 aliases = Dict(aliases)
161 161 frontend_flags = Any(qt_flags)
162 162 frontend_aliases = Any(qt_aliases)
163 163 kernel_client_class = QtKernelClient
164 164 kernel_manager_class = QtKernelManager
165 165
166 166 stylesheet = Unicode('', config=True,
167 167 help="path to a custom CSS stylesheet")
168 168
169 169 hide_menubar = CBool(False, config=True,
170 170 help="Start the console window with the menu bar hidden.")
171 171
172 172 maximize = CBool(False, config=True,
173 173 help="Start the console window maximized.")
174 174
175 175 plain = CBool(False, config=True,
176 176 help="Use a plaintext widget instead of rich text (plain can't print/save).")
177 177
178 178 def _plain_changed(self, name, old, new):
179 179 kind = 'plain' if new else 'rich'
180 180 self.config.ConsoleWidget.kind = kind
181 181 if new:
182 182 self.widget_factory = IPythonWidget
183 183 else:
184 184 self.widget_factory = RichIPythonWidget
185 185
186 186 # the factory for creating a widget
187 187 widget_factory = Any(RichIPythonWidget)
188 188
189 189 def parse_command_line(self, argv=None):
190 190 super(IPythonQtConsoleApp, self).parse_command_line(argv)
191 191 self.build_kernel_argv(argv)
192 192
193 193
194 194 def new_frontend_master(self):
195 195 """ Create and return new frontend attached to new kernel, launched on localhost.
196 196 """
197 197 kernel_manager = self.kernel_manager_class(
198 198 connection_file=self._new_connection_file(),
199 199 config=self.config,
200 200 autorestart=True,
201 201 )
202 202 # start the kernel
203 203 kwargs = dict()
204 204 kwargs['extra_arguments'] = self.kernel_argv
205 205 kernel_manager.start_kernel(**kwargs)
206 206 kernel_manager.client_factory = self.kernel_client_class
207 207 kernel_client = kernel_manager.client()
208 208 kernel_client.start_channels(shell=True, iopub=True)
209 209 widget = self.widget_factory(config=self.config,
210 210 local_kernel=True)
211 211 self.init_colors(widget)
212 212 widget.kernel_manager = kernel_manager
213 213 widget.kernel_client = kernel_client
214 214 widget._existing = False
215 215 widget._may_close = True
216 216 widget._confirm_exit = self.confirm_exit
217 217 return widget
218 218
219 219 def new_frontend_slave(self, current_widget):
220 220 """Create and return a new frontend attached to an existing kernel.
221 221
222 222 Parameters
223 223 ----------
224 224 current_widget : IPythonWidget
225 225 The IPythonWidget whose kernel this frontend is to share
226 226 """
227 227 kernel_client = self.kernel_client_class(
228 228 connection_file=current_widget.kernel_client.connection_file,
229 229 config = self.config,
230 230 )
231 231 kernel_client.load_connection_file()
232 232 kernel_client.start_channels()
233 233 widget = self.widget_factory(config=self.config,
234 234 local_kernel=False)
235 235 self.init_colors(widget)
236 236 widget._existing = True
237 237 widget._may_close = False
238 238 widget._confirm_exit = False
239 239 widget.kernel_client = kernel_client
240 240 widget.kernel_manager = current_widget.kernel_manager
241 241 return widget
242 242
243 243 def init_qt_app(self):
244 244 # separate from qt_elements, because it must run first
245 245 self.app = QtGui.QApplication([])
246 246
247 247 def init_qt_elements(self):
248 248 # Create the widget.
249 249
250 250 base_path = os.path.abspath(os.path.dirname(__file__))
251 251 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
252 252 self.app.icon = QtGui.QIcon(icon_path)
253 253 QtGui.QApplication.setWindowIcon(self.app.icon)
254 254
255 255 try:
256 256 ip = self.config.KernelManager.ip
257 257 except AttributeError:
258 258 ip = LOCALHOST
259 259 local_kernel = (not self.existing) or ip in LOCAL_IPS
260 260 self.widget = self.widget_factory(config=self.config,
261 261 local_kernel=local_kernel)
262 262 self.init_colors(self.widget)
263 263 self.widget._existing = self.existing
264 264 self.widget._may_close = not self.existing
265 265 self.widget._confirm_exit = self.confirm_exit
266 266
267 267 self.widget.kernel_manager = self.kernel_manager
268 268 self.widget.kernel_client = self.kernel_client
269 269 self.window = MainWindow(self.app,
270 270 confirm_exit=self.confirm_exit,
271 271 new_frontend_factory=self.new_frontend_master,
272 272 slave_frontend_factory=self.new_frontend_slave,
273 273 )
274 274 self.window.log = self.log
275 275 self.window.add_tab_with_frontend(self.widget)
276 276 self.window.init_menu_bar()
277 277
278 278 # Ignore on OSX, where there is always a menu bar
279 279 if sys.platform != 'darwin' and self.hide_menubar:
280 280 self.window.menuBar().setVisible(False)
281 281
282 282 self.window.setWindowTitle('IPython')
283 283
284 284 def init_colors(self, widget):
285 285 """Configure the coloring of the widget"""
286 286 # Note: This will be dramatically simplified when colors
287 287 # are removed from the backend.
288 288
289 289 # parse the colors arg down to current known labels
290 290 try:
291 291 colors = self.config.ZMQInteractiveShell.colors
292 292 except AttributeError:
293 293 colors = None
294 294 try:
295 295 style = self.config.IPythonWidget.syntax_style
296 296 except AttributeError:
297 297 style = None
298 298 try:
299 299 sheet = self.config.IPythonWidget.style_sheet
300 300 except AttributeError:
301 301 sheet = None
302 302
303 303 # find the value for colors:
304 304 if colors:
305 305 colors=colors.lower()
306 306 if colors in ('lightbg', 'light'):
307 307 colors='lightbg'
308 308 elif colors in ('dark', 'linux'):
309 309 colors='linux'
310 310 else:
311 311 colors='nocolor'
312 312 elif style:
313 313 if style=='bw':
314 314 colors='nocolor'
315 315 elif styles.dark_style(style):
316 316 colors='linux'
317 317 else:
318 318 colors='lightbg'
319 319 else:
320 320 colors=None
321 321
322 322 # Configure the style
323 323 if style:
324 324 widget.style_sheet = styles.sheet_from_template(style, colors)
325 325 widget.syntax_style = style
326 326 widget._syntax_style_changed()
327 327 widget._style_sheet_changed()
328 328 elif colors:
329 329 # use a default dark/light/bw style
330 330 widget.set_default_style(colors=colors)
331 331
332 332 if self.stylesheet:
333 333 # we got an explicit stylesheet
334 334 if os.path.isfile(self.stylesheet):
335 335 with open(self.stylesheet) as f:
336 336 sheet = f.read()
337 337 else:
338 338 raise IOError("Stylesheet %r not found." % self.stylesheet)
339 339 if sheet:
340 340 widget.style_sheet = sheet
341 341 widget._style_sheet_changed()
342 342
343 343
344 344 def init_signal(self):
345 345 """allow clean shutdown on sigint"""
346 346 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
347 347 # need a timer, so that QApplication doesn't block until a real
348 348 # Qt event fires (can require mouse movement)
349 349 # timer trick from http://stackoverflow.com/q/4938723/938949
350 350 timer = QtCore.QTimer()
351 351 # Let the interpreter run each 200 ms:
352 352 timer.timeout.connect(lambda: None)
353 353 timer.start(200)
354 354 # hold onto ref, so the timer doesn't get cleaned up
355 355 self._sigint_timer = timer
356 356
357 357 @catch_config_error
358 358 def initialize(self, argv=None):
359 359 self.init_qt_app()
360 360 super(IPythonQtConsoleApp, self).initialize(argv)
361 361 IPythonConsoleApp.initialize(self,argv)
362 362 self.init_qt_elements()
363 363 self.init_signal()
364 364
365 365 def start(self):
366 366
367 367 # draw the window
368 368 if self.maximize:
369 369 self.window.showMaximized()
370 370 else:
371 371 self.window.show()
372 372 self.window.raise_()
373 373
374 374 # Start the application main loop.
375 375 self.app.exec_()
376 376
377 377 #-----------------------------------------------------------------------------
378 378 # Main entry point
379 379 #-----------------------------------------------------------------------------
380 380
381 381 def main():
382 382 app = IPythonQtConsoleApp()
383 383 app.initialize()
384 384 app.start()
385 385
386 386
387 387 if __name__ == '__main__':
388 388 main()
@@ -1,325 +1,325 b''
1 1 #-----------------------------------------------------------------------------
2 2 # Copyright (c) 2010, IPython Development Team.
3 3 #
4 4 # Distributed under the terms of the Modified BSD License.
5 5 #
6 6 # The full license is in the file COPYING.txt, distributed with this software.
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard libary imports.
10 10 from base64 import decodestring
11 11 import os
12 12 import re
13 13
14 14 # System libary imports.
15 15 from IPython.external.qt import QtCore, QtGui
16 16
17 17 # Local imports
18 18 from IPython.utils.traitlets import Bool
19 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 20 from ipython_widget import IPythonWidget
21 21
22 22
23 23 class RichIPythonWidget(IPythonWidget):
24 24 """ An IPythonWidget that supports rich text, including lists, images, and
25 25 tables. Note that raw performance will be reduced compared to the plain
26 26 text version.
27 27 """
28 28
29 29 # RichIPythonWidget protected class variables.
30 30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 31 _jpg_supported = Bool(False)
32 32
33 33 # Used to determine whether a given html export attempt has already
34 34 # displayed a warning about being unable to convert a png to svg.
35 35 _svg_warning_displayed = False
36 36
37 37 #---------------------------------------------------------------------------
38 38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 41 def __init__(self, *args, **kw):
42 42 """ Create a RichIPythonWidget.
43 43 """
44 44 kw['kind'] = 'rich'
45 45 super(RichIPythonWidget, self).__init__(*args, **kw)
46 46
47 47 # Configure the ConsoleWidget HTML exporter for our formats.
48 48 self._html_exporter.image_tag = self._get_image_tag
49 49
50 50 # Dictionary for resolving document resource names to SVG data.
51 51 self._name_to_svg_map = {}
52 52
53 53 # Do we support jpg ?
54 54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 55 # it is not always supported.
56 56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 57 self._jpg_supported = 'jpeg' in _supported_format
58 58
59 59
60 60 #---------------------------------------------------------------------------
61 61 # 'ConsoleWidget' public interface overides
62 62 #---------------------------------------------------------------------------
63 63
64 64 def export_html(self):
65 65 """ Shows a dialog to export HTML/XML in various formats.
66 66
67 67 Overridden in order to reset the _svg_warning_displayed flag prior
68 68 to the export running.
69 69 """
70 70 self._svg_warning_displayed = False
71 71 super(RichIPythonWidget, self).export_html()
72 72
73 73
74 74 #---------------------------------------------------------------------------
75 75 # 'ConsoleWidget' protected interface
76 76 #---------------------------------------------------------------------------
77 77
78 78 def _context_menu_make(self, pos):
79 79 """ Reimplemented to return a custom context menu for images.
80 80 """
81 81 format = self._control.cursorForPosition(pos).charFormat()
82 82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 83 if name:
84 84 menu = QtGui.QMenu()
85 85
86 86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 88 menu.addSeparator()
89 89
90 90 svg = self._name_to_svg_map.get(name, None)
91 91 if svg is not None:
92 92 menu.addSeparator()
93 93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 94 menu.addAction('Save SVG As...',
95 95 lambda: save_svg(svg, self._control))
96 96 else:
97 97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 98 return menu
99 99
100 100 #---------------------------------------------------------------------------
101 101 # 'BaseFrontendMixin' abstract interface
102 102 #---------------------------------------------------------------------------
103 103 def _pre_image_append(self, msg, prompt_number):
104 104 """ Append the Out[] prompt and make the output nicer
105 105
106 106 Shared code for some the following if statement
107 107 """
108 108 self.log.debug("pyout: %s", msg.get('content', ''))
109 109 self._append_plain_text(self.output_sep, True)
110 110 self._append_html(self._make_out_prompt(prompt_number), True)
111 111 self._append_plain_text('\n', True)
112 112
113 113 def _handle_pyout(self, msg):
114 114 """ Overridden to handle rich data types, like SVG.
115 115 """
116 116 if not self._hidden and self._is_from_this_session(msg):
117 117 content = msg['content']
118 118 prompt_number = content.get('execution_count', 0)
119 119 data = content['data']
120 120 if 'image/svg+xml' in data:
121 121 self._pre_image_append(msg, prompt_number)
122 122 self._append_svg(data['image/svg+xml'], True)
123 123 self._append_html(self.output_sep2, True)
124 124 elif 'image/png' in data:
125 125 self._pre_image_append(msg, prompt_number)
126 126 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
127 127 self._append_html(self.output_sep2, True)
128 128 elif 'image/jpeg' in data and self._jpg_supported:
129 129 self._pre_image_append(msg, prompt_number)
130 130 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
131 131 self._append_html(self.output_sep2, True)
132 132 else:
133 133 # Default back to the plain text representation.
134 134 return super(RichIPythonWidget, self)._handle_pyout(msg)
135 135
136 136 def _handle_display_data(self, msg):
137 137 """ Overridden to handle rich data types, like SVG.
138 138 """
139 139 if not self._hidden and self._is_from_this_session(msg):
140 140 source = msg['content']['source']
141 141 data = msg['content']['data']
142 142 metadata = msg['content']['metadata']
143 143 # Try to use the svg or html representations.
144 144 # FIXME: Is this the right ordering of things to try?
145 145 if 'image/svg+xml' in data:
146 146 self.log.debug("display: %s", msg.get('content', ''))
147 147 svg = data['image/svg+xml']
148 148 self._append_svg(svg, True)
149 149 elif 'image/png' in data:
150 150 self.log.debug("display: %s", msg.get('content', ''))
151 151 # PNG data is base64 encoded as it passes over the network
152 152 # in a JSON structure so we decode it.
153 153 png = decodestring(data['image/png'].encode('ascii'))
154 154 self._append_png(png, True)
155 155 elif 'image/jpeg' in data and self._jpg_supported:
156 156 self.log.debug("display: %s", msg.get('content', ''))
157 157 jpg = decodestring(data['image/jpeg'].encode('ascii'))
158 158 self._append_jpg(jpg, True)
159 159 else:
160 160 # Default back to the plain text representation.
161 161 return super(RichIPythonWidget, self)._handle_display_data(msg)
162 162
163 163 #---------------------------------------------------------------------------
164 164 # 'RichIPythonWidget' protected interface
165 165 #---------------------------------------------------------------------------
166 166
167 167 def _append_jpg(self, jpg, before_prompt=False):
168 168 """ Append raw JPG data to the widget."""
169 169 self._append_custom(self._insert_jpg, jpg, before_prompt)
170 170
171 171 def _append_png(self, png, before_prompt=False):
172 172 """ Append raw PNG data to the widget.
173 173 """
174 174 self._append_custom(self._insert_png, png, before_prompt)
175 175
176 176 def _append_svg(self, svg, before_prompt=False):
177 177 """ Append raw SVG data to the widget.
178 178 """
179 179 self._append_custom(self._insert_svg, svg, before_prompt)
180 180
181 181 def _add_image(self, image):
182 182 """ Adds the specified QImage to the document and returns a
183 183 QTextImageFormat that references it.
184 184 """
185 185 document = self._control.document()
186 186 name = str(image.cacheKey())
187 187 document.addResource(QtGui.QTextDocument.ImageResource,
188 188 QtCore.QUrl(name), image)
189 189 format = QtGui.QTextImageFormat()
190 190 format.setName(name)
191 191 return format
192 192
193 193 def _copy_image(self, name):
194 194 """ Copies the ImageResource with 'name' to the clipboard.
195 195 """
196 196 image = self._get_image(name)
197 197 QtGui.QApplication.clipboard().setImage(image)
198 198
199 199 def _get_image(self, name):
200 200 """ Returns the QImage stored as the ImageResource with 'name'.
201 201 """
202 202 document = self._control.document()
203 203 image = document.resource(QtGui.QTextDocument.ImageResource,
204 204 QtCore.QUrl(name))
205 205 return image
206 206
207 207 def _get_image_tag(self, match, path = None, format = "png"):
208 208 """ Return (X)HTML mark-up for the image-tag given by match.
209 209
210 210 Parameters
211 211 ----------
212 212 match : re.SRE_Match
213 213 A match to an HTML image tag as exported by Qt, with
214 214 match.group("Name") containing the matched image ID.
215 215
216 216 path : string|None, optional [default None]
217 217 If not None, specifies a path to which supporting files may be
218 218 written (e.g., for linked images). If None, all images are to be
219 219 included inline.
220 220
221 221 format : "png"|"svg"|"jpg", optional [default "png"]
222 222 Format for returned or referenced images.
223 223 """
224 224 if format in ("png","jpg"):
225 225 try:
226 226 image = self._get_image(match.group("name"))
227 227 except KeyError:
228 228 return "<b>Couldn't find image %s</b>" % match.group("name")
229 229
230 230 if path is not None:
231 231 if not os.path.exists(path):
232 232 os.mkdir(path)
233 233 relpath = os.path.basename(path)
234 234 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
235 235 "PNG"):
236 236 return '<img src="%s/qt_img%s.%s">' % (relpath,
237 237 match.group("name"),format)
238 238 else:
239 239 return "<b>Couldn't save image!</b>"
240 240 else:
241 241 ba = QtCore.QByteArray()
242 242 buffer_ = QtCore.QBuffer(ba)
243 243 buffer_.open(QtCore.QIODevice.WriteOnly)
244 244 image.save(buffer_, format.upper())
245 245 buffer_.close()
246 246 return '<img src="data:image/%s;base64,\n%s\n" />' % (
247 247 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
248 248
249 249 elif format == "svg":
250 250 try:
251 251 svg = str(self._name_to_svg_map[match.group("name")])
252 252 except KeyError:
253 253 if not self._svg_warning_displayed:
254 254 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
255 255 'Cannot convert a PNG to SVG. To fix this, add this '
256 256 'to your ipython config:\n\n'
257 257 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
258 258 'And regenerate the figures.',
259 259 QtGui.QMessageBox.Ok)
260 260 self._svg_warning_displayed = True
261 261 return ("<b>Cannot convert a PNG to SVG.</b> "
262 262 "To fix this, add this to your config: "
263 263 "<span>c.InlineBackendConfig.figure_format = 'svg'</span> "
264 264 "and regenerate the figures.")
265 265
266 266 # Not currently checking path, because it's tricky to find a
267 267 # cross-browser way to embed external SVG images (e.g., via
268 268 # object or embed tags).
269 269
270 270 # Chop stand-alone header from matplotlib SVG
271 271 offset = svg.find("<svg")
272 272 assert(offset > -1)
273 273
274 274 return svg[offset:]
275 275
276 276 else:
277 277 return '<b>Unrecognized image format</b>'
278 278
279 279 def _insert_jpg(self, cursor, jpg):
280 280 """ Insert raw PNG data into the widget."""
281 281 self._insert_img(cursor, jpg, 'jpg')
282 282
283 283 def _insert_png(self, cursor, png):
284 284 """ Insert raw PNG data into the widget.
285 285 """
286 286 self._insert_img(cursor, png, 'png')
287 287
288 288 def _insert_img(self, cursor, img, fmt):
289 289 """ insert a raw image, jpg or png """
290 290 try:
291 291 image = QtGui.QImage()
292 292 image.loadFromData(img, fmt.upper())
293 293 except ValueError:
294 294 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
295 295 else:
296 296 format = self._add_image(image)
297 297 cursor.insertBlock()
298 298 cursor.insertImage(format)
299 299 cursor.insertBlock()
300 300
301 301 def _insert_svg(self, cursor, svg):
302 302 """ Insert raw SVG data into the widet.
303 303 """
304 304 try:
305 305 image = svg_to_image(svg)
306 306 except ValueError:
307 307 self._insert_plain_text(cursor, 'Received invalid SVG data.')
308 308 else:
309 309 format = self._add_image(image)
310 310 self._name_to_svg_map[format.name()] = svg
311 311 cursor.insertBlock()
312 312 cursor.insertImage(format)
313 313 cursor.insertBlock()
314 314
315 315 def _save_image(self, name, format='PNG'):
316 316 """ Shows a save dialog for the ImageResource with 'name'.
317 317 """
318 318 dialog = QtGui.QFileDialog(self._control, 'Save Image')
319 319 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
320 320 dialog.setDefaultSuffix(format.lower())
321 321 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
322 322 if dialog.exec_():
323 323 filename = dialog.selectedFiles()[0]
324 324 image = self._get_image(name)
325 325 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now