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