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