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