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