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