##// END OF EJS Templates
Refactored ConsoleWidget to encapsulate, rather than inherit from, QPlainTextEdit. This permits a QTextEdit to be substituted for a QPlainTextEdit if desired. It also makes it more clear what is the public interface of ConsoleWidget.
epatters -
Show More
This diff has been collapsed as it changes many lines, (694 lines changed) Show them Hide them
@@ -9,7 +9,7 b' from ansi_code_processor import QtAnsiCodeProcessor'
9 9 from completion_widget import CompletionWidget
10 10
11 11
12 class ConsoleWidget(QtGui.QPlainTextEdit):
12 class ConsoleWidget(QtGui.QWidget):
13 13 """ Base class for console-type widgets. This class is mainly concerned with
14 14 dealing with the prompt, keeping the cursor inside the editing line, and
15 15 handling ANSI escape sequences.
@@ -29,6 +29,11 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
29 29 # priority (when it has focus) over, e.g., window-level menu shortcuts.
30 30 override_shortcuts = False
31 31
32 # Signals that indicate ConsoleWidget state.
33 copy_available = QtCore.pyqtSignal(bool)
34 redo_available = QtCore.pyqtSignal(bool)
35 undo_available = QtCore.pyqtSignal(bool)
36
32 37 # Protected class variables.
33 38 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
34 39 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
@@ -44,13 +49,28 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
44 49 # 'QObject' interface
45 50 #---------------------------------------------------------------------------
46 51
47 def __init__(self, parent=None):
48 QtGui.QPlainTextEdit.__init__(self, parent)
52 def __init__(self, kind='plain', parent=None):
53 """ Create a ConsoleWidget.
54
55 Parameters
56 ----------
57 kind : str, optional [default 'plain']
58 The type of text widget to use. Valid values are 'plain', which
59 specifies a QPlainTextEdit, and 'rich', which specifies an
60 QTextEdit.
61
62 parent : QWidget, optional [default None]
63 The parent for this widget.
64 """
65 super(ConsoleWidget, self).__init__(parent)
66
67 # Create the underlying text widget.
68 self._control = self._create_control(kind)
49 69
50 70 # Initialize protected variables. Some variables contain useful state
51 71 # information for subclasses; they should be considered read-only.
52 72 self._ansi_processor = QtAnsiCodeProcessor()
53 self._completion_widget = CompletionWidget(self)
73 self._completion_widget = CompletionWidget(self._control)
54 74 self._continuation_prompt = '> '
55 75 self._continuation_prompt_html = None
56 76 self._executing = False
@@ -61,252 +81,63 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
61 81 self._reading_callback = None
62 82 self._tab_width = 8
63 83
64 # Set a monospaced font.
65 self.reset_font()
66
67 84 # Define a custom context menu.
68 self._context_menu = QtGui.QMenu(self)
69
70 copy_action = QtGui.QAction('Copy', self)
71 copy_action.triggered.connect(self.copy)
72 self.copyAvailable.connect(copy_action.setEnabled)
73 self._context_menu.addAction(copy_action)
85 self._context_menu = self._create_context_menu()
74 86
75 self._paste_action = QtGui.QAction('Paste', self)
76 self._paste_action.triggered.connect(self.paste)
77 self._context_menu.addAction(self._paste_action)
78 self._context_menu.addSeparator()
87 # Set a monospaced font.
88 self.reset_font()
79 89
80 select_all_action = QtGui.QAction('Select All', self)
81 select_all_action.triggered.connect(self.selectAll)
82 self._context_menu.addAction(select_all_action)
83
84 def event(self, event):
85 """ Reimplemented to override shortcuts, if necessary.
86 """
87 # On Mac OS, it is always unnecessary to override shortcuts, hence the
88 # check below. Users should just use the Control key instead of the
89 # Command key.
90 if self.override_shortcuts and \
91 sys.platform != 'darwin' and \
92 event.type() == QtCore.QEvent.ShortcutOverride and \
93 self._control_down(event.modifiers()) and \
94 event.key() in self._shortcuts:
95 event.accept()
96 return True
97 else:
98 return QtGui.QPlainTextEdit.event(self, event)
90 def eventFilter(self, obj, event):
91 """ Reimplemented to ensure a console-like behavior in the underlying
92 text widget.
93 """
94 if obj == self._control:
95 etype = event.type()
96
97 # Override the default context menu with one that does not have
98 # destructive actions.
99 if etype == QtCore.QEvent.ContextMenu:
100 self._context_menu.exec_(event.globalPos())
101 return True
102
103 # Disable moving text by drag and drop.
104 elif etype == QtCore.QEvent.DragMove:
105 return True
106
107 elif etype == QtCore.QEvent.KeyPress:
108 return self._event_filter_keypress(event)
109
110 # On Mac OS, it is always unnecessary to override shortcuts, hence
111 # the check below. Users should just use the Control key instead of
112 # the Command key.
113 elif etype == QtCore.QEvent.ShortcutOverride:
114 if sys.platform != 'darwin' and \
115 self._control_key_down(event.modifiers()) and \
116 event.key() in self._shortcuts:
117 event.accept()
118 return False
119
120 return super(ConsoleWidget, self).eventFilter(obj, event)
99 121
100 122 #---------------------------------------------------------------------------
101 # 'QWidget' interface
123 # 'ConsoleWidget' public interface
102 124 #---------------------------------------------------------------------------
103 125
104 def contextMenuEvent(self, event):
105 """ Reimplemented to create a menu without destructive actions like
106 'Cut' and 'Delete'.
107 """
108 clipboard_empty = QtGui.QApplication.clipboard().text().isEmpty()
109 self._paste_action.setEnabled(not clipboard_empty)
110
111 self._context_menu.exec_(event.globalPos())
112
113 def dragMoveEvent(self, event):
114 """ Reimplemented to disable moving text by drag and drop.
115 """
116 event.ignore()
117
118 def keyPressEvent(self, event):
119 """ Reimplemented to create a console-like interface.
120 """
121 intercepted = False
122 cursor = self.textCursor()
123 position = cursor.position()
124 key = event.key()
125 ctrl_down = self._control_down(event.modifiers())
126 alt_down = event.modifiers() & QtCore.Qt.AltModifier
127 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
128
129 # Even though we have reimplemented 'paste', the C++ level slot is still
130 # called by Qt. So we intercept the key press here.
131 if event.matches(QtGui.QKeySequence.Paste):
132 self.paste()
133 intercepted = True
134
135 elif ctrl_down:
136 if key in self._ctrl_down_remap:
137 ctrl_down = False
138 key = self._ctrl_down_remap[key]
139 event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
140 QtCore.Qt.NoModifier)
141
142 elif key == QtCore.Qt.Key_K:
143 if self._in_buffer(position):
144 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
145 QtGui.QTextCursor.KeepAnchor)
146 cursor.removeSelectedText()
147 intercepted = True
148
149 elif key == QtCore.Qt.Key_X:
150 intercepted = True
151
152 elif key == QtCore.Qt.Key_Y:
153 self.paste()
154 intercepted = True
155
156 elif alt_down:
157 if key == QtCore.Qt.Key_B:
158 self.setTextCursor(self._get_word_start_cursor(position))
159 intercepted = True
160
161 elif key == QtCore.Qt.Key_F:
162 self.setTextCursor(self._get_word_end_cursor(position))
163 intercepted = True
164
165 elif key == QtCore.Qt.Key_Backspace:
166 cursor = self._get_word_start_cursor(position)
167 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
168 cursor.removeSelectedText()
169 intercepted = True
170
171 elif key == QtCore.Qt.Key_D:
172 cursor = self._get_word_end_cursor(position)
173 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
174 cursor.removeSelectedText()
175 intercepted = True
176
177 if self._completion_widget.isVisible():
178 self._completion_widget.keyPressEvent(event)
179 intercepted = event.isAccepted()
180
181 else:
182 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
183 if self._reading:
184 self.appendPlainText('\n')
185 self._reading = False
186 if self._reading_callback:
187 self._reading_callback()
188 elif not self._executing:
189 self.execute(interactive=True)
190 intercepted = True
191
192 elif key == QtCore.Qt.Key_Up:
193 if self._reading or not self._up_pressed():
194 intercepted = True
195 else:
196 prompt_line = self._get_prompt_cursor().blockNumber()
197 intercepted = cursor.blockNumber() <= prompt_line
198
199 elif key == QtCore.Qt.Key_Down:
200 if self._reading or not self._down_pressed():
201 intercepted = True
202 else:
203 end_line = self._get_end_cursor().blockNumber()
204 intercepted = cursor.blockNumber() == end_line
205
206 elif key == QtCore.Qt.Key_Tab:
207 if self._reading:
208 intercepted = False
209 else:
210 intercepted = not self._tab_pressed()
211
212 elif key == QtCore.Qt.Key_Left:
213 intercepted = not self._in_buffer(position - 1)
214
215 elif key == QtCore.Qt.Key_Home:
216 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
217 start_line = cursor.blockNumber()
218 if start_line == self._get_prompt_cursor().blockNumber():
219 start_pos = self._prompt_pos
220 else:
221 start_pos = cursor.position()
222 start_pos += len(self._continuation_prompt)
223 if shift_down and self._in_buffer(position):
224 self._set_selection(position, start_pos)
225 else:
226 self._set_position(start_pos)
227 intercepted = True
228
229 elif key == QtCore.Qt.Key_Backspace and not alt_down:
230
231 # Line deletion (remove continuation prompt)
232 len_prompt = len(self._continuation_prompt)
233 if not self._reading and \
234 cursor.columnNumber() == len_prompt and \
235 position != self._prompt_pos:
236 cursor.setPosition(position - len_prompt,
237 QtGui.QTextCursor.KeepAnchor)
238 cursor.removeSelectedText()
239
240 # Regular backwards deletion
241 else:
242 anchor = cursor.anchor()
243 if anchor == position:
244 intercepted = not self._in_buffer(position - 1)
245 else:
246 intercepted = not self._in_buffer(min(anchor, position))
247
248 elif key == QtCore.Qt.Key_Delete:
249 anchor = cursor.anchor()
250 intercepted = not self._in_buffer(min(anchor, position))
251
252 # Don't move cursor if control is down to allow copy-paste using
253 # the keyboard in any part of the buffer.
254 if not ctrl_down:
255 self._keep_cursor_in_buffer()
256
257 if not intercepted:
258 QtGui.QPlainTextEdit.keyPressEvent(self, event)
259
260 #--------------------------------------------------------------------------
261 # 'QPlainTextEdit' interface
262 #--------------------------------------------------------------------------
263
264 def appendHtml(self, html):
265 """ Reimplemented to not append HTML as a new paragraph, which doesn't
266 make sense for a console widget.
267 """
268 cursor = self._get_end_cursor()
269 self._insert_html(cursor, html)
270
271 def appendPlainText(self, text):
272 """ Reimplemented to not append text as a new paragraph, which doesn't
273 make sense for a console widget. Also, if enabled, handle ANSI
274 codes.
275 """
276 cursor = self._get_end_cursor()
277 if self.ansi_codes:
278 for substring in self._ansi_processor.split_string(text):
279 format = self._ansi_processor.get_format()
280 cursor.insertText(substring, format)
281 else:
282 cursor.insertText(text)
283
284 126 def clear(self, keep_input=False):
285 """ Reimplemented to write a new prompt. If 'keep_input' is set,
127 """ Clear the console, then write a new prompt. If 'keep_input' is set,
286 128 restores the old input buffer when the new prompt is written.
287 129 """
288 QtGui.QPlainTextEdit.clear(self)
130 self._control.clear()
289 131 if keep_input:
290 132 input_buffer = self.input_buffer
291 133 self._show_prompt()
292 134 if keep_input:
293 135 self.input_buffer = input_buffer
294 136
295 def paste(self):
296 """ Reimplemented to ensure that text is pasted in the editing region.
137 def copy(self):
138 """ Copy the current selected text to the clipboard.
297 139 """
298 self._keep_cursor_in_buffer()
299 QtGui.QPlainTextEdit.paste(self)
300
301 def print_(self, printer):
302 """ Reimplemented to work around a bug in PyQt: the C++ level 'print_'
303 slot has the wrong signature.
304 """
305 QtGui.QPlainTextEdit.print_(self, printer)
306
307 #---------------------------------------------------------------------------
308 # 'ConsoleWidget' public interface
309 #---------------------------------------------------------------------------
140 self._control.copy()
310 141
311 142 def execute(self, source=None, hidden=False, interactive=False):
312 143 """ Executes source or the input buffer, possibly prompting for more
@@ -346,7 +177,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
346 177 if source is not None:
347 178 self.input_buffer = source
348 179
349 self.appendPlainText('\n')
180 self._append_plain_text('\n')
350 181 self._executing_input_buffer = self.input_buffer
351 182 self._executing = True
352 183 self._prompt_finished()
@@ -358,7 +189,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
358 189 # The maximum block count is only in effect during execution.
359 190 # This ensures that _prompt_pos does not become invalid due to
360 191 # text truncation.
361 self.setMaximumBlockCount(self.buffer_size)
192 self._control.document().setMaximumBlockCount(self.buffer_size)
362 193 self._execute(real_source, hidden)
363 194 elif hidden:
364 195 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
@@ -393,14 +224,14 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
393 224 # Insert new text with continuation prompts.
394 225 lines = string.splitlines(True)
395 226 if lines:
396 self.appendPlainText(lines[0])
227 self._append_plain_text(lines[0])
397 228 for i in xrange(1, len(lines)):
398 229 if self._continuation_prompt_html is None:
399 self.appendPlainText(self._continuation_prompt)
230 self._append_plain_text(self._continuation_prompt)
400 231 else:
401 self.appendHtml(self._continuation_prompt_html)
402 self.appendPlainText(lines[i])
403 self.moveCursor(QtGui.QTextCursor.End)
232 self._append_html(self._continuation_prompt_html)
233 self._append_plain_text(lines[i])
234 self._control.moveCursor(QtGui.QTextCursor.End)
404 235
405 236 input_buffer = property(_get_input_buffer, _set_input_buffer)
406 237
@@ -410,7 +241,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
410 241 """
411 242 if self._executing:
412 243 return None
413 cursor = self.textCursor()
244 cursor = self._control.textCursor()
414 245 if cursor.position() >= self._prompt_pos:
415 246 text = self._get_block_plain_text(cursor.block())
416 247 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
@@ -425,19 +256,36 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
425 256 def _get_font(self):
426 257 """ The base font being used by the ConsoleWidget.
427 258 """
428 return self.document().defaultFont()
259 return self._control.document().defaultFont()
429 260
430 261 def _set_font(self, font):
431 262 """ Sets the base font for the ConsoleWidget to the specified QFont.
432 263 """
433 264 font_metrics = QtGui.QFontMetrics(font)
434 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
265 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
435 266
436 267 self._completion_widget.setFont(font)
437 self.document().setDefaultFont(font)
268 self._control.document().setDefaultFont(font)
438 269
439 270 font = property(_get_font, _set_font)
440 271
272 def paste(self):
273 """ Paste the contents of the clipboard into the input region.
274 """
275 self._keep_cursor_in_buffer()
276 self._control.paste()
277
278 def print_(self, printer):
279 """ Print the contents of the ConsoleWidget to the specified QPrinter.
280 """
281 self._control.print_(printer)
282
283 def redo(self):
284 """ Redo the last operation. If there is no operation to redo, nothing
285 happens.
286 """
287 self._control.redo()
288
441 289 def reset_font(self):
442 290 """ Sets the font to the default fixed-width font for this platform.
443 291 """
@@ -451,6 +299,11 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
451 299 font.setStyleHint(QtGui.QFont.TypeWriter)
452 300 self._set_font(font)
453 301
302 def select_all(self):
303 """ Selects all the text in the buffer.
304 """
305 self._control.selectAll()
306
454 307 def _get_tab_width(self):
455 308 """ The width (in terms of space characters) for tab characters.
456 309 """
@@ -460,12 +313,18 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
460 313 """ Sets the width (in terms of space characters) for tab characters.
461 314 """
462 315 font_metrics = QtGui.QFontMetrics(self.font)
463 self.setTabStopWidth(tab_width * font_metrics.width(' '))
316 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
464 317
465 318 self._tab_width = tab_width
466 319
467 320 tab_width = property(_get_tab_width, _set_tab_width)
468
321
322 def undo(self):
323 """ Undo the last operation. If there is no operation to undo, nothing
324 happens.
325 """
326 self._control.undo()
327
469 328 #---------------------------------------------------------------------------
470 329 # 'ConsoleWidget' abstract interface
471 330 #---------------------------------------------------------------------------
@@ -515,28 +374,60 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
515 374 # 'ConsoleWidget' protected interface
516 375 #--------------------------------------------------------------------------
517 376
377 def _append_html(self, html):
378 """ Appends html at the end of the console buffer.
379 """
380 cursor = self._get_end_cursor()
381 self._insert_html(cursor, html)
382
518 383 def _append_html_fetching_plain_text(self, html):
519 384 """ Appends 'html', then returns the plain text version of it.
520 385 """
521 386 anchor = self._get_end_cursor().position()
522 self.appendHtml(html)
387 self._append_html(html)
523 388 cursor = self._get_end_cursor()
524 389 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
525 390 return str(cursor.selection().toPlainText())
526 391
392 def _append_plain_text(self, text):
393 """ Appends plain text at the end of the console buffer, processing
394 ANSI codes if enabled.
395 """
396 cursor = self._get_end_cursor()
397 if self.ansi_codes:
398 for substring in self._ansi_processor.split_string(text):
399 format = self._ansi_processor.get_format()
400 cursor.insertText(substring, format)
401 else:
402 cursor.insertText(text)
403
527 404 def _append_plain_text_keeping_prompt(self, text):
528 405 """ Writes 'text' after the current prompt, then restores the old prompt
529 406 with its old input buffer.
530 407 """
531 408 input_buffer = self.input_buffer
532 self.appendPlainText('\n')
409 self._append_plain_text('\n')
533 410 self._prompt_finished()
534 411
535 self.appendPlainText(text)
412 self._append_plain_text(text)
536 413 self._show_prompt()
537 414 self.input_buffer = input_buffer
538 415
539 def _control_down(self, modifiers):
416 def _complete_with_items(self, cursor, items):
417 """ Performs completion with 'items' at the specified cursor location.
418 """
419 if len(items) == 1:
420 cursor.setPosition(self._control.textCursor().position(),
421 QtGui.QTextCursor.KeepAnchor)
422 cursor.insertText(items[0])
423 elif len(items) > 1:
424 if self.gui_completion:
425 self._completion_widget.show_items(cursor, items)
426 else:
427 text = self._format_as_columns(items)
428 self._append_plain_text_keeping_prompt(text)
429
430 def _control_key_down(self, modifiers):
540 431 """ Given a KeyboardModifiers flags object, return whether the Control
541 432 key is down (on Mac OS, treat the Command key as a synonym for
542 433 Control).
@@ -549,20 +440,195 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
549 440 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
550 441
551 442 return down
552
553 def _complete_with_items(self, cursor, items):
554 """ Performs completion with 'items' at the specified cursor location.
443
444 def _create_context_menu(self):
445 """ Creates a context menu for the underlying text widget.
555 446 """
556 if len(items) == 1:
557 cursor.setPosition(self.textCursor().position(),
558 QtGui.QTextCursor.KeepAnchor)
559 cursor.insertText(items[0])
560 elif len(items) > 1:
561 if self.gui_completion:
562 self._completion_widget.show_items(cursor, items)
563 else:
564 text = self._format_as_columns(items)
565 self._append_plain_text_keeping_prompt(text)
447 menu = QtGui.QMenu(self)
448 clipboard = QtGui.QApplication.clipboard()
449
450 copy_action = QtGui.QAction('Copy', self)
451 copy_action.triggered.connect(self.copy)
452 self.copy_available.connect(copy_action.setEnabled)
453 menu.addAction(copy_action)
454
455 paste_action = QtGui.QAction('Paste', self)
456 paste_action.triggered.connect(self.paste)
457 clipboard.dataChanged.connect(
458 lambda: paste_action.setEnabled(not clipboard.text().isEmpty()))
459 menu.addAction(paste_action)
460 menu.addSeparator()
461
462 select_all_action = QtGui.QAction('Select All', self)
463 select_all_action.triggered.connect(self.select_all)
464 menu.addAction(select_all_action)
465
466 return menu
467
468 def _create_control(self, kind):
469 """ Creates and sets the underlying text widget.
470 """
471 layout = QtGui.QVBoxLayout(self)
472 layout.setMargin(0)
473 if kind == 'plain':
474 control = QtGui.QPlainTextEdit()
475 elif kind == 'rich':
476 control = QtGui.QTextEdit()
477 else:
478 raise ValueError("Kind %s unknown." % repr(kind))
479 layout.addWidget(control)
480
481 control.installEventFilter(self)
482 control.copyAvailable.connect(self.copy_available)
483 control.redoAvailable.connect(self.redo_available)
484 control.undoAvailable.connect(self.undo_available)
485
486 return control
487
488 def _event_filter_keypress(self, event):
489 """ Filter key events for the underlying text widget to create a
490 console-like interface.
491 """
492 intercepted = False
493 replaced_event = None
494 cursor = self._control.textCursor()
495 position = cursor.position()
496 key = event.key()
497 ctrl_down = self._control_key_down(event.modifiers())
498 alt_down = event.modifiers() & QtCore.Qt.AltModifier
499 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
500
501 # Even though we have reimplemented 'paste', the C++ level slot is still
502 # called by Qt. So we intercept the key press here.
503 if event.matches(QtGui.QKeySequence.Paste):
504 self.paste()
505 intercepted = True
506
507 elif ctrl_down:
508 if key in self._ctrl_down_remap:
509 ctrl_down = False
510 key = self._ctrl_down_remap[key]
511 replaced_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress, key,
512 QtCore.Qt.NoModifier)
513
514 elif key == QtCore.Qt.Key_K:
515 if self._in_buffer(position):
516 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
517 QtGui.QTextCursor.KeepAnchor)
518 cursor.removeSelectedText()
519 intercepted = True
520
521 elif key == QtCore.Qt.Key_X:
522 intercepted = True
523
524 elif key == QtCore.Qt.Key_Y:
525 self.paste()
526 intercepted = True
527
528 elif alt_down:
529 if key == QtCore.Qt.Key_B:
530 self._set_cursor(self._get_word_start_cursor(position))
531 intercepted = True
532
533 elif key == QtCore.Qt.Key_F:
534 self._set_cursor(self._get_word_end_cursor(position))
535 intercepted = True
536
537 elif key == QtCore.Qt.Key_Backspace:
538 cursor = self._get_word_start_cursor(position)
539 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
540 cursor.removeSelectedText()
541 intercepted = True
542
543 elif key == QtCore.Qt.Key_D:
544 cursor = self._get_word_end_cursor(position)
545 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
546 cursor.removeSelectedText()
547 intercepted = True
548
549 if self._completion_widget.isVisible():
550 self._completion_widget.keyPressEvent(event)
551 intercepted = event.isAccepted()
552
553 else:
554 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
555 if self._reading:
556 self._append_plain_text('\n')
557 self._reading = False
558 if self._reading_callback:
559 self._reading_callback()
560 elif not self._executing:
561 self.execute(interactive=True)
562 intercepted = True
563
564 elif key == QtCore.Qt.Key_Up:
565 if self._reading or not self._up_pressed():
566 intercepted = True
567 else:
568 prompt_line = self._get_prompt_cursor().blockNumber()
569 intercepted = cursor.blockNumber() <= prompt_line
570
571 elif key == QtCore.Qt.Key_Down:
572 if self._reading or not self._down_pressed():
573 intercepted = True
574 else:
575 end_line = self._get_end_cursor().blockNumber()
576 intercepted = cursor.blockNumber() == end_line
577
578 elif key == QtCore.Qt.Key_Tab:
579 if self._reading:
580 intercepted = False
581 else:
582 intercepted = not self._tab_pressed()
583
584 elif key == QtCore.Qt.Key_Left:
585 intercepted = not self._in_buffer(position - 1)
586
587 elif key == QtCore.Qt.Key_Home:
588 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
589 start_line = cursor.blockNumber()
590 if start_line == self._get_prompt_cursor().blockNumber():
591 start_pos = self._prompt_pos
592 else:
593 start_pos = cursor.position()
594 start_pos += len(self._continuation_prompt)
595 if shift_down and self._in_buffer(position):
596 self._set_selection(position, start_pos)
597 else:
598 self._set_position(start_pos)
599 intercepted = True
600
601 elif key == QtCore.Qt.Key_Backspace and not alt_down:
602
603 # Line deletion (remove continuation prompt)
604 len_prompt = len(self._continuation_prompt)
605 if not self._reading and \
606 cursor.columnNumber() == len_prompt and \
607 position != self._prompt_pos:
608 cursor.setPosition(position - len_prompt,
609 QtGui.QTextCursor.KeepAnchor)
610 cursor.removeSelectedText()
611
612 # Regular backwards deletion
613 else:
614 anchor = cursor.anchor()
615 if anchor == position:
616 intercepted = not self._in_buffer(position - 1)
617 else:
618 intercepted = not self._in_buffer(min(anchor, position))
619
620 elif key == QtCore.Qt.Key_Delete:
621 anchor = cursor.anchor()
622 intercepted = not self._in_buffer(min(anchor, position))
623
624 # Don't move cursor if control is down to allow copy-paste using
625 # the keyboard in any part of the buffer.
626 if not ctrl_down:
627 self._keep_cursor_in_buffer()
628
629 if not intercepted and replaced_event:
630 QtGui.qApp.sendEvent(self._control, replaced_event)
631 return intercepted
566 632
567 633 def _format_as_columns(self, items, separator=' '):
568 634 """ Transform a list of strings into a single string with columns.
@@ -639,18 +705,23 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
639 705 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
640 706 QtGui.QTextCursor.KeepAnchor)
641 707 return str(cursor.selection().toPlainText())
708
709 def _get_cursor(self):
710 """ Convenience method that returns a cursor for the current position.
711 """
712 return self._control.textCursor()
642 713
643 714 def _get_end_cursor(self):
644 715 """ Convenience method that returns a cursor for the last character.
645 716 """
646 cursor = self.textCursor()
717 cursor = self._control.textCursor()
647 718 cursor.movePosition(QtGui.QTextCursor.End)
648 719 return cursor
649 720
650 721 def _get_prompt_cursor(self):
651 722 """ Convenience method that returns a cursor for the prompt position.
652 723 """
653 cursor = self.textCursor()
724 cursor = self._control.textCursor()
654 725 cursor.setPosition(self._prompt_pos)
655 726 return cursor
656 727
@@ -658,7 +729,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
658 729 """ Convenience method that returns a cursor with text selected between
659 730 the positions 'start' and 'end'.
660 731 """
661 cursor = self.textCursor()
732 cursor = self._control.textCursor()
662 733 cursor.setPosition(start)
663 734 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
664 735 return cursor
@@ -668,7 +739,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
668 739 sequence of non-word characters precedes the first word, skip over
669 740 them. (This emulates the behavior of bash, emacs, etc.)
670 741 """
671 document = self.document()
742 document = self._control.document()
672 743 position -= 1
673 744 while self._in_buffer(position) and \
674 745 not document.characterAt(position).isLetterOrNumber():
@@ -676,7 +747,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
676 747 while self._in_buffer(position) and \
677 748 document.characterAt(position).isLetterOrNumber():
678 749 position -= 1
679 cursor = self.textCursor()
750 cursor = self._control.textCursor()
680 751 cursor.setPosition(position + 1)
681 752 return cursor
682 753
@@ -685,7 +756,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
685 756 sequence of non-word characters precedes the first word, skip over
686 757 them. (This emulates the behavior of bash, emacs, etc.)
687 758 """
688 document = self.document()
759 document = self._control.document()
689 760 end = self._get_end_cursor().position()
690 761 while position < end and \
691 762 not document.characterAt(position).isLetterOrNumber():
@@ -693,7 +764,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
693 764 while position < end and \
694 765 document.characterAt(position).isLetterOrNumber():
695 766 position += 1
696 cursor = self.textCursor()
767 cursor = self._control.textCursor()
697 768 cursor.setPosition(position)
698 769 return cursor
699 770
@@ -718,17 +789,33 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
718 789 cursor.movePosition(QtGui.QTextCursor.Right)
719 790 cursor.insertText(' ', QtGui.QTextCharFormat())
720 791
792 def _in_buffer(self, position):
793 """ Returns whether the given position is inside the editing region.
794 """
795 return position >= self._prompt_pos
796
797 def _keep_cursor_in_buffer(self):
798 """ Ensures that the cursor is inside the editing region. Returns
799 whether the cursor was moved.
800 """
801 cursor = self._control.textCursor()
802 if cursor.position() < self._prompt_pos:
803 cursor.movePosition(QtGui.QTextCursor.End)
804 self._control.setTextCursor(cursor)
805 return True
806 else:
807 return False
808
721 809 def _prompt_started(self):
722 810 """ Called immediately after a new prompt is displayed.
723 811 """
724 812 # Temporarily disable the maximum block count to permit undo/redo and
725 813 # to ensure that the prompt position does not change due to truncation.
726 self.setMaximumBlockCount(0)
727 self.setUndoRedoEnabled(True)
814 self._control.document().setMaximumBlockCount(0)
815 self._control.setUndoRedoEnabled(True)
728 816
729 self.setReadOnly(False)
730 self.moveCursor(QtGui.QTextCursor.End)
731 self.centerCursor()
817 self._control.setReadOnly(False)
818 self._control.moveCursor(QtGui.QTextCursor.End)
732 819
733 820 self._executing = False
734 821 self._prompt_started_hook()
@@ -737,8 +824,8 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
737 824 """ Called immediately after a prompt is finished, i.e. when some input
738 825 will be processed and a new prompt displayed.
739 826 """
740 self.setUndoRedoEnabled(False)
741 self.setReadOnly(True)
827 self._control.setUndoRedoEnabled(False)
828 self._control.setReadOnly(True)
742 829 self._prompt_finished_hook()
743 830
744 831 def _readline(self, prompt='', callback=None):
@@ -783,7 +870,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
783 870 def _reset(self):
784 871 """ Clears the console and resets internal state variables.
785 872 """
786 QtGui.QPlainTextEdit.clear(self)
873 self._control.clear()
787 874 self._executing = self._reading = False
788 875
789 876 def _set_continuation_prompt(self, prompt, html=False):
@@ -804,18 +891,23 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
804 891 else:
805 892 self._continuation_prompt = prompt
806 893 self._continuation_prompt_html = None
894
895 def _set_cursor(self, cursor):
896 """ Convenience method to set the current cursor.
897 """
898 self._control.setTextCursor(cursor)
807 899
808 900 def _set_position(self, position):
809 901 """ Convenience method to set the position of the cursor.
810 902 """
811 cursor = self.textCursor()
903 cursor = self._control.textCursor()
812 904 cursor.setPosition(position)
813 self.setTextCursor(cursor)
905 self._control.setTextCursor(cursor)
814 906
815 907 def _set_selection(self, start, end):
816 908 """ Convenience method to set the current selected text.
817 909 """
818 self.setTextCursor(self._get_selection_cursor(start, end))
910 self._control.setTextCursor(self._get_selection_cursor(start, end))
819 911
820 912 def _show_prompt(self, prompt=None, html=False, newline=True):
821 913 """ Writes a new prompt at the end of the buffer.
@@ -841,20 +933,20 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
841 933 cursor.movePosition(QtGui.QTextCursor.Left,
842 934 QtGui.QTextCursor.KeepAnchor)
843 935 if str(cursor.selection().toPlainText()) != '\n':
844 self.appendPlainText('\n')
936 self._append_plain_text('\n')
845 937
846 938 # Write the prompt.
847 939 if prompt is None:
848 940 if self._prompt_html is None:
849 self.appendPlainText(self._prompt)
941 self._append_plain_text(self._prompt)
850 942 else:
851 self.appendHtml(self._prompt_html)
943 self._append_html(self._prompt_html)
852 944 else:
853 945 if html:
854 946 self._prompt = self._append_html_fetching_plain_text(prompt)
855 947 self._prompt_html = prompt
856 948 else:
857 self.appendPlainText(prompt)
949 self._append_plain_text(prompt)
858 950 self._prompt = prompt
859 951 self._prompt_html = None
860 952
@@ -865,30 +957,13 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
865 957 """ Writes a new continuation prompt at the end of the buffer.
866 958 """
867 959 if self._continuation_prompt_html is None:
868 self.appendPlainText(self._continuation_prompt)
960 self._append_plain_text(self._continuation_prompt)
869 961 else:
870 962 self._continuation_prompt = self._append_html_fetching_plain_text(
871 963 self._continuation_prompt_html)
872 964
873 965 self._prompt_started()
874 966
875 def _in_buffer(self, position):
876 """ Returns whether the given position is inside the editing region.
877 """
878 return position >= self._prompt_pos
879
880 def _keep_cursor_in_buffer(self):
881 """ Ensures that the cursor is inside the editing region. Returns
882 whether the cursor was moved.
883 """
884 cursor = self.textCursor()
885 if cursor.position() < self._prompt_pos:
886 cursor.movePosition(QtGui.QTextCursor.End)
887 self.setTextCursor(cursor)
888 return True
889 else:
890 return False
891
892 967
893 968 class HistoryConsoleWidget(ConsoleWidget):
894 969 """ A ConsoleWidget that keeps a history of the commands that have been
@@ -896,12 +971,11 b' class HistoryConsoleWidget(ConsoleWidget):'
896 971 """
897 972
898 973 #---------------------------------------------------------------------------
899 # 'QObject' interface
974 # 'object' interface
900 975 #---------------------------------------------------------------------------
901 976
902 def __init__(self, parent=None):
903 super(HistoryConsoleWidget, self).__init__(parent)
904
977 def __init__(self, *args, **kw):
978 super(HistoryConsoleWidget, self).__init__(*args, **kw)
905 979 self._history = []
906 980 self._history_index = 0
907 981
@@ -933,13 +1007,13 b' class HistoryConsoleWidget(ConsoleWidget):'
933 1007 processing the event.
934 1008 """
935 1009 prompt_cursor = self._get_prompt_cursor()
936 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
1010 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
937 1011 self.history_previous()
938 1012
939 1013 # Go to the first line of prompt for seemless history scrolling.
940 1014 cursor = self._get_prompt_cursor()
941 1015 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
942 self.setTextCursor(cursor)
1016 self._set_cursor(cursor)
943 1017
944 1018 return False
945 1019 return True
@@ -949,7 +1023,7 b' class HistoryConsoleWidget(ConsoleWidget):'
949 1023 processing the event.
950 1024 """
951 1025 end_cursor = self._get_end_cursor()
952 if self.textCursor().blockNumber() == end_cursor.blockNumber():
1026 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
953 1027 self.history_next()
954 1028 return False
955 1029 return True
@@ -21,7 +21,7 b' class FrontendHighlighter(PygmentsHighlighter):'
21 21 """
22 22
23 23 def __init__(self, frontend):
24 super(FrontendHighlighter, self).__init__(frontend.document())
24 super(FrontendHighlighter, self).__init__(frontend._control.document())
25 25 self._current_offset = 0
26 26 self._frontend = frontend
27 27 self.highlighting_on = False
@@ -68,14 +68,14 b' class FrontendWidget(HistoryConsoleWidget):'
68 68 executed = QtCore.pyqtSignal(object)
69 69
70 70 #---------------------------------------------------------------------------
71 # 'QObject' interface
71 # 'object' interface
72 72 #---------------------------------------------------------------------------
73 73
74 def __init__(self, parent=None):
75 super(FrontendWidget, self).__init__(parent)
74 def __init__(self, *args, **kw):
75 super(FrontendWidget, self).__init__(*args, **kw)
76 76
77 77 # FrontendWidget protected variables.
78 self._call_tip_widget = CallTipWidget(self)
78 self._call_tip_widget = CallTipWidget(self._control)
79 79 self._completion_lexer = CompletionLexer(PythonLexer())
80 80 self._hidden = True
81 81 self._highlighter = FrontendHighlighter(self)
@@ -86,7 +86,9 b' class FrontendWidget(HistoryConsoleWidget):'
86 86 self.tab_width = 4
87 87 self._set_continuation_prompt('... ')
88 88
89 self.document().contentsChange.connect(self._document_contents_change)
89 # Connect signal handlers.
90 document = self._control.document()
91 document.contentsChange.connect(self._document_contents_change)
90 92
91 93 #---------------------------------------------------------------------------
92 94 # 'QWidget' interface
@@ -140,8 +142,8 b' class FrontendWidget(HistoryConsoleWidget):'
140 142 if self._get_prompt_cursor().blockNumber() != \
141 143 self._get_end_cursor().blockNumber():
142 144 spaces = self._input_splitter.indent_spaces
143 self.appendPlainText('\t' * (spaces / self.tab_width))
144 self.appendPlainText(' ' * (spaces % self.tab_width))
145 self._append_plain_text('\t' * (spaces / self.tab_width))
146 self._append_plain_text(' ' * (spaces % self.tab_width))
145 147
146 148 def _prompt_finished_hook(self):
147 149 """ Called immediately after a prompt is finished, i.e. when some input
@@ -155,7 +157,7 b' class FrontendWidget(HistoryConsoleWidget):'
155 157 processing the event.
156 158 """
157 159 self._keep_cursor_in_buffer()
158 cursor = self.textCursor()
160 cursor = self._get_cursor()
159 161 return not self._complete()
160 162
161 163 #---------------------------------------------------------------------------
@@ -232,9 +234,9 b' class FrontendWidget(HistoryConsoleWidget):'
232 234 """ Shows a call tip, if appropriate, at the current cursor location.
233 235 """
234 236 # Decide if it makes sense to show a call tip
235 cursor = self.textCursor()
237 cursor = self._get_cursor()
236 238 cursor.movePosition(QtGui.QTextCursor.Left)
237 document = self.document()
239 document = self._control.document()
238 240 if document.characterAt(cursor.position()).toAscii() != '(':
239 241 return False
240 242 context = self._get_context(cursor)
@@ -244,7 +246,7 b' class FrontendWidget(HistoryConsoleWidget):'
244 246 # Send the metadata request to the kernel
245 247 name = '.'.join(context)
246 248 self._calltip_id = self.kernel_manager.xreq_channel.object_info(name)
247 self._calltip_pos = self.textCursor().position()
249 self._calltip_pos = self._get_cursor().position()
248 250 return True
249 251
250 252 def _complete(self):
@@ -259,7 +261,7 b' class FrontendWidget(HistoryConsoleWidget):'
259 261 text = '.'.join(context)
260 262 self._complete_id = self.kernel_manager.xreq_channel.complete(
261 263 text, self.input_buffer_cursor_line, self.input_buffer)
262 self._complete_pos = self.textCursor().position()
264 self._complete_pos = self._get_cursor().position()
263 265 return True
264 266
265 267 def _get_banner(self):
@@ -273,7 +275,7 b' class FrontendWidget(HistoryConsoleWidget):'
273 275 """ Gets the context at the current cursor location.
274 276 """
275 277 if cursor is None:
276 cursor = self.textCursor()
278 cursor = self._get_cursor()
277 279 cursor.movePosition(QtGui.QTextCursor.StartOfLine,
278 280 QtGui.QTextCursor.KeepAnchor)
279 281 text = str(cursor.selection().toPlainText())
@@ -285,8 +287,8 b' class FrontendWidget(HistoryConsoleWidget):'
285 287 if self.kernel_manager.has_kernel:
286 288 self.kernel_manager.signal_kernel(signal.SIGINT)
287 289 else:
288 self.appendPlainText('Kernel process is either remote or '
289 'unspecified. Cannot interrupt.\n')
290 self._append_plain_text('Kernel process is either remote or '
291 'unspecified. Cannot interrupt.\n')
290 292
291 293 def _show_interpreter_prompt(self):
292 294 """ Shows a prompt for the interpreter.
@@ -299,7 +301,7 b' class FrontendWidget(HistoryConsoleWidget):'
299 301 """ Called when the kernel manager has started listening.
300 302 """
301 303 self._reset()
302 self.appendPlainText(self._get_banner())
304 self._append_plain_text(self._get_banner())
303 305 self._show_interpreter_prompt()
304 306
305 307 def _stopped_channels(self):
@@ -315,8 +317,8 b' class FrontendWidget(HistoryConsoleWidget):'
315 317 # Calculate where the cursor should be *after* the change:
316 318 position += added
317 319
318 document = self.document()
319 if position == self.textCursor().position():
320 document = self._control.document()
321 if position == self._get_cursor().position():
320 322 self._call_tip()
321 323
322 324 def _handle_req(self, req):
@@ -336,11 +338,11 b' class FrontendWidget(HistoryConsoleWidget):'
336 338 handler(omsg)
337 339
338 340 def _handle_pyout(self, omsg):
339 self.appendPlainText(omsg['content']['data'] + '\n')
341 self._append_plain_text(omsg['content']['data'] + '\n')
340 342
341 343 def _handle_stream(self, omsg):
342 self.appendPlainText(omsg['content']['data'])
343 self.moveCursor(QtGui.QTextCursor.End)
344 self._append_plain_text(omsg['content']['data'])
345 self._control.moveCursor(QtGui.QTextCursor.End)
344 346
345 347 def _handle_execute_reply(self, reply):
346 348 if self._hidden:
@@ -355,7 +357,7 b' class FrontendWidget(HistoryConsoleWidget):'
355 357 self._handle_execute_error(reply)
356 358 elif status == 'aborted':
357 359 text = "ERROR: ABORTED\n"
358 self.appendPlainText(text)
360 self._append_plain_text(text)
359 361 self._hidden = True
360 362 self._show_interpreter_prompt()
361 363 self.executed.emit(reply)
@@ -363,10 +365,10 b' class FrontendWidget(HistoryConsoleWidget):'
363 365 def _handle_execute_error(self, reply):
364 366 content = reply['content']
365 367 traceback = ''.join(content['traceback'])
366 self.appendPlainText(traceback)
368 self._append_plain_text(traceback)
367 369
368 370 def _handle_complete_reply(self, rep):
369 cursor = self.textCursor()
371 cursor = self._get_cursor()
370 372 if rep['parent_header']['msg_id'] == self._complete_id and \
371 373 cursor.position() == self._complete_pos:
372 374 text = '.'.join(self._get_context())
@@ -374,7 +376,7 b' class FrontendWidget(HistoryConsoleWidget):'
374 376 self._complete_with_items(cursor, rep['content']['matches'])
375 377
376 378 def _handle_object_info_reply(self, rep):
377 cursor = self.textCursor()
379 cursor = self._get_cursor()
378 380 if rep['parent_header']['msg_id'] == self._calltip_id and \
379 381 cursor.position() == self._calltip_pos:
380 382 doc = rep['content']['docstring']
@@ -35,11 +35,11 b' class IPythonWidget(FrontendWidget):'
35 35 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36
37 37 #---------------------------------------------------------------------------
38 # 'QObject' interface
38 # 'object' interface
39 39 #---------------------------------------------------------------------------
40 40
41 def __init__(self, parent=None):
42 super(IPythonWidget, self).__init__(parent)
41 def __init__(self, *args, **kw):
42 super(IPythonWidget, self).__init__(*args, **kw)
43 43
44 44 # Initialize protected variables.
45 45 self._previous_prompt_blocks = []
@@ -108,15 +108,15 b' class IPythonWidget(FrontendWidget):'
108 108 ename_styled = '<span class="error">%s</span>' % ename
109 109 traceback = traceback.replace(ename, ename_styled)
110 110
111 self.appendHtml(traceback)
111 self._append_html(traceback)
112 112
113 113 def _handle_pyout(self, omsg):
114 114 """ Reimplemented for IPython-style "display hook".
115 115 """
116 self.appendHtml(self._make_out_prompt(self._prompt_count))
116 self._append_html(self._make_out_prompt(self._prompt_count))
117 117 self._save_prompt_block()
118 118
119 self.appendPlainText(omsg['content']['data'] + '\n')
119 self._append_plain_text(omsg['content']['data'] + '\n')
120 120
121 121 #---------------------------------------------------------------------------
122 122 # 'IPythonWidget' interface
@@ -144,7 +144,7 b' class IPythonWidget(FrontendWidget):'
144 144 the stylesheet is queried for Pygments style information.
145 145 """
146 146 self.setStyleSheet(stylesheet)
147 self.document().setDefaultStyleSheet(stylesheet)
147 self._control.document().setDefaultStyleSheet(stylesheet)
148 148
149 149 if syntax_style is None:
150 150 self._highlighter.set_style_sheet(stylesheet)
@@ -180,7 +180,7 b' class IPythonWidget(FrontendWidget):'
180 180 """ Assuming a prompt has just been written at the end of the buffer,
181 181 store the QTextBlock that contains it and its length.
182 182 """
183 block = self.document().lastBlock()
183 block = self._control.document().lastBlock()
184 184 self._previous_prompt_blocks.append((block, block.length()))
185 185
186 186
General Comments 0
You need to be logged in to leave comments. Login now