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