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