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