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