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