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