##// END OF EJS Templates
Merge branch 'epatters-qtfrontend' into kernelmanager
Brian Granger -
r2741:e34a2ef9 merge
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (683 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
@@ -64,249 +84,51 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
64 84 # Set a monospaced font.
65 85 self.reset_font()
66 86
67 # 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)
74
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()
79
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)
99
100 #---------------------------------------------------------------------------
101 # 'QWidget' interface
102 #---------------------------------------------------------------------------
103
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.
87 def eventFilter(self, obj, event):
88 """ Reimplemented to ensure a console-like behavior in the underlying
89 text widget.
120 90 """
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)
91 if obj == self._control:
92 etype = event.type()
214 93
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
94 # Disable moving text by drag and drop.
95 if etype == QtCore.QEvent.DragMove:
96 return True
228 97
229 elif key == QtCore.Qt.Key_Backspace and not alt_down:
98 elif etype == QtCore.QEvent.KeyPress:
99 return self._event_filter_keypress(event)
230 100
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()
101 # On Mac OS, it is always unnecessary to override shortcuts, hence
102 # the check below. Users should just use the Control key instead of
103 # the Command key.
104 elif etype == QtCore.QEvent.ShortcutOverride:
105 if sys.platform != 'darwin' and \
106 self._control_key_down(event.modifiers()) and \
107 event.key() in self._shortcuts:
108 event.accept()
109 return False
239 110
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 #--------------------------------------------------------------------------
111 return super(ConsoleWidget, self).eventFilter(obj, event)
263 112
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)
113 #---------------------------------------------------------------------------
114 # 'ConsoleWidget' public interface
115 #---------------------------------------------------------------------------
283 116
284 117 def clear(self, keep_input=False):
285 """ Reimplemented to write a new prompt. If 'keep_input' is set,
118 """ Clear the console, then write a new prompt. If 'keep_input' is set,
286 119 restores the old input buffer when the new prompt is written.
287 120 """
288 QtGui.QPlainTextEdit.clear(self)
121 self._control.clear()
289 122 if keep_input:
290 123 input_buffer = self.input_buffer
291 124 self._show_prompt()
292 125 if keep_input:
293 126 self.input_buffer = input_buffer
294 127
295 def paste(self):
296 """ Reimplemented to ensure that text is pasted in the editing region.
128 def copy(self):
129 """ Copy the current selected text to the clipboard.
297 130 """
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 #---------------------------------------------------------------------------
131 self._control.copy()
310 132
311 133 def execute(self, source=None, hidden=False, interactive=False):
312 134 """ Executes source or the input buffer, possibly prompting for more
@@ -346,7 +168,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
346 168 if source is not None:
347 169 self.input_buffer = source
348 170
349 self.appendPlainText('\n')
171 self._append_plain_text('\n')
350 172 self._executing_input_buffer = self.input_buffer
351 173 self._executing = True
352 174 self._prompt_finished()
@@ -358,7 +180,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
358 180 # The maximum block count is only in effect during execution.
359 181 # This ensures that _prompt_pos does not become invalid due to
360 182 # text truncation.
361 self.setMaximumBlockCount(self.buffer_size)
183 self._control.document().setMaximumBlockCount(self.buffer_size)
362 184 self._execute(real_source, hidden)
363 185 elif hidden:
364 186 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
@@ -393,14 +215,14 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
393 215 # Insert new text with continuation prompts.
394 216 lines = string.splitlines(True)
395 217 if lines:
396 self.appendPlainText(lines[0])
218 self._append_plain_text(lines[0])
397 219 for i in xrange(1, len(lines)):
398 220 if self._continuation_prompt_html is None:
399 self.appendPlainText(self._continuation_prompt)
221 self._append_plain_text(self._continuation_prompt)
400 222 else:
401 self.appendHtml(self._continuation_prompt_html)
402 self.appendPlainText(lines[i])
403 self.moveCursor(QtGui.QTextCursor.End)
223 self._append_html(self._continuation_prompt_html)
224 self._append_plain_text(lines[i])
225 self._control.moveCursor(QtGui.QTextCursor.End)
404 226
405 227 input_buffer = property(_get_input_buffer, _set_input_buffer)
406 228
@@ -410,7 +232,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
410 232 """
411 233 if self._executing:
412 234 return None
413 cursor = self.textCursor()
235 cursor = self._control.textCursor()
414 236 if cursor.position() >= self._prompt_pos:
415 237 text = self._get_block_plain_text(cursor.block())
416 238 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
@@ -425,19 +247,36 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
425 247 def _get_font(self):
426 248 """ The base font being used by the ConsoleWidget.
427 249 """
428 return self.document().defaultFont()
250 return self._control.document().defaultFont()
429 251
430 252 def _set_font(self, font):
431 253 """ Sets the base font for the ConsoleWidget to the specified QFont.
432 254 """
433 255 font_metrics = QtGui.QFontMetrics(font)
434 self.setTabStopWidth(self.tab_width * font_metrics.width(' '))
256 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
435 257
436 258 self._completion_widget.setFont(font)
437 self.document().setDefaultFont(font)
259 self._control.document().setDefaultFont(font)
438 260
439 261 font = property(_get_font, _set_font)
440 262
263 def paste(self):
264 """ Paste the contents of the clipboard into the input region.
265 """
266 self._keep_cursor_in_buffer()
267 self._control.paste()
268
269 def print_(self, printer):
270 """ Print the contents of the ConsoleWidget to the specified QPrinter.
271 """
272 self._control.print_(printer)
273
274 def redo(self):
275 """ Redo the last operation. If there is no operation to redo, nothing
276 happens.
277 """
278 self._control.redo()
279
441 280 def reset_font(self):
442 281 """ Sets the font to the default fixed-width font for this platform.
443 282 """
@@ -451,6 +290,11 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
451 290 font.setStyleHint(QtGui.QFont.TypeWriter)
452 291 self._set_font(font)
453 292
293 def select_all(self):
294 """ Selects all the text in the buffer.
295 """
296 self._control.selectAll()
297
454 298 def _get_tab_width(self):
455 299 """ The width (in terms of space characters) for tab characters.
456 300 """
@@ -460,12 +304,18 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
460 304 """ Sets the width (in terms of space characters) for tab characters.
461 305 """
462 306 font_metrics = QtGui.QFontMetrics(self.font)
463 self.setTabStopWidth(tab_width * font_metrics.width(' '))
307 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
464 308
465 309 self._tab_width = tab_width
466 310
467 311 tab_width = property(_get_tab_width, _set_tab_width)
468
312
313 def undo(self):
314 """ Undo the last operation. If there is no operation to undo, nothing
315 happens.
316 """
317 self._control.undo()
318
469 319 #---------------------------------------------------------------------------
470 320 # 'ConsoleWidget' abstract interface
471 321 #---------------------------------------------------------------------------
@@ -515,28 +365,60 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
515 365 # 'ConsoleWidget' protected interface
516 366 #--------------------------------------------------------------------------
517 367
368 def _append_html(self, html):
369 """ Appends html at the end of the console buffer.
370 """
371 cursor = self._get_end_cursor()
372 self._insert_html(cursor, html)
373
518 374 def _append_html_fetching_plain_text(self, html):
519 375 """ Appends 'html', then returns the plain text version of it.
520 376 """
521 377 anchor = self._get_end_cursor().position()
522 self.appendHtml(html)
378 self._append_html(html)
523 379 cursor = self._get_end_cursor()
524 380 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
525 381 return str(cursor.selection().toPlainText())
526 382
383 def _append_plain_text(self, text):
384 """ Appends plain text at the end of the console buffer, processing
385 ANSI codes if enabled.
386 """
387 cursor = self._get_end_cursor()
388 if self.ansi_codes:
389 for substring in self._ansi_processor.split_string(text):
390 format = self._ansi_processor.get_format()
391 cursor.insertText(substring, format)
392 else:
393 cursor.insertText(text)
394
527 395 def _append_plain_text_keeping_prompt(self, text):
528 396 """ Writes 'text' after the current prompt, then restores the old prompt
529 397 with its old input buffer.
530 398 """
531 399 input_buffer = self.input_buffer
532 self.appendPlainText('\n')
400 self._append_plain_text('\n')
533 401 self._prompt_finished()
534 402
535 self.appendPlainText(text)
403 self._append_plain_text(text)
536 404 self._show_prompt()
537 405 self.input_buffer = input_buffer
538 406
539 def _control_down(self, modifiers):
407 def _complete_with_items(self, cursor, items):
408 """ Performs completion with 'items' at the specified cursor location.
409 """
410 if len(items) == 1:
411 cursor.setPosition(self._control.textCursor().position(),
412 QtGui.QTextCursor.KeepAnchor)
413 cursor.insertText(items[0])
414 elif len(items) > 1:
415 if self.gui_completion:
416 self._completion_widget.show_items(cursor, items)
417 else:
418 text = self._format_as_columns(items)
419 self._append_plain_text_keeping_prompt(text)
420
421 def _control_key_down(self, modifiers):
540 422 """ Given a KeyboardModifiers flags object, return whether the Control
541 423 key is down (on Mac OS, treat the Command key as a synonym for
542 424 Control).
@@ -549,20 +431,175 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
549 431 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
550 432
551 433 return down
552
553 def _complete_with_items(self, cursor, items):
554 """ Performs completion with 'items' at the specified cursor location.
434
435 def _create_control(self, kind):
436 """ Creates and sets the underlying text widget.
555 437 """
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)
438 layout = QtGui.QVBoxLayout(self)
439 layout.setMargin(0)
440 if kind == 'plain':
441 control = QtGui.QPlainTextEdit()
442 elif kind == 'rich':
443 control = QtGui.QTextEdit()
444 else:
445 raise ValueError("Kind %s unknown." % repr(kind))
446 layout.addWidget(control)
447
448 control.installEventFilter(self)
449 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
450 control.customContextMenuRequested.connect(self._show_context_menu)
451 control.copyAvailable.connect(self.copy_available)
452 control.redoAvailable.connect(self.redo_available)
453 control.undoAvailable.connect(self.undo_available)
454
455 return control
456
457 def _event_filter_keypress(self, event):
458 """ Filter key events for the underlying text widget to create a
459 console-like interface.
460 """
461 key = event.key()
462 ctrl_down = self._control_key_down(event.modifiers())
463
464 # If the key is remapped, return immediately.
465 if ctrl_down and key in self._ctrl_down_remap:
466 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
467 self._ctrl_down_remap[key],
468 QtCore.Qt.NoModifier)
469 QtGui.qApp.sendEvent(self._control, new_event)
470 return True
471
472 # If the completion widget accepts the key press, return immediately.
473 if self._completion_widget.isVisible():
474 self._completion_widget.keyPressEvent(event)
475 if event.isAccepted():
476 return True
477
478 # Otherwise, proceed normally and do not return early.
479 intercepted = False
480 cursor = self._control.textCursor()
481 position = cursor.position()
482 alt_down = event.modifiers() & QtCore.Qt.AltModifier
483 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
484
485 if event.matches(QtGui.QKeySequence.Paste):
486 # Call our paste instead of the underlying text widget's.
487 self.paste()
488 intercepted = True
489
490 elif ctrl_down:
491 if key == QtCore.Qt.Key_K:
492 if self._in_buffer(position):
493 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
494 QtGui.QTextCursor.KeepAnchor)
495 cursor.removeSelectedText()
496 intercepted = True
497
498 elif key == QtCore.Qt.Key_X:
499 intercepted = True
500
501 elif key == QtCore.Qt.Key_Y:
502 self.paste()
503 intercepted = True
504
505 elif alt_down:
506 if key == QtCore.Qt.Key_B:
507 self._set_cursor(self._get_word_start_cursor(position))
508 intercepted = True
509
510 elif key == QtCore.Qt.Key_F:
511 self._set_cursor(self._get_word_end_cursor(position))
512 intercepted = True
513
514 elif key == QtCore.Qt.Key_Backspace:
515 cursor = self._get_word_start_cursor(position)
516 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
517 cursor.removeSelectedText()
518 intercepted = True
519
520 elif key == QtCore.Qt.Key_D:
521 cursor = self._get_word_end_cursor(position)
522 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
523 cursor.removeSelectedText()
524 intercepted = True
525
526 else:
527 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
528 if self._reading:
529 self._append_plain_text('\n')
530 self._reading = False
531 if self._reading_callback:
532 self._reading_callback()
533 elif not self._executing:
534 self.execute(interactive=True)
535 intercepted = True
536
537 elif key == QtCore.Qt.Key_Up:
538 if self._reading or not self._up_pressed():
539 intercepted = True
540 else:
541 prompt_line = self._get_prompt_cursor().blockNumber()
542 intercepted = cursor.blockNumber() <= prompt_line
543
544 elif key == QtCore.Qt.Key_Down:
545 if self._reading or not self._down_pressed():
546 intercepted = True
547 else:
548 end_line = self._get_end_cursor().blockNumber()
549 intercepted = cursor.blockNumber() == end_line
550
551 elif key == QtCore.Qt.Key_Tab:
552 if self._reading:
553 intercepted = False
554 else:
555 intercepted = not self._tab_pressed()
556
557 elif key == QtCore.Qt.Key_Left:
558 intercepted = not self._in_buffer(position - 1)
559
560 elif key == QtCore.Qt.Key_Home:
561 cursor.movePosition(QtGui.QTextCursor.StartOfLine)
562 start_line = cursor.blockNumber()
563 if start_line == self._get_prompt_cursor().blockNumber():
564 start_pos = self._prompt_pos
565 else:
566 start_pos = cursor.position()
567 start_pos += len(self._continuation_prompt)
568 if shift_down and self._in_buffer(position):
569 self._set_selection(position, start_pos)
570 else:
571 self._set_position(start_pos)
572 intercepted = True
573
574 elif key == QtCore.Qt.Key_Backspace and not alt_down:
575
576 # Line deletion (remove continuation prompt)
577 len_prompt = len(self._continuation_prompt)
578 if not self._reading and \
579 cursor.columnNumber() == len_prompt and \
580 position != self._prompt_pos:
581 cursor.setPosition(position - len_prompt,
582 QtGui.QTextCursor.KeepAnchor)
583 cursor.removeSelectedText()
584
585 # Regular backwards deletion
586 else:
587 anchor = cursor.anchor()
588 if anchor == position:
589 intercepted = not self._in_buffer(position - 1)
590 else:
591 intercepted = not self._in_buffer(min(anchor, position))
592
593 elif key == QtCore.Qt.Key_Delete:
594 anchor = cursor.anchor()
595 intercepted = not self._in_buffer(min(anchor, position))
596
597 # Don't move the cursor if control is down to allow copy-paste using
598 # the keyboard in any part of the buffer.
599 if not ctrl_down:
600 self._keep_cursor_in_buffer()
601
602 return intercepted
566 603
567 604 def _format_as_columns(self, items, separator=' '):
568 605 """ Transform a list of strings into a single string with columns.
@@ -639,18 +676,23 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
639 676 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
640 677 QtGui.QTextCursor.KeepAnchor)
641 678 return str(cursor.selection().toPlainText())
679
680 def _get_cursor(self):
681 """ Convenience method that returns a cursor for the current position.
682 """
683 return self._control.textCursor()
642 684
643 685 def _get_end_cursor(self):
644 686 """ Convenience method that returns a cursor for the last character.
645 687 """
646 cursor = self.textCursor()
688 cursor = self._control.textCursor()
647 689 cursor.movePosition(QtGui.QTextCursor.End)
648 690 return cursor
649 691
650 692 def _get_prompt_cursor(self):
651 693 """ Convenience method that returns a cursor for the prompt position.
652 694 """
653 cursor = self.textCursor()
695 cursor = self._control.textCursor()
654 696 cursor.setPosition(self._prompt_pos)
655 697 return cursor
656 698
@@ -658,7 +700,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
658 700 """ Convenience method that returns a cursor with text selected between
659 701 the positions 'start' and 'end'.
660 702 """
661 cursor = self.textCursor()
703 cursor = self._control.textCursor()
662 704 cursor.setPosition(start)
663 705 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
664 706 return cursor
@@ -668,7 +710,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
668 710 sequence of non-word characters precedes the first word, skip over
669 711 them. (This emulates the behavior of bash, emacs, etc.)
670 712 """
671 document = self.document()
713 document = self._control.document()
672 714 position -= 1
673 715 while self._in_buffer(position) and \
674 716 not document.characterAt(position).isLetterOrNumber():
@@ -676,7 +718,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
676 718 while self._in_buffer(position) and \
677 719 document.characterAt(position).isLetterOrNumber():
678 720 position -= 1
679 cursor = self.textCursor()
721 cursor = self._control.textCursor()
680 722 cursor.setPosition(position + 1)
681 723 return cursor
682 724
@@ -685,7 +727,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
685 727 sequence of non-word characters precedes the first word, skip over
686 728 them. (This emulates the behavior of bash, emacs, etc.)
687 729 """
688 document = self.document()
730 document = self._control.document()
689 731 end = self._get_end_cursor().position()
690 732 while position < end and \
691 733 not document.characterAt(position).isLetterOrNumber():
@@ -693,7 +735,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
693 735 while position < end and \
694 736 document.characterAt(position).isLetterOrNumber():
695 737 position += 1
696 cursor = self.textCursor()
738 cursor = self._control.textCursor()
697 739 cursor.setPosition(position)
698 740 return cursor
699 741
@@ -718,17 +760,33 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
718 760 cursor.movePosition(QtGui.QTextCursor.Right)
719 761 cursor.insertText(' ', QtGui.QTextCharFormat())
720 762
763 def _in_buffer(self, position):
764 """ Returns whether the given position is inside the editing region.
765 """
766 return position >= self._prompt_pos
767
768 def _keep_cursor_in_buffer(self):
769 """ Ensures that the cursor is inside the editing region. Returns
770 whether the cursor was moved.
771 """
772 cursor = self._control.textCursor()
773 if cursor.position() < self._prompt_pos:
774 cursor.movePosition(QtGui.QTextCursor.End)
775 self._control.setTextCursor(cursor)
776 return True
777 else:
778 return False
779
721 780 def _prompt_started(self):
722 781 """ Called immediately after a new prompt is displayed.
723 782 """
724 783 # Temporarily disable the maximum block count to permit undo/redo and
725 784 # to ensure that the prompt position does not change due to truncation.
726 self.setMaximumBlockCount(0)
727 self.setUndoRedoEnabled(True)
785 self._control.document().setMaximumBlockCount(0)
786 self._control.setUndoRedoEnabled(True)
728 787
729 self.setReadOnly(False)
730 self.moveCursor(QtGui.QTextCursor.End)
731 self.centerCursor()
788 self._control.setReadOnly(False)
789 self._control.moveCursor(QtGui.QTextCursor.End)
732 790
733 791 self._executing = False
734 792 self._prompt_started_hook()
@@ -737,8 +795,8 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
737 795 """ Called immediately after a prompt is finished, i.e. when some input
738 796 will be processed and a new prompt displayed.
739 797 """
740 self.setUndoRedoEnabled(False)
741 self.setReadOnly(True)
798 self._control.setUndoRedoEnabled(False)
799 self._control.setReadOnly(True)
742 800 self._prompt_finished_hook()
743 801
744 802 def _readline(self, prompt='', callback=None):
@@ -783,7 +841,7 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
783 841 def _reset(self):
784 842 """ Clears the console and resets internal state variables.
785 843 """
786 QtGui.QPlainTextEdit.clear(self)
844 self._control.clear()
787 845 self._executing = self._reading = False
788 846
789 847 def _set_continuation_prompt(self, prompt, html=False):
@@ -804,18 +862,47 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
804 862 else:
805 863 self._continuation_prompt = prompt
806 864 self._continuation_prompt_html = None
865
866 def _set_cursor(self, cursor):
867 """ Convenience method to set the current cursor.
868 """
869 self._control.setTextCursor(cursor)
807 870
808 871 def _set_position(self, position):
809 872 """ Convenience method to set the position of the cursor.
810 873 """
811 cursor = self.textCursor()
874 cursor = self._control.textCursor()
812 875 cursor.setPosition(position)
813 self.setTextCursor(cursor)
876 self._control.setTextCursor(cursor)
814 877
815 878 def _set_selection(self, start, end):
816 879 """ Convenience method to set the current selected text.
817 880 """
818 self.setTextCursor(self._get_selection_cursor(start, end))
881 self._control.setTextCursor(self._get_selection_cursor(start, end))
882
883 def _show_context_menu(self, pos):
884 """ Shows a context menu at the given QPoint (in widget coordinates).
885 """
886 menu = QtGui.QMenu()
887
888 copy_action = QtGui.QAction('Copy', menu)
889 copy_action.triggered.connect(self.copy)
890 copy_action.setEnabled(self._get_cursor().hasSelection())
891 copy_action.setShortcut(QtGui.QKeySequence.Copy)
892 menu.addAction(copy_action)
893
894 paste_action = QtGui.QAction('Paste', menu)
895 paste_action.triggered.connect(self.paste)
896 paste_action.setEnabled(self._control.canPaste())
897 paste_action.setShortcut(QtGui.QKeySequence.Paste)
898 menu.addAction(paste_action)
899 menu.addSeparator()
900
901 select_all_action = QtGui.QAction('Select All', menu)
902 select_all_action.triggered.connect(self.select_all)
903 menu.addAction(select_all_action)
904
905 menu.exec_(self._control.mapToGlobal(pos))
819 906
820 907 def _show_prompt(self, prompt=None, html=False, newline=True):
821 908 """ Writes a new prompt at the end of the buffer.
@@ -841,20 +928,20 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
841 928 cursor.movePosition(QtGui.QTextCursor.Left,
842 929 QtGui.QTextCursor.KeepAnchor)
843 930 if str(cursor.selection().toPlainText()) != '\n':
844 self.appendPlainText('\n')
931 self._append_plain_text('\n')
845 932
846 933 # Write the prompt.
847 934 if prompt is None:
848 935 if self._prompt_html is None:
849 self.appendPlainText(self._prompt)
936 self._append_plain_text(self._prompt)
850 937 else:
851 self.appendHtml(self._prompt_html)
938 self._append_html(self._prompt_html)
852 939 else:
853 940 if html:
854 941 self._prompt = self._append_html_fetching_plain_text(prompt)
855 942 self._prompt_html = prompt
856 943 else:
857 self.appendPlainText(prompt)
944 self._append_plain_text(prompt)
858 945 self._prompt = prompt
859 946 self._prompt_html = None
860 947
@@ -865,30 +952,13 b' class ConsoleWidget(QtGui.QPlainTextEdit):'
865 952 """ Writes a new continuation prompt at the end of the buffer.
866 953 """
867 954 if self._continuation_prompt_html is None:
868 self.appendPlainText(self._continuation_prompt)
955 self._append_plain_text(self._continuation_prompt)
869 956 else:
870 957 self._continuation_prompt = self._append_html_fetching_plain_text(
871 958 self._continuation_prompt_html)
872 959
873 960 self._prompt_started()
874 961
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 962
893 963 class HistoryConsoleWidget(ConsoleWidget):
894 964 """ A ConsoleWidget that keeps a history of the commands that have been
@@ -896,12 +966,11 b' class HistoryConsoleWidget(ConsoleWidget):'
896 966 """
897 967
898 968 #---------------------------------------------------------------------------
899 # 'QObject' interface
969 # 'object' interface
900 970 #---------------------------------------------------------------------------
901 971
902 def __init__(self, parent=None):
903 super(HistoryConsoleWidget, self).__init__(parent)
904
972 def __init__(self, *args, **kw):
973 super(HistoryConsoleWidget, self).__init__(*args, **kw)
905 974 self._history = []
906 975 self._history_index = 0
907 976
@@ -933,13 +1002,13 b' class HistoryConsoleWidget(ConsoleWidget):'
933 1002 processing the event.
934 1003 """
935 1004 prompt_cursor = self._get_prompt_cursor()
936 if self.textCursor().blockNumber() == prompt_cursor.blockNumber():
1005 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
937 1006 self.history_previous()
938 1007
939 1008 # Go to the first line of prompt for seemless history scrolling.
940 1009 cursor = self._get_prompt_cursor()
941 1010 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
942 self.setTextCursor(cursor)
1011 self._set_cursor(cursor)
943 1012
944 1013 return False
945 1014 return True
@@ -949,7 +1018,7 b' class HistoryConsoleWidget(ConsoleWidget):'
949 1018 processing the event.
950 1019 """
951 1020 end_cursor = self._get_end_cursor()
952 if self.textCursor().blockNumber() == end_cursor.blockNumber():
1021 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
953 1022 self.history_next()
954 1023 return False
955 1024 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