##// END OF EJS Templates
* Fixed regression from last commit: syntax highlighting works robustly again....
epatters -
Show More
@@ -1,1338 +1,1353 b''
1 1 # Standard library imports
2 2 import re
3 3 import sys
4 4 from textwrap import dedent
5 5
6 6 # System library imports
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from ansi_code_processor import QtAnsiCodeProcessor
11 11 from completion_widget import CompletionWidget
12 12
13 13
14 14 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
15 15 """ A QPlainTextEdit suitable for use with ConsoleWidget.
16 16 """
17 17 # Prevents text from being moved by drag and drop. Note that is not, for
18 18 # some reason, sufficient to catch drag events in the ConsoleWidget's
19 19 # event filter.
20 20 def dragEnterEvent(self, event): pass
21 21 def dragLeaveEvent(self, event): pass
22 22 def dragMoveEvent(self, event): pass
23 23 def dropEvent(self, event): pass
24 24
25 25 class ConsoleTextEdit(QtGui.QTextEdit):
26 26 """ A QTextEdit suitable for use with ConsoleWidget.
27 27 """
28 28 # See above.
29 29 def dragEnterEvent(self, event): pass
30 30 def dragLeaveEvent(self, event): pass
31 31 def dragMoveEvent(self, event): pass
32 32 def dropEvent(self, event): pass
33 33
34 34
35 35 class ConsoleWidget(QtGui.QWidget):
36 36 """ An abstract base class for console-type widgets. This class has
37 37 functionality for:
38 38
39 39 * Maintaining a prompt and editing region
40 40 * Providing the traditional Unix-style console keyboard shortcuts
41 41 * Performing tab completion
42 42 * Paging text
43 43 * Handling ANSI escape codes
44 44
45 45 ConsoleWidget also provides a number of utility methods that will be
46 46 convenient to implementors of a console-style widget.
47 47 """
48 48
49 49 # Whether to process ANSI escape codes.
50 50 ansi_codes = True
51 51
52 52 # The maximum number of lines of text before truncation.
53 53 buffer_size = 500
54 54
55 55 # Whether to use a list widget or plain text output for tab completion.
56 56 gui_completion = True
57 57
58 58 # Whether to override ShortcutEvents for the keybindings defined by this
59 59 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
60 60 # priority (when it has focus) over, e.g., window-level menu shortcuts.
61 61 override_shortcuts = False
62 62
63 63 # Signals that indicate ConsoleWidget state.
64 64 copy_available = QtCore.pyqtSignal(bool)
65 65 redo_available = QtCore.pyqtSignal(bool)
66 66 undo_available = QtCore.pyqtSignal(bool)
67 67
68 68 # Signal emitted when paging is needed and the paging style has been
69 69 # specified as 'custom'.
70 70 custom_page_requested = QtCore.pyqtSignal(object)
71 71
72 72 # Protected class variables.
73 73 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
74 74 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
75 75 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
76 76 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
77 77 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
78 78 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
79 79 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
80 80 _shortcuts = set(_ctrl_down_remap.keys() +
81 81 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
82 82
83 83 #---------------------------------------------------------------------------
84 84 # 'QObject' interface
85 85 #---------------------------------------------------------------------------
86 86
87 87 def __init__(self, kind='plain', paging='inside', parent=None):
88 88 """ Create a ConsoleWidget.
89 89
90 90 Parameters
91 91 ----------
92 92 kind : str, optional [default 'plain']
93 93 The type of underlying text widget to use. Valid values are 'plain',
94 94 which specifies a QPlainTextEdit, and 'rich', which specifies a
95 95 QTextEdit.
96 96
97 97 paging : str, optional [default 'inside']
98 98 The type of paging to use. Valid values are:
99 99 'inside' : The widget pages like a traditional terminal pager.
100 100 'hsplit' : When paging is requested, the widget is split
101 101 horizontally. The top pane contains the console,
102 102 and the bottom pane contains the paged text.
103 103 'vsplit' : Similar to 'hsplit', except that a vertical splitter
104 104 used.
105 105 'custom' : No action is taken by the widget beyond emitting a
106 106 'custom_page_requested(str)' signal.
107 107 'none' : The text is written directly to the console.
108 108
109 109 parent : QWidget, optional [default None]
110 110 The parent for this widget.
111 111 """
112 112 super(ConsoleWidget, self).__init__(parent)
113 113
114 114 # Create the layout and underlying text widget.
115 115 layout = QtGui.QStackedLayout(self)
116 116 layout.setMargin(0)
117 117 self._control = self._create_control(kind)
118 118 self._page_control = None
119 119 self._splitter = None
120 120 if paging in ('hsplit', 'vsplit'):
121 121 self._splitter = QtGui.QSplitter()
122 122 if paging == 'hsplit':
123 123 self._splitter.setOrientation(QtCore.Qt.Horizontal)
124 124 else:
125 125 self._splitter.setOrientation(QtCore.Qt.Vertical)
126 126 self._splitter.addWidget(self._control)
127 127 layout.addWidget(self._splitter)
128 128 else:
129 129 layout.addWidget(self._control)
130 130
131 131 # Create the paging widget, if necessary.
132 132 self._page_style = paging
133 133 if paging in ('inside', 'hsplit', 'vsplit'):
134 134 self._page_control = self._create_page_control()
135 135 if self._splitter:
136 136 self._page_control.hide()
137 137 self._splitter.addWidget(self._page_control)
138 138 else:
139 139 layout.addWidget(self._page_control)
140 140 elif paging not in ('custom', 'none'):
141 141 raise ValueError('Paging style %s unknown.' % repr(paging))
142 142
143 143 # Initialize protected variables. Some variables contain useful state
144 144 # information for subclasses; they should be considered read-only.
145 145 self._ansi_processor = QtAnsiCodeProcessor()
146 146 self._completion_widget = CompletionWidget(self._control)
147 147 self._continuation_prompt = '> '
148 148 self._continuation_prompt_html = None
149 149 self._executing = False
150 150 self._prompt = ''
151 151 self._prompt_html = None
152 152 self._prompt_pos = 0
153 self._prompt_sep = ''
153 154 self._reading = False
154 155 self._reading_callback = None
155 156 self._tab_width = 8
156 157
157 158 # Set a monospaced font.
158 159 self.reset_font()
159 160
160 161 def eventFilter(self, obj, event):
161 162 """ Reimplemented to ensure a console-like behavior in the underlying
162 163 text widget.
163 164 """
164 165 # Re-map keys for all filtered widgets.
165 166 etype = event.type()
166 167 if etype == QtCore.QEvent.KeyPress and \
167 168 self._control_key_down(event.modifiers()) and \
168 169 event.key() in self._ctrl_down_remap:
169 170 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
170 171 self._ctrl_down_remap[event.key()],
171 172 QtCore.Qt.NoModifier)
172 173 QtGui.qApp.sendEvent(obj, new_event)
173 174 return True
174 175
175 176 # Override shortucts for all filtered widgets. Note that on Mac OS it is
176 177 # always unnecessary to override shortcuts, hence the check below (users
177 178 # should just use the Control key instead of the Command key).
178 179 elif etype == QtCore.QEvent.ShortcutOverride and \
179 180 sys.platform != 'darwin' and \
180 181 self._control_key_down(event.modifiers()) and \
181 182 event.key() in self._shortcuts:
182 183 event.accept()
183 184 return False
184 185
185 186 elif etype == QtCore.QEvent.KeyPress:
186 187 if obj == self._control:
187 188 return self._event_filter_console_keypress(event)
188 189 elif obj == self._page_control:
189 190 return self._event_filter_page_keypress(event)
190 191
191 192 return super(ConsoleWidget, self).eventFilter(obj, event)
192 193
193 194 #---------------------------------------------------------------------------
194 195 # 'QWidget' interface
195 196 #---------------------------------------------------------------------------
196 197
197 198 def sizeHint(self):
198 199 """ Reimplemented to suggest a size that is 80 characters wide and
199 200 25 lines high.
200 201 """
201 202 style = self.style()
202 203 opt = QtGui.QStyleOptionHeader()
203 204 font_metrics = QtGui.QFontMetrics(self.font)
204 205 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
205 206
206 207 width = font_metrics.width(' ') * 80
207 208 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
208 209 if self._page_style == 'hsplit':
209 210 width = width * 2 + splitwidth
210 211
211 212 height = font_metrics.height() * 25
212 213 if self._page_style == 'vsplit':
213 214 height = height * 2 + splitwidth
214 215
215 216 return QtCore.QSize(width, height)
216 217
217 218 #---------------------------------------------------------------------------
218 219 # 'ConsoleWidget' public interface
219 220 #---------------------------------------------------------------------------
220 221
221 222 def can_paste(self):
222 223 """ Returns whether text can be pasted from the clipboard.
223 224 """
224 225 # Accept only text that can be ASCII encoded.
225 226 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
226 227 text = QtGui.QApplication.clipboard().text()
227 228 if not text.isEmpty():
228 229 try:
229 230 str(text)
230 231 return True
231 232 except UnicodeEncodeError:
232 233 pass
233 234 return False
234 235
235 236 def clear(self, keep_input=True):
236 237 """ Clear the console, then write a new prompt. If 'keep_input' is set,
237 238 restores the old input buffer when the new prompt is written.
238 239 """
239 240 if keep_input:
240 241 input_buffer = self.input_buffer
241 242 self._control.clear()
242 243 self._show_prompt()
243 244 if keep_input:
244 245 self.input_buffer = input_buffer
245 246
246 247 def copy(self):
247 248 """ Copy the current selected text to the clipboard.
248 249 """
249 250 self._control.copy()
250 251
251 252 def execute(self, source=None, hidden=False, interactive=False):
252 253 """ Executes source or the input buffer, possibly prompting for more
253 254 input.
254 255
255 256 Parameters:
256 257 -----------
257 258 source : str, optional
258 259
259 260 The source to execute. If not specified, the input buffer will be
260 261 used. If specified and 'hidden' is False, the input buffer will be
261 262 replaced with the source before execution.
262 263
263 264 hidden : bool, optional (default False)
264 265
265 266 If set, no output will be shown and the prompt will not be modified.
266 267 In other words, it will be completely invisible to the user that
267 268 an execution has occurred.
268 269
269 270 interactive : bool, optional (default False)
270 271
271 272 Whether the console is to treat the source as having been manually
272 273 entered by the user. The effect of this parameter depends on the
273 274 subclass implementation.
274 275
275 276 Raises:
276 277 -------
277 278 RuntimeError
278 279 If incomplete input is given and 'hidden' is True. In this case,
279 280 it is not possible to prompt for more input.
280 281
281 282 Returns:
282 283 --------
283 284 A boolean indicating whether the source was executed.
284 285 """
285 if not hidden:
286 # Do everything here inside an edit block so continuation prompts
287 # are removed seamlessly via undo/redo.
288 cursor = self._control.textCursor()
289 cursor.beginEditBlock()
286 # WARNING: The order in which things happen here is very particular, in
287 # large part because our syntax highlighting is fragile. If you change
288 # something, test carefully!
290 289
291 if source is not None:
290 # Decide what to execute.
291 if source is None:
292 source = self.input_buffer
293 if not hidden:
294 # A newline is appended later, but it should be considered part
295 # of the input buffer.
296 source += '\n'
297 elif not hidden:
292 298 self.input_buffer = source
293 299
300 # Execute the source or show a continuation prompt if it is incomplete.
301 complete = self._is_complete(source, interactive)
302 if hidden:
303 if complete:
304 self._execute(source, hidden)
305 else:
306 error = 'Incomplete noninteractive input: "%s"'
307 raise RuntimeError(error % source)
308 else:
309 if complete:
294 310 self._append_plain_text('\n')
295 311 self._executing_input_buffer = self.input_buffer
296 312 self._executing = True
297 313 self._prompt_finished()
298 314
299 real_source = self.input_buffer if source is None else source
300 complete = self._is_complete(real_source, interactive)
301 if complete:
302 if not hidden:
303 315 # The maximum block count is only in effect during execution.
304 316 # This ensures that _prompt_pos does not become invalid due to
305 317 # text truncation.
306 318 self._control.document().setMaximumBlockCount(self.buffer_size)
307 319
308 320 # Setting a positive maximum block count will automatically
309 321 # disable the undo/redo history, but just to be safe:
310 322 self._control.setUndoRedoEnabled(False)
311 323
312 self._execute(real_source, hidden)
313 elif hidden:
314 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
324 self._execute(source, hidden)
325
315 326 else:
327 # Do this inside an edit block so continuation prompts are
328 # removed seamlessly via undo/redo.
329 cursor = self._control.textCursor()
330 cursor.beginEditBlock()
331
332 self._append_plain_text('\n')
316 333 self._show_continuation_prompt()
317 334
318 if not hidden:
319 335 cursor.endEditBlock()
320 336
321 337 return complete
322 338
323 339 def _get_input_buffer(self):
324 340 """ The text that the user has entered entered at the current prompt.
325 341 """
326 342 # If we're executing, the input buffer may not even exist anymore due to
327 343 # the limit imposed by 'buffer_size'. Therefore, we store it.
328 344 if self._executing:
329 345 return self._executing_input_buffer
330 346
331 347 cursor = self._get_end_cursor()
332 348 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
333 349 input_buffer = str(cursor.selection().toPlainText())
334 350
335 351 # Strip out continuation prompts.
336 352 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
337 353
338 354 def _set_input_buffer(self, string):
339 355 """ Replaces the text in the input buffer with 'string'.
340 356 """
341 357 # For now, it is an error to modify the input buffer during execution.
342 358 if self._executing:
343 359 raise RuntimeError("Cannot change input buffer during execution.")
344 360
345 361 # Remove old text.
346 362 cursor = self._get_end_cursor()
347 363 cursor.beginEditBlock()
348 364 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
349 365 cursor.removeSelectedText()
350 366
351 367 # Insert new text with continuation prompts.
352 368 lines = string.splitlines(True)
353 369 if lines:
354 370 self._append_plain_text(lines[0])
355 371 for i in xrange(1, len(lines)):
356 372 if self._continuation_prompt_html is None:
357 373 self._append_plain_text(self._continuation_prompt)
358 374 else:
359 375 self._append_html(self._continuation_prompt_html)
360 376 self._append_plain_text(lines[i])
361 377 cursor.endEditBlock()
362 378 self._control.moveCursor(QtGui.QTextCursor.End)
363 379
364 380 input_buffer = property(_get_input_buffer, _set_input_buffer)
365 381
366 382 def _get_font(self):
367 383 """ The base font being used by the ConsoleWidget.
368 384 """
369 385 return self._control.document().defaultFont()
370 386
371 387 def _set_font(self, font):
372 388 """ Sets the base font for the ConsoleWidget to the specified QFont.
373 389 """
374 390 font_metrics = QtGui.QFontMetrics(font)
375 391 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
376 392
377 393 self._completion_widget.setFont(font)
378 394 self._control.document().setDefaultFont(font)
379 395 if self._page_control:
380 396 self._page_control.document().setDefaultFont(font)
381 397
382 398 font = property(_get_font, _set_font)
383 399
384 400 def paste(self):
385 401 """ Paste the contents of the clipboard into the input region.
386 402 """
387 403 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
388 404 try:
389 405 text = str(QtGui.QApplication.clipboard().text())
390 406 except UnicodeEncodeError:
391 407 pass
392 408 else:
393 409 self._insert_plain_text_into_buffer(dedent(text))
394 410
395 411 def print_(self, printer):
396 412 """ Print the contents of the ConsoleWidget to the specified QPrinter.
397 413 """
398 414 self._control.print_(printer)
399 415
400 416 def redo(self):
401 417 """ Redo the last operation. If there is no operation to redo, nothing
402 418 happens.
403 419 """
404 420 self._control.redo()
405 421
406 422 def reset_font(self):
407 423 """ Sets the font to the default fixed-width font for this platform.
408 424 """
409 425 # FIXME: font family and size should be configurable by the user.
410 426
411 427 if sys.platform == 'win32':
412 428 # Fixme: we should test whether Consolas is available and use it
413 429 # first if it is. Consolas ships by default from Vista onwards,
414 430 # it's *vastly* more readable and prettier than Courier, and is
415 431 # often installed even on XP systems. So we should first check for
416 432 # it, and only fallback to Courier if absolutely necessary.
417 433 name = 'Courier'
418 434 elif sys.platform == 'darwin':
419 435 name = 'Monaco'
420 436 else:
421 437 name = 'Monospace'
422 438 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
423 439 font.setStyleHint(QtGui.QFont.TypeWriter)
424 440 self._set_font(font)
425 441
426 442 def select_all(self):
427 443 """ Selects all the text in the buffer.
428 444 """
429 445 self._control.selectAll()
430 446
431 447 def _get_tab_width(self):
432 448 """ The width (in terms of space characters) for tab characters.
433 449 """
434 450 return self._tab_width
435 451
436 452 def _set_tab_width(self, tab_width):
437 453 """ Sets the width (in terms of space characters) for tab characters.
438 454 """
439 455 font_metrics = QtGui.QFontMetrics(self.font)
440 456 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
441 457
442 458 self._tab_width = tab_width
443 459
444 460 tab_width = property(_get_tab_width, _set_tab_width)
445 461
446 462 def undo(self):
447 463 """ Undo the last operation. If there is no operation to undo, nothing
448 464 happens.
449 465 """
450 466 self._control.undo()
451 467
452 468 #---------------------------------------------------------------------------
453 469 # 'ConsoleWidget' abstract interface
454 470 #---------------------------------------------------------------------------
455 471
456 472 def _is_complete(self, source, interactive):
457 473 """ Returns whether 'source' can be executed. When triggered by an
458 474 Enter/Return key press, 'interactive' is True; otherwise, it is
459 475 False.
460 476 """
461 477 raise NotImplementedError
462 478
463 479 def _execute(self, source, hidden):
464 480 """ Execute 'source'. If 'hidden', do not show any output.
465 481 """
466 482 raise NotImplementedError
467 483
468 484 def _prompt_started_hook(self):
469 485 """ Called immediately after a new prompt is displayed.
470 486 """
471 487 pass
472 488
473 489 def _prompt_finished_hook(self):
474 490 """ Called immediately after a prompt is finished, i.e. when some input
475 491 will be processed and a new prompt displayed.
476 492 """
477 493 pass
478 494
479 495 def _up_pressed(self):
480 496 """ Called when the up key is pressed. Returns whether to continue
481 497 processing the event.
482 498 """
483 499 return True
484 500
485 501 def _down_pressed(self):
486 502 """ Called when the down key is pressed. Returns whether to continue
487 503 processing the event.
488 504 """
489 505 return True
490 506
491 507 def _tab_pressed(self):
492 508 """ Called when the tab key is pressed. Returns whether to continue
493 509 processing the event.
494 510 """
495 511 return False
496 512
497 513 #--------------------------------------------------------------------------
498 514 # 'ConsoleWidget' protected interface
499 515 #--------------------------------------------------------------------------
500 516
501 517 def _append_html(self, html):
502 518 """ Appends html at the end of the console buffer.
503 519 """
504 520 cursor = self._get_end_cursor()
505 521 self._insert_html(cursor, html)
506 522
507 523 def _append_html_fetching_plain_text(self, html):
508 524 """ Appends 'html', then returns the plain text version of it.
509 525 """
510 526 cursor = self._get_end_cursor()
511 527 return self._insert_html_fetching_plain_text(cursor, html)
512 528
513 529 def _append_plain_text(self, text):
514 530 """ Appends plain text at the end of the console buffer, processing
515 531 ANSI codes if enabled.
516 532 """
517 533 cursor = self._get_end_cursor()
518 534 self._insert_plain_text(cursor, text)
519 535
520 536 def _append_plain_text_keeping_prompt(self, text):
521 537 """ Writes 'text' after the current prompt, then restores the old prompt
522 538 with its old input buffer.
523 539 """
524 540 input_buffer = self.input_buffer
525 541 self._append_plain_text('\n')
526 542 self._prompt_finished()
527 543
528 544 self._append_plain_text(text)
529 545 self._show_prompt()
530 546 self.input_buffer = input_buffer
531 547
532 548 def _complete_with_items(self, cursor, items):
533 549 """ Performs completion with 'items' at the specified cursor location.
534 550 """
535 551 if len(items) == 1:
536 552 cursor.setPosition(self._control.textCursor().position(),
537 553 QtGui.QTextCursor.KeepAnchor)
538 554 cursor.insertText(items[0])
539 555 elif len(items) > 1:
540 556 if self.gui_completion:
541 557 self._completion_widget.show_items(cursor, items)
542 558 else:
543 559 text = self._format_as_columns(items)
544 560 self._append_plain_text_keeping_prompt(text)
545 561
546 562 def _control_key_down(self, modifiers):
547 563 """ Given a KeyboardModifiers flags object, return whether the Control
548 564 key is down (on Mac OS, treat the Command key as a synonym for
549 565 Control).
550 566 """
551 567 down = bool(modifiers & QtCore.Qt.ControlModifier)
552 568
553 569 # Note: on Mac OS, ControlModifier corresponds to the Command key while
554 570 # MetaModifier corresponds to the Control key.
555 571 if sys.platform == 'darwin':
556 572 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
557 573
558 574 return down
559 575
560 576 def _create_control(self, kind):
561 577 """ Creates and connects the underlying text widget.
562 578 """
563 579 if kind == 'plain':
564 580 control = ConsolePlainTextEdit()
565 581 elif kind == 'rich':
566 582 control = ConsoleTextEdit()
567 583 control.setAcceptRichText(False)
568 584 else:
569 585 raise ValueError("Kind %s unknown." % repr(kind))
570 586 control.installEventFilter(self)
571 587 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
572 588 control.customContextMenuRequested.connect(self._show_context_menu)
573 589 control.copyAvailable.connect(self.copy_available)
574 590 control.redoAvailable.connect(self.redo_available)
575 591 control.undoAvailable.connect(self.undo_available)
576 592 control.setReadOnly(True)
577 593 control.setUndoRedoEnabled(False)
578 594 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
579 595 return control
580 596
581 597 def _create_page_control(self):
582 598 """ Creates and connects the underlying paging widget.
583 599 """
584 600 control = ConsolePlainTextEdit()
585 601 control.installEventFilter(self)
586 602 control.setReadOnly(True)
587 603 control.setUndoRedoEnabled(False)
588 604 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
589 605 return control
590 606
591 607 def _event_filter_console_keypress(self, event):
592 608 """ Filter key events for the underlying text widget to create a
593 609 console-like interface.
594 610 """
595 611 intercepted = False
596 612 cursor = self._control.textCursor()
597 613 position = cursor.position()
598 614 key = event.key()
599 615 ctrl_down = self._control_key_down(event.modifiers())
600 616 alt_down = event.modifiers() & QtCore.Qt.AltModifier
601 617 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
602 618
603 619 if event.matches(QtGui.QKeySequence.Paste):
604 620 # Call our paste instead of the underlying text widget's.
605 621 self.paste()
606 622 intercepted = True
607 623
608 624 elif ctrl_down:
609 625 if key == QtCore.Qt.Key_K:
610 626 if self._in_buffer(position):
611 627 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
612 628 QtGui.QTextCursor.KeepAnchor)
613 629 cursor.removeSelectedText()
614 630 intercepted = True
615 631
616 632 elif key == QtCore.Qt.Key_X:
617 633 intercepted = True
618 634
619 635 elif key == QtCore.Qt.Key_Y:
620 636 self.paste()
621 637 intercepted = True
622 638
623 639 elif alt_down:
624 640 if key == QtCore.Qt.Key_B:
625 641 self._set_cursor(self._get_word_start_cursor(position))
626 642 intercepted = True
627 643
628 644 elif key == QtCore.Qt.Key_F:
629 645 self._set_cursor(self._get_word_end_cursor(position))
630 646 intercepted = True
631 647
632 648 elif key == QtCore.Qt.Key_Backspace:
633 649 cursor = self._get_word_start_cursor(position)
634 650 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
635 651 cursor.removeSelectedText()
636 652 intercepted = True
637 653
638 654 elif key == QtCore.Qt.Key_D:
639 655 cursor = self._get_word_end_cursor(position)
640 656 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
641 657 cursor.removeSelectedText()
642 658 intercepted = True
643 659
644 660 else:
645 661 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
646 662 if self._reading:
647 663 self._append_plain_text('\n')
648 664 self._reading = False
649 665 if self._reading_callback:
650 666 self._reading_callback()
651 667 elif not self._executing:
652 668 self.execute(interactive=True)
653 669 intercepted = True
654 670
655 671 elif key == QtCore.Qt.Key_Up:
656 672 if self._reading or not self._up_pressed():
657 673 intercepted = True
658 674 else:
659 675 prompt_line = self._get_prompt_cursor().blockNumber()
660 676 intercepted = cursor.blockNumber() <= prompt_line
661 677
662 678 elif key == QtCore.Qt.Key_Down:
663 679 if self._reading or not self._down_pressed():
664 680 intercepted = True
665 681 else:
666 682 end_line = self._get_end_cursor().blockNumber()
667 683 intercepted = cursor.blockNumber() == end_line
668 684
669 685 elif key == QtCore.Qt.Key_Tab:
670 686 if not self._reading:
671 687 intercepted = not self._tab_pressed()
672 688
673 689 elif key == QtCore.Qt.Key_Left:
674 690 intercepted = not self._in_buffer(position - 1)
675 691
676 692 elif key == QtCore.Qt.Key_Home:
677 693 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
678 694 start_line = cursor.blockNumber()
679 695 if start_line == self._get_prompt_cursor().blockNumber():
680 696 start_pos = self._prompt_pos
681 697 else:
682 698 start_pos = cursor.position()
683 699 start_pos += len(self._continuation_prompt)
684 700 if shift_down and self._in_buffer(position):
685 701 self._set_selection(position, start_pos)
686 702 else:
687 703 self._set_position(start_pos)
688 704 intercepted = True
689 705
690 706 elif key == QtCore.Qt.Key_Backspace:
691 707
692 708 # Line deletion (remove continuation prompt)
693 709 len_prompt = len(self._continuation_prompt)
694 710 if not self._reading and \
695 711 cursor.columnNumber() == len_prompt and \
696 712 position != self._prompt_pos:
697 713 cursor.beginEditBlock()
698 714 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
699 715 QtGui.QTextCursor.KeepAnchor)
700 716 cursor.removeSelectedText()
701 717 cursor.deletePreviousChar()
702 718 cursor.endEditBlock()
703 719 intercepted = True
704 720
705 721 # Regular backwards deletion
706 722 else:
707 723 anchor = cursor.anchor()
708 724 if anchor == position:
709 725 intercepted = not self._in_buffer(position - 1)
710 726 else:
711 727 intercepted = not self._in_buffer(min(anchor, position))
712 728
713 729 elif key == QtCore.Qt.Key_Delete:
714 730
715 731 # Line deletion (remove continuation prompt)
716 732 if not self._reading and cursor.atBlockEnd() and not \
717 733 cursor.hasSelection():
718 734 cursor.movePosition(QtGui.QTextCursor.NextBlock,
719 735 QtGui.QTextCursor.KeepAnchor)
720 736 cursor.movePosition(QtGui.QTextCursor.Right,
721 737 QtGui.QTextCursor.KeepAnchor,
722 738 len(self._continuation_prompt))
723 739 cursor.removeSelectedText()
724 740 intercepted = True
725 741
726 742 # Regular forwards deletion:
727 743 else:
728 744 anchor = cursor.anchor()
729 745 intercepted = (not self._in_buffer(anchor) or
730 746 not self._in_buffer(position))
731 747
732 748 # Don't move the cursor if control is down to allow copy-paste using
733 749 # the keyboard in any part of the buffer.
734 750 if not ctrl_down:
735 751 self._keep_cursor_in_buffer()
736 752
737 753 return intercepted
738 754
739 755 def _event_filter_page_keypress(self, event):
740 756 """ Filter key events for the paging widget to create console-like
741 757 interface.
742 758 """
743 759 key = event.key()
744 760
745 761 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
746 762 if self._splitter:
747 763 self._page_control.hide()
748 764 else:
749 765 self.layout().setCurrentWidget(self._control)
750 766 return True
751 767
752 768 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
753 769 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
754 770 QtCore.Qt.Key_Down,
755 771 QtCore.Qt.NoModifier)
756 772 QtGui.qApp.sendEvent(self._page_control, new_event)
757 773 return True
758 774
759 775 return False
760 776
761 777 def _format_as_columns(self, items, separator=' '):
762 778 """ Transform a list of strings into a single string with columns.
763 779
764 780 Parameters
765 781 ----------
766 782 items : sequence of strings
767 783 The strings to process.
768 784
769 785 separator : str, optional [default is two spaces]
770 786 The string that separates columns.
771 787
772 788 Returns
773 789 -------
774 790 The formatted string.
775 791 """
776 792 # Note: this code is adapted from columnize 0.3.2.
777 793 # See http://code.google.com/p/pycolumnize/
778 794
779 795 width = self._control.viewport().width()
780 796 char_width = QtGui.QFontMetrics(self.font).width(' ')
781 797 displaywidth = max(5, width / char_width)
782 798
783 799 # Some degenerate cases.
784 800 size = len(items)
785 801 if size == 0:
786 802 return '\n'
787 803 elif size == 1:
788 804 return '%s\n' % str(items[0])
789 805
790 806 # Try every row count from 1 upwards
791 807 array_index = lambda nrows, row, col: nrows*col + row
792 808 for nrows in range(1, size):
793 809 ncols = (size + nrows - 1) // nrows
794 810 colwidths = []
795 811 totwidth = -len(separator)
796 812 for col in range(ncols):
797 813 # Get max column width for this column
798 814 colwidth = 0
799 815 for row in range(nrows):
800 816 i = array_index(nrows, row, col)
801 817 if i >= size: break
802 818 x = items[i]
803 819 colwidth = max(colwidth, len(x))
804 820 colwidths.append(colwidth)
805 821 totwidth += colwidth + len(separator)
806 822 if totwidth > displaywidth:
807 823 break
808 824 if totwidth <= displaywidth:
809 825 break
810 826
811 827 # The smallest number of rows computed and the max widths for each
812 828 # column has been obtained. Now we just have to format each of the rows.
813 829 string = ''
814 830 for row in range(nrows):
815 831 texts = []
816 832 for col in range(ncols):
817 833 i = row + nrows*col
818 834 if i >= size:
819 835 texts.append('')
820 836 else:
821 837 texts.append(items[i])
822 838 while texts and not texts[-1]:
823 839 del texts[-1]
824 840 for col in range(len(texts)):
825 841 texts[col] = texts[col].ljust(colwidths[col])
826 842 string += '%s\n' % str(separator.join(texts))
827 843 return string
828 844
829 845 def _get_block_plain_text(self, block):
830 846 """ Given a QTextBlock, return its unformatted text.
831 847 """
832 848 cursor = QtGui.QTextCursor(block)
833 849 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
834 850 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
835 851 QtGui.QTextCursor.KeepAnchor)
836 852 return str(cursor.selection().toPlainText())
837 853
838 854 def _get_cursor(self):
839 855 """ Convenience method that returns a cursor for the current position.
840 856 """
841 857 return self._control.textCursor()
842 858
843 859 def _get_end_cursor(self):
844 860 """ Convenience method that returns a cursor for the last character.
845 861 """
846 862 cursor = self._control.textCursor()
847 863 cursor.movePosition(QtGui.QTextCursor.End)
848 864 return cursor
849 865
850 866 def _get_input_buffer_cursor_column(self):
851 867 """ Returns the column of the cursor in the input buffer, excluding the
852 868 contribution by the prompt, or -1 if there is no such column.
853 869 """
854 870 prompt = self._get_input_buffer_cursor_prompt()
855 871 if prompt is None:
856 872 return -1
857 873 else:
858 874 cursor = self._control.textCursor()
859 875 return cursor.columnNumber() - len(prompt)
860 876
861 877 def _get_input_buffer_cursor_line(self):
862 878 """ Returns line of the input buffer that contains the cursor, or None
863 879 if there is no such line.
864 880 """
865 881 prompt = self._get_input_buffer_cursor_prompt()
866 882 if prompt is None:
867 883 return None
868 884 else:
869 885 cursor = self._control.textCursor()
870 886 text = self._get_block_plain_text(cursor.block())
871 887 return text[len(prompt):]
872 888
873 889 def _get_input_buffer_cursor_prompt(self):
874 890 """ Returns the (plain text) prompt for line of the input buffer that
875 891 contains the cursor, or None if there is no such line.
876 892 """
877 893 if self._executing:
878 894 return None
879 895 cursor = self._control.textCursor()
880 896 if cursor.position() >= self._prompt_pos:
881 897 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
882 898 return self._prompt
883 899 else:
884 900 return self._continuation_prompt
885 901 else:
886 902 return None
887 903
888 904 def _get_prompt_cursor(self):
889 905 """ Convenience method that returns a cursor for the prompt position.
890 906 """
891 907 cursor = self._control.textCursor()
892 908 cursor.setPosition(self._prompt_pos)
893 909 return cursor
894 910
895 911 def _get_selection_cursor(self, start, end):
896 912 """ Convenience method that returns a cursor with text selected between
897 913 the positions 'start' and 'end'.
898 914 """
899 915 cursor = self._control.textCursor()
900 916 cursor.setPosition(start)
901 917 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
902 918 return cursor
903 919
904 920 def _get_word_start_cursor(self, position):
905 921 """ Find the start of the word to the left the given position. If a
906 922 sequence of non-word characters precedes the first word, skip over
907 923 them. (This emulates the behavior of bash, emacs, etc.)
908 924 """
909 925 document = self._control.document()
910 926 position -= 1
911 927 while position >= self._prompt_pos and \
912 928 not document.characterAt(position).isLetterOrNumber():
913 929 position -= 1
914 930 while position >= self._prompt_pos and \
915 931 document.characterAt(position).isLetterOrNumber():
916 932 position -= 1
917 933 cursor = self._control.textCursor()
918 934 cursor.setPosition(position + 1)
919 935 return cursor
920 936
921 937 def _get_word_end_cursor(self, position):
922 938 """ Find the end of the word to the right the given position. If a
923 939 sequence of non-word characters precedes the first word, skip over
924 940 them. (This emulates the behavior of bash, emacs, etc.)
925 941 """
926 942 document = self._control.document()
927 943 end = self._get_end_cursor().position()
928 944 while position < end and \
929 945 not document.characterAt(position).isLetterOrNumber():
930 946 position += 1
931 947 while position < end and \
932 948 document.characterAt(position).isLetterOrNumber():
933 949 position += 1
934 950 cursor = self._control.textCursor()
935 951 cursor.setPosition(position)
936 952 return cursor
937 953
938 954 def _insert_html(self, cursor, html):
939 955 """ Inserts HTML using the specified cursor in such a way that future
940 956 formatting is unaffected.
941 957 """
942 958 cursor.beginEditBlock()
943 959 cursor.insertHtml(html)
944 960
945 961 # After inserting HTML, the text document "remembers" it's in "html
946 962 # mode", which means that subsequent calls adding plain text will result
947 963 # in unwanted formatting, lost tab characters, etc. The following code
948 964 # hacks around this behavior, which I consider to be a bug in Qt, by
949 965 # (crudely) resetting the document's style state.
950 966 cursor.movePosition(QtGui.QTextCursor.Left,
951 967 QtGui.QTextCursor.KeepAnchor)
952 968 if cursor.selection().toPlainText() == ' ':
953 969 cursor.removeSelectedText()
954 970 else:
955 971 cursor.movePosition(QtGui.QTextCursor.Right)
956 972 cursor.insertText(' ', QtGui.QTextCharFormat())
957 973 cursor.endEditBlock()
958 974
959 975 def _insert_html_fetching_plain_text(self, cursor, html):
960 976 """ Inserts HTML using the specified cursor, then returns its plain text
961 977 version.
962 978 """
963 979 cursor.beginEditBlock()
964 980 cursor.removeSelectedText()
965 981
966 982 start = cursor.position()
967 983 self._insert_html(cursor, html)
968 984 end = cursor.position()
969 985 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
970 986 text = str(cursor.selection().toPlainText())
971 987
972 988 cursor.setPosition(end)
973 989 cursor.endEditBlock()
974 990 return text
975 991
976 992 def _insert_plain_text(self, cursor, text):
977 993 """ Inserts plain text using the specified cursor, processing ANSI codes
978 994 if enabled.
979 995 """
980 996 cursor.beginEditBlock()
981 997 if self.ansi_codes:
982 998 for substring in self._ansi_processor.split_string(text):
983 999 for action in self._ansi_processor.actions:
984 1000 if action.kind == 'erase' and action.area == 'screen':
985 1001 cursor.select(QtGui.QTextCursor.Document)
986 1002 cursor.removeSelectedText()
987 1003 format = self._ansi_processor.get_format()
988 1004 cursor.insertText(substring, format)
989 1005 else:
990 1006 cursor.insertText(text)
991 1007 cursor.endEditBlock()
992 1008
993 1009 def _insert_plain_text_into_buffer(self, text):
994 1010 """ Inserts text into the input buffer at the current cursor position,
995 1011 ensuring that continuation prompts are inserted as necessary.
996 1012 """
997 1013 lines = str(text).splitlines(True)
998 1014 if lines:
999 1015 self._keep_cursor_in_buffer()
1000 1016 cursor = self._control.textCursor()
1001 1017 cursor.beginEditBlock()
1002 1018 cursor.insertText(lines[0])
1003 1019 for line in lines[1:]:
1004 1020 if self._continuation_prompt_html is None:
1005 1021 cursor.insertText(self._continuation_prompt)
1006 1022 else:
1007 1023 self._continuation_prompt = \
1008 1024 self._insert_html_fetching_plain_text(
1009 1025 cursor, self._continuation_prompt_html)
1010 1026 cursor.insertText(line)
1011 1027 cursor.endEditBlock()
1012 1028 self._control.setTextCursor(cursor)
1013 1029
1014 1030 def _in_buffer(self, position=None):
1015 1031 """ Returns whether the current cursor (or, if specified, a position) is
1016 1032 inside the editing region.
1017 1033 """
1018 1034 cursor = self._control.textCursor()
1019 1035 if position is None:
1020 1036 position = cursor.position()
1021 1037 else:
1022 1038 cursor.setPosition(position)
1023 1039 line = cursor.blockNumber()
1024 1040 prompt_line = self._get_prompt_cursor().blockNumber()
1025 1041 if line == prompt_line:
1026 1042 return position >= self._prompt_pos
1027 1043 elif line > prompt_line:
1028 1044 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1029 1045 prompt_pos = cursor.position() + len(self._continuation_prompt)
1030 1046 return position >= prompt_pos
1031 1047 return False
1032 1048
1033 1049 def _keep_cursor_in_buffer(self):
1034 1050 """ Ensures that the cursor is inside the editing region. Returns
1035 1051 whether the cursor was moved.
1036 1052 """
1037 1053 moved = not self._in_buffer()
1038 1054 if moved:
1039 1055 cursor = self._control.textCursor()
1040 1056 cursor.movePosition(QtGui.QTextCursor.End)
1041 1057 self._control.setTextCursor(cursor)
1042 1058 return moved
1043 1059
1044 1060 def _page(self, text):
1045 1061 """ Displays text using the pager if it exceeds the height of the
1046 1062 visible area.
1047 1063 """
1048 1064 if self._page_style == 'none':
1049 1065 self._append_plain_text(text)
1050 1066 else:
1051 1067 line_height = QtGui.QFontMetrics(self.font).height()
1052 1068 minlines = self._control.viewport().height() / line_height
1053 1069 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1054 1070 if self._page_style == 'custom':
1055 1071 self.custom_page_requested.emit(text)
1056 1072 else:
1057 1073 self._page_control.clear()
1058 1074 cursor = self._page_control.textCursor()
1059 1075 self._insert_plain_text(cursor, text)
1060 1076 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1061 1077
1062 1078 self._page_control.viewport().resize(self._control.size())
1063 1079 if self._splitter:
1064 1080 self._page_control.show()
1065 1081 self._page_control.setFocus()
1066 1082 else:
1067 1083 self.layout().setCurrentWidget(self._page_control)
1068 1084 else:
1069 1085 self._append_plain_text(text)
1070 1086
1071 1087 def _prompt_started(self):
1072 1088 """ Called immediately after a new prompt is displayed.
1073 1089 """
1074 1090 # Temporarily disable the maximum block count to permit undo/redo and
1075 1091 # to ensure that the prompt position does not change due to truncation.
1076 1092 # Because setting this property clears the undo/redo history, we only
1077 1093 # set it if we have to.
1078 1094 if self._control.document().maximumBlockCount() > 0:
1079 1095 self._control.document().setMaximumBlockCount(0)
1080 1096 self._control.setUndoRedoEnabled(True)
1081 1097
1082 1098 self._control.setReadOnly(False)
1083 1099 self._control.moveCursor(QtGui.QTextCursor.End)
1084 1100
1085 1101 self._executing = False
1086 1102 self._prompt_started_hook()
1087 1103
1088 1104 def _prompt_finished(self):
1089 1105 """ Called immediately after a prompt is finished, i.e. when some input
1090 1106 will be processed and a new prompt displayed.
1091 1107 """
1092 1108 self._control.setReadOnly(True)
1093 1109 self._prompt_finished_hook()
1094 1110
1095 1111 def _readline(self, prompt='', callback=None):
1096 1112 """ Reads one line of input from the user.
1097 1113
1098 1114 Parameters
1099 1115 ----------
1100 1116 prompt : str, optional
1101 1117 The prompt to print before reading the line.
1102 1118
1103 1119 callback : callable, optional
1104 1120 A callback to execute with the read line. If not specified, input is
1105 1121 read *synchronously* and this method does not return until it has
1106 1122 been read.
1107 1123
1108 1124 Returns
1109 1125 -------
1110 1126 If a callback is specified, returns nothing. Otherwise, returns the
1111 1127 input string with the trailing newline stripped.
1112 1128 """
1113 1129 if self._reading:
1114 1130 raise RuntimeError('Cannot read a line. Widget is already reading.')
1115 1131
1116 1132 if not callback and not self.isVisible():
1117 1133 # If the user cannot see the widget, this function cannot return.
1118 1134 raise RuntimeError('Cannot synchronously read a line if the widget'
1119 1135 'is not visible!')
1120 1136
1121 1137 self._reading = True
1122 1138 self._show_prompt(prompt, newline=False)
1123 1139
1124 1140 if callback is None:
1125 1141 self._reading_callback = None
1126 1142 while self._reading:
1127 1143 QtCore.QCoreApplication.processEvents()
1128 1144 return self.input_buffer.rstrip('\n')
1129 1145
1130 1146 else:
1131 1147 self._reading_callback = lambda: \
1132 1148 callback(self.input_buffer.rstrip('\n'))
1133 1149
1134 1150 def _set_continuation_prompt(self, prompt, html=False):
1135 1151 """ Sets the continuation prompt.
1136 1152
1137 1153 Parameters
1138 1154 ----------
1139 1155 prompt : str
1140 1156 The prompt to show when more input is needed.
1141 1157
1142 1158 html : bool, optional (default False)
1143 1159 If set, the prompt will be inserted as formatted HTML. Otherwise,
1144 1160 the prompt will be treated as plain text, though ANSI color codes
1145 1161 will be handled.
1146 1162 """
1147 1163 if html:
1148 1164 self._continuation_prompt_html = prompt
1149 1165 else:
1150 1166 self._continuation_prompt = prompt
1151 1167 self._continuation_prompt_html = None
1152 1168
1153 1169 def _set_cursor(self, cursor):
1154 1170 """ Convenience method to set the current cursor.
1155 1171 """
1156 1172 self._control.setTextCursor(cursor)
1157 1173
1158 1174 def _set_position(self, position):
1159 1175 """ Convenience method to set the position of the cursor.
1160 1176 """
1161 1177 cursor = self._control.textCursor()
1162 1178 cursor.setPosition(position)
1163 1179 self._control.setTextCursor(cursor)
1164 1180
1165 1181 def _set_selection(self, start, end):
1166 1182 """ Convenience method to set the current selected text.
1167 1183 """
1168 1184 self._control.setTextCursor(self._get_selection_cursor(start, end))
1169 1185
1170 1186 def _show_context_menu(self, pos):
1171 1187 """ Shows a context menu at the given QPoint (in widget coordinates).
1172 1188 """
1173 1189 menu = QtGui.QMenu()
1174 1190
1175 1191 copy_action = menu.addAction('Copy', self.copy)
1176 1192 copy_action.setEnabled(self._get_cursor().hasSelection())
1177 1193 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1178 1194
1179 1195 paste_action = menu.addAction('Paste', self.paste)
1180 1196 paste_action.setEnabled(self.can_paste())
1181 1197 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1182 1198
1183 1199 menu.addSeparator()
1184 1200 menu.addAction('Select All', self.select_all)
1185 1201
1186 1202 menu.exec_(self._control.mapToGlobal(pos))
1187 1203
1188 1204 def _show_prompt(self, prompt=None, html=False, newline=True):
1189 1205 """ Writes a new prompt at the end of the buffer.
1190 1206
1191 1207 Parameters
1192 1208 ----------
1193 1209 prompt : str, optional
1194 1210 The prompt to show. If not specified, the previous prompt is used.
1195 1211
1196 1212 html : bool, optional (default False)
1197 1213 Only relevant when a prompt is specified. If set, the prompt will
1198 1214 be inserted as formatted HTML. Otherwise, the prompt will be treated
1199 1215 as plain text, though ANSI color codes will be handled.
1200 1216
1201 1217 newline : bool, optional (default True)
1202 1218 If set, a new line will be written before showing the prompt if
1203 1219 there is not already a newline at the end of the buffer.
1204 1220 """
1205 1221 # Insert a preliminary newline, if necessary.
1206 1222 if newline:
1207 1223 cursor = self._get_end_cursor()
1208 1224 if cursor.position() > 0:
1209 1225 cursor.movePosition(QtGui.QTextCursor.Left,
1210 1226 QtGui.QTextCursor.KeepAnchor)
1211 1227 if str(cursor.selection().toPlainText()) != '\n':
1212 1228 self._append_plain_text('\n')
1213 1229
1214 1230 # Write the prompt.
1231 self._append_plain_text(self._prompt_sep)
1215 1232 if prompt is None:
1216 1233 if self._prompt_html is None:
1217 1234 self._append_plain_text(self._prompt)
1218 1235 else:
1219 1236 self._append_html(self._prompt_html)
1220 1237 else:
1221 1238 if html:
1222 1239 self._prompt = self._append_html_fetching_plain_text(prompt)
1223 1240 self._prompt_html = prompt
1224 1241 else:
1225 1242 self._append_plain_text(prompt)
1226 1243 self._prompt = prompt
1227 1244 self._prompt_html = None
1228 1245
1229 1246 self._prompt_pos = self._get_end_cursor().position()
1230 1247 self._prompt_started()
1231 1248
1232 1249 def _show_continuation_prompt(self):
1233 1250 """ Writes a new continuation prompt at the end of the buffer.
1234 1251 """
1235 1252 if self._continuation_prompt_html is None:
1236 1253 self._append_plain_text(self._continuation_prompt)
1237 1254 else:
1238 1255 self._continuation_prompt = self._append_html_fetching_plain_text(
1239 1256 self._continuation_prompt_html)
1240 1257
1241 self._prompt_started()
1242
1243 1258
1244 1259 class HistoryConsoleWidget(ConsoleWidget):
1245 1260 """ A ConsoleWidget that keeps a history of the commands that have been
1246 1261 executed.
1247 1262 """
1248 1263
1249 1264 #---------------------------------------------------------------------------
1250 1265 # 'object' interface
1251 1266 #---------------------------------------------------------------------------
1252 1267
1253 1268 def __init__(self, *args, **kw):
1254 1269 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1255 1270 self._history = []
1256 1271 self._history_index = 0
1257 1272
1258 1273 #---------------------------------------------------------------------------
1259 1274 # 'ConsoleWidget' public interface
1260 1275 #---------------------------------------------------------------------------
1261 1276
1262 1277 def execute(self, source=None, hidden=False, interactive=False):
1263 1278 """ Reimplemented to the store history.
1264 1279 """
1265 1280 if not hidden:
1266 1281 history = self.input_buffer if source is None else source
1267 1282
1268 1283 executed = super(HistoryConsoleWidget, self).execute(
1269 1284 source, hidden, interactive)
1270 1285
1271 1286 if executed and not hidden:
1272 1287 self._history.append(history.rstrip())
1273 1288 self._history_index = len(self._history)
1274 1289
1275 1290 return executed
1276 1291
1277 1292 #---------------------------------------------------------------------------
1278 1293 # 'ConsoleWidget' abstract interface
1279 1294 #---------------------------------------------------------------------------
1280 1295
1281 1296 def _up_pressed(self):
1282 1297 """ Called when the up key is pressed. Returns whether to continue
1283 1298 processing the event.
1284 1299 """
1285 1300 prompt_cursor = self._get_prompt_cursor()
1286 1301 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1287 1302 self.history_previous()
1288 1303
1289 1304 # Go to the first line of prompt for seemless history scrolling.
1290 1305 cursor = self._get_prompt_cursor()
1291 1306 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1292 1307 self._set_cursor(cursor)
1293 1308
1294 1309 return False
1295 1310 return True
1296 1311
1297 1312 def _down_pressed(self):
1298 1313 """ Called when the down key is pressed. Returns whether to continue
1299 1314 processing the event.
1300 1315 """
1301 1316 end_cursor = self._get_end_cursor()
1302 1317 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1303 1318 self.history_next()
1304 1319 return False
1305 1320 return True
1306 1321
1307 1322 #---------------------------------------------------------------------------
1308 1323 # 'HistoryConsoleWidget' public interface
1309 1324 #---------------------------------------------------------------------------
1310 1325
1311 1326 def history_previous(self):
1312 1327 """ If possible, set the input buffer to the previous item in the
1313 1328 history.
1314 1329 """
1315 1330 if self._history_index > 0:
1316 1331 self._history_index -= 1
1317 1332 self.input_buffer = self._history[self._history_index]
1318 1333
1319 1334 def history_next(self):
1320 1335 """ Set the input buffer to the next item in the history, or a blank
1321 1336 line if there is no subsequent item.
1322 1337 """
1323 1338 if self._history_index < len(self._history):
1324 1339 self._history_index += 1
1325 1340 if self._history_index < len(self._history):
1326 1341 self.input_buffer = self._history[self._history_index]
1327 1342 else:
1328 1343 self.input_buffer = ''
1329 1344
1330 1345 #---------------------------------------------------------------------------
1331 1346 # 'HistoryConsoleWidget' protected interface
1332 1347 #---------------------------------------------------------------------------
1333 1348
1334 1349 def _set_history(self, history):
1335 1350 """ Replace the current history with a sequence of history items.
1336 1351 """
1337 1352 self._history = list(history)
1338 1353 self._history_index = len(self._history)
@@ -1,343 +1,343 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 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 # Standard library imports
10 10 from subprocess import Popen
11 11
12 12 # System library imports
13 13 from PyQt4 import QtCore, QtGui
14 14
15 15 # Local imports
16 16 from IPython.core.inputsplitter import IPythonInputSplitter
17 17 from IPython.core.usage import default_banner
18 18 from frontend_widget import FrontendWidget
19 19
20 20
21 21 class IPythonPromptBlock(object):
22 22 """ An internal storage object for IPythonWidget.
23 23 """
24 24 def __init__(self, block, length, number):
25 25 self.block = block
26 26 self.length = length
27 27 self.number = number
28 28
29 29
30 30 class IPythonWidget(FrontendWidget):
31 31 """ A FrontendWidget for an IPython kernel.
32 32 """
33 33
34 34 # Signal emitted when an editor is needed for a file and the editor has been
35 35 # specified as 'custom'. See 'set_editor' for more information.
36 36 custom_edit_requested = QtCore.pyqtSignal(object, object)
37 37
38 38 # The default stylesheet: black text on a white background.
39 39 default_stylesheet = """
40 40 .error { color: red; }
41 41 .in-prompt { color: navy; }
42 42 .in-prompt-number { font-weight: bold; }
43 43 .out-prompt { color: darkred; }
44 44 .out-prompt-number { font-weight: bold; }
45 45 """
46 46
47 47 # A dark stylesheet: white text on a black background.
48 48 dark_stylesheet = """
49 49 QPlainTextEdit, QTextEdit { background-color: black; color: white }
50 50 QFrame { border: 1px solid grey; }
51 51 .error { color: red; }
52 52 .in-prompt { color: lime; }
53 53 .in-prompt-number { color: lime; font-weight: bold; }
54 54 .out-prompt { color: red; }
55 55 .out-prompt-number { color: red; font-weight: bold; }
56 56 """
57 57
58 58 # Default prompts.
59 59 in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
60 60 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
61 61
62 62 # FrontendWidget protected class variables.
63 63 _input_splitter_class = IPythonInputSplitter
64 64
65 65 # IPythonWidget protected class variables.
66 66 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
67 67 _payload_source_page = 'IPython.zmq.page.page'
68 68
69 69 #---------------------------------------------------------------------------
70 70 # 'object' interface
71 71 #---------------------------------------------------------------------------
72 72
73 73 def __init__(self, *args, **kw):
74 74 super(IPythonWidget, self).__init__(*args, **kw)
75 75
76 76 # IPythonWidget protected variables.
77 77 self._previous_prompt_obj = None
78 78
79 79 # Set a default editor and stylesheet.
80 80 self.set_editor('default')
81 81 self.reset_styling()
82 82
83 83 #---------------------------------------------------------------------------
84 84 # 'BaseFrontendMixin' abstract interface
85 85 #---------------------------------------------------------------------------
86 86
87 87 def _handle_history_reply(self, msg):
88 88 """ Implemented to handle history replies, which are only supported by
89 89 the IPython kernel.
90 90 """
91 91 history_dict = msg['content']['history']
92 92 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
93 93 self._set_history(items)
94 94
95 95 def _handle_prompt_reply(self, msg):
96 96 """ Implemented to handle prompt number replies, which are only
97 97 supported by the IPython kernel.
98 98 """
99 99 content = msg['content']
100 100 self._show_interpreter_prompt(content['prompt_number'],
101 101 content['input_sep'])
102 102
103 103 def _handle_pyout(self, msg):
104 104 """ Reimplemented for IPython-style "display hook".
105 105 """
106 106 if not self._hidden and self._is_from_this_session(msg):
107 107 content = msg['content']
108 108 prompt_number = content['prompt_number']
109 109 self._append_plain_text(content['output_sep'])
110 110 self._append_html(self._make_out_prompt(prompt_number))
111 111 self._append_plain_text(content['data'] + '\n' +
112 112 content['output_sep2'])
113 113
114 114 def _started_channels(self):
115 115 """ Reimplemented to make a history request.
116 116 """
117 117 super(IPythonWidget, self)._started_channels()
118 118 # FIXME: Disabled until history requests are properly implemented.
119 119 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
120 120
121 121 #---------------------------------------------------------------------------
122 122 # 'FrontendWidget' interface
123 123 #---------------------------------------------------------------------------
124 124
125 125 def execute_file(self, path, hidden=False):
126 126 """ Reimplemented to use the 'run' magic.
127 127 """
128 128 self.execute('%%run %s' % path, hidden=hidden)
129 129
130 130 #---------------------------------------------------------------------------
131 131 # 'FrontendWidget' protected interface
132 132 #---------------------------------------------------------------------------
133 133
134 134 def _get_banner(self):
135 135 """ Reimplemented to return IPython's default banner.
136 136 """
137 137 return default_banner + '\n'
138 138
139 139 def _process_execute_error(self, msg):
140 140 """ Reimplemented for IPython-style traceback formatting.
141 141 """
142 142 content = msg['content']
143 143 traceback = '\n'.join(content['traceback']) + '\n'
144 144 if False:
145 145 # FIXME: For now, tracebacks come as plain text, so we can't use
146 146 # the html renderer yet. Once we refactor ultratb to produce
147 147 # properly styled tracebacks, this branch should be the default
148 148 traceback = traceback.replace(' ', '&nbsp;')
149 149 traceback = traceback.replace('\n', '<br/>')
150 150
151 151 ename = content['ename']
152 152 ename_styled = '<span class="error">%s</span>' % ename
153 153 traceback = traceback.replace(ename, ename_styled)
154 154
155 155 self._append_html(traceback)
156 156 else:
157 157 # This is the fallback for now, using plain text with ansi escapes
158 158 self._append_plain_text(traceback)
159 159
160 160 def _process_execute_payload(self, item):
161 161 """ Reimplemented to handle %edit and paging payloads.
162 162 """
163 163 if item['source'] == self._payload_source_edit:
164 164 self._edit(item['filename'], item['line_number'])
165 165 return True
166 166 elif item['source'] == self._payload_source_page:
167 167 self._page(item['data'])
168 168 return True
169 169 else:
170 170 return False
171 171
172 172 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
173 173 """ Reimplemented for IPython-style prompts.
174 174 """
175 175 # If a number was not specified, make a prompt number request.
176 176 if number is None:
177 177 self.kernel_manager.xreq_channel.prompt()
178 178 return
179 179
180 180 # Show a new prompt and save information about it so that it can be
181 181 # updated later if the prompt number turns out to be wrong.
182 self._append_plain_text(input_sep)
182 self._prompt_sep = input_sep
183 183 self._show_prompt(self._make_in_prompt(number), html=True)
184 184 block = self._control.document().lastBlock()
185 185 length = len(self._prompt)
186 186 self._previous_prompt_obj = IPythonPromptBlock(block, length, number)
187 187
188 188 # Update continuation prompt to reflect (possibly) new prompt length.
189 189 self._set_continuation_prompt(
190 190 self._make_continuation_prompt(self._prompt), html=True)
191 191
192 192 def _show_interpreter_prompt_for_reply(self, msg):
193 193 """ Reimplemented for IPython-style prompts.
194 194 """
195 195 # Update the old prompt number if necessary.
196 196 content = msg['content']
197 197 previous_prompt_number = content['prompt_number']
198 198 if self._previous_prompt_obj and \
199 199 self._previous_prompt_obj.number != previous_prompt_number:
200 200 block = self._previous_prompt_obj.block
201 201
202 202 # Make sure the prompt block has not been erased.
203 203 if block.isValid() and not block.text().isEmpty():
204 204
205 205 # Remove the old prompt and insert a new prompt.
206 206 cursor = QtGui.QTextCursor(block)
207 207 cursor.movePosition(QtGui.QTextCursor.Right,
208 208 QtGui.QTextCursor.KeepAnchor,
209 209 self._previous_prompt_obj.length)
210 210 prompt = self._make_in_prompt(previous_prompt_number)
211 211 self._prompt = self._insert_html_fetching_plain_text(
212 212 cursor, prompt)
213 213
214 214 # When the HTML is inserted, Qt blows away the syntax
215 215 # highlighting for the line, so we need to rehighlight it.
216 216 self._highlighter.rehighlightBlock(cursor.block())
217 217
218 218 self._previous_prompt_obj = None
219 219
220 220 # Show a new prompt with the kernel's estimated prompt number.
221 221 next_prompt = content['next_prompt']
222 222 self._show_interpreter_prompt(next_prompt['prompt_number'],
223 223 next_prompt['input_sep'])
224 224
225 225 #---------------------------------------------------------------------------
226 226 # 'IPythonWidget' interface
227 227 #---------------------------------------------------------------------------
228 228
229 229 def reset_styling(self):
230 230 """ Restores the default IPythonWidget styling.
231 231 """
232 232 self.set_styling(self.default_stylesheet, syntax_style='default')
233 233 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
234 234
235 235 def set_editor(self, editor, line_editor=None):
236 236 """ Sets the editor to use with the %edit magic.
237 237
238 238 Parameters:
239 239 -----------
240 240 editor : str
241 241 A command for invoking a system text editor. If the string contains
242 242 a {filename} format specifier, it will be used. Otherwise, the
243 243 filename will be appended to the end the command.
244 244
245 245 This parameter also takes a special value:
246 246 'custom' : Emit a 'custom_edit_requested(str, int)' signal
247 247 instead of opening an editor.
248 248
249 249 line_editor : str, optional
250 250 The editor command to use when a specific line number is
251 251 requested. The string should contain two format specifiers: {line}
252 252 and {filename}. If this parameter is not specified, the line number
253 253 option to the %edit magic will be ignored.
254 254 """
255 255 self._editor = editor
256 256 self._editor_line = line_editor
257 257
258 258 def set_styling(self, stylesheet, syntax_style=None):
259 259 """ Sets the IPythonWidget styling.
260 260
261 261 Parameters:
262 262 -----------
263 263 stylesheet : str
264 264 A CSS stylesheet. The stylesheet can contain classes for:
265 265 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
266 266 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
267 267 3. IPython: .error, .in-prompt, .out-prompt, etc.
268 268
269 269 syntax_style : str or None [default None]
270 270 If specified, use the Pygments style with given name. Otherwise,
271 271 the stylesheet is queried for Pygments style information.
272 272 """
273 273 self.setStyleSheet(stylesheet)
274 274 self._control.document().setDefaultStyleSheet(stylesheet)
275 275 if self._page_control:
276 276 self._page_control.document().setDefaultStyleSheet(stylesheet)
277 277
278 278 if syntax_style is None:
279 279 self._highlighter.set_style_sheet(stylesheet)
280 280 else:
281 281 self._highlighter.set_style(syntax_style)
282 282
283 283 #---------------------------------------------------------------------------
284 284 # 'IPythonWidget' protected interface
285 285 #---------------------------------------------------------------------------
286 286
287 287 def _edit(self, filename, line=None):
288 288 """ Opens a Python script for editing.
289 289
290 290 Parameters:
291 291 -----------
292 292 filename : str
293 293 A path to a local system file.
294 294
295 295 line : int, optional
296 296 A line of interest in the file.
297 297 """
298 298 if self._editor == 'custom':
299 299 self.custom_edit_requested.emit(filename, line)
300 300 elif self._editor == 'default':
301 301 self._append_plain_text('No default editor available.\n')
302 302 else:
303 303 try:
304 304 filename = '"%s"' % filename
305 305 if line and self._editor_line:
306 306 command = self._editor_line.format(filename=filename,
307 307 line=line)
308 308 else:
309 309 try:
310 310 command = self._editor.format()
311 311 except KeyError:
312 312 command = self._editor.format(filename=filename)
313 313 else:
314 314 command += ' ' + filename
315 315 except KeyError:
316 316 self._append_plain_text('Invalid editor command.\n')
317 317 else:
318 318 try:
319 319 Popen(command, shell=True)
320 320 except OSError:
321 321 msg = 'Opening editor with command "%s" failed.\n'
322 322 self._append_plain_text(msg % command)
323 323
324 324 def _make_in_prompt(self, number):
325 325 """ Given a prompt number, returns an HTML In prompt.
326 326 """
327 327 body = self.in_prompt % number
328 328 return '<span class="in-prompt">%s</span>' % body
329 329
330 330 def _make_continuation_prompt(self, prompt):
331 331 """ Given a plain text version of an In prompt, returns an HTML
332 332 continuation prompt.
333 333 """
334 334 end_chars = '...: '
335 335 space_count = len(prompt.lstrip('\n')) - len(end_chars)
336 336 body = '&nbsp;' * space_count + end_chars
337 337 return '<span class="in-prompt">%s</span>' % body
338 338
339 339 def _make_out_prompt(self, number):
340 340 """ Given a prompt number, returns an HTML Out prompt.
341 341 """
342 342 body = self.out_prompt % number
343 343 return '<span class="out-prompt">%s</span>' % body
@@ -1,86 +1,92 b''
1 1 #!/usr/bin/env python
2 2
3 3 """ A minimal application using the Qt console-style IPython frontend.
4 4 """
5 5
6 6 # Systemm library imports
7 7 from PyQt4 import QtCore, QtGui
8 8
9 9 # Local imports
10 10 from IPython.external.argparse import ArgumentParser
11 11 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
12 12 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
13 13 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
14 14 from IPython.frontend.qt.kernelmanager import QtKernelManager
15 15
16 16 # Constants
17 17 LOCALHOST = '127.0.0.1'
18 18
19 19
20 20 def main():
21 21 """ Entry point for application.
22 22 """
23 23 # Parse command line arguments.
24 24 parser = ArgumentParser()
25 parser.add_argument('-e', '--existing', action='store_true',
25 parser.add_argument('-r', '--rich', action='store_true',
26 help='use a rich text frontend')
27 parser.add_argument('-t', '--tab-simple', action='store_true',
28 help='do tab completion ala a Unix terminal')
29
30 parser.add_argument('--existing', action='store_true',
26 31 help='connect to an existing kernel')
27 32 parser.add_argument('--ip', type=str, default=LOCALHOST,
28 33 help='set the kernel\'s IP address [default localhost]')
29 34 parser.add_argument('--xreq', type=int, metavar='PORT', default=0,
30 35 help='set the XREQ channel port [default random]')
31 36 parser.add_argument('--sub', type=int, metavar='PORT', default=0,
32 37 help='set the SUB channel port [default random]')
33 38 parser.add_argument('--rep', type=int, metavar='PORT', default=0,
34 39 help='set the REP channel port [default random]')
40
35 41 group = parser.add_mutually_exclusive_group()
36 42 group.add_argument('--pure', action='store_true', help = \
37 43 'use a pure Python kernel instead of an IPython kernel')
38 44 group.add_argument('--pylab', action='store_true',
39 45 help='use a kernel with PyLab enabled')
40 parser.add_argument('--rich', action='store_true',
41 help='use a rich text frontend')
46
42 47 args = parser.parse_args()
43 48
44 49 # Don't let Qt or ZMQ swallow KeyboardInterupts.
45 50 import signal
46 51 signal.signal(signal.SIGINT, signal.SIG_DFL)
47 52
48 53 # Create a KernelManager and start a kernel.
49 54 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
50 55 sub_address=(args.ip, args.sub),
51 56 rep_address=(args.ip, args.rep))
52 57 if args.ip == LOCALHOST and not args.existing:
53 58 if args.pure:
54 59 kernel_manager.start_kernel(ipython=False)
55 60 elif args.pylab:
56 61 if args.rich:
57 62 kernel_manager.start_kernel(pylab='payload-svg')
58 63 else:
59 64 kernel_manager.start_kernel(pylab='qt4')
60 65 else:
61 66 kernel_manager.start_kernel()
62 67 kernel_manager.start_channels()
63 68
64 69 # Create the widget.
65 70 app = QtGui.QApplication([])
66 71 if args.pure:
67 72 kind = 'rich' if args.rich else 'plain'
68 73 widget = FrontendWidget(kind=kind)
69 74 elif args.rich:
70 75 widget = RichIPythonWidget()
71 76 else:
72 77 widget = IPythonWidget()
78 widget.gui_completion = not args.tab_simple
73 79 widget.kernel_manager = kernel_manager
74 80 widget.setWindowTitle('Python' if args.pure else 'IPython')
75 81 widget.show()
76 82
77 83 # FIXME: This is a hack: set colors to lightbg by default in qt terminal
78 84 # unconditionally, regardless of user settings in config files.
79 85 widget.execute("%colors lightbg", hidden=True)
80 86
81 87 # Start the application main loop.
82 88 app.exec_()
83 89
84 90
85 91 if __name__ == '__main__':
86 92 main()
General Comments 0
You need to be logged in to leave comments. Login now