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