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