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