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