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