##// END OF EJS Templates
* Fixed bug with terminal-style tab completion for multi-line input....
epatters -
Show More
@@ -1,1498 +1,1502 b''
1 1 # Standard library imports
2 2 from os.path import commonprefix
3 3 import re
4 4 import sys
5 5 from textwrap import dedent
6 6
7 7 # System library imports
8 8 from PyQt4 import QtCore, QtGui
9 9
10 10 # Local imports
11 11 from IPython.config.configurable import Configurable
12 12 from IPython.frontend.qt.util import MetaQObjectHasTraits
13 13 from IPython.utils.traitlets import Bool, Enum, Int
14 14 from ansi_code_processor import QtAnsiCodeProcessor
15 15 from completion_widget import CompletionWidget
16 16
17 17
18 18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
19 19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
20 20 """
21 21 # Prevents text from being moved by drag and drop. Note that is not, for
22 22 # some reason, sufficient to catch drag events in the ConsoleWidget's
23 23 # event filter.
24 24 def dragEnterEvent(self, event): pass
25 25 def dragLeaveEvent(self, event): pass
26 26 def dragMoveEvent(self, event): pass
27 27 def dropEvent(self, event): pass
28 28
29 29 class ConsoleTextEdit(QtGui.QTextEdit):
30 30 """ A QTextEdit suitable for use with ConsoleWidget.
31 31 """
32 32 # See above.
33 33 def dragEnterEvent(self, event): pass
34 34 def dragLeaveEvent(self, event): pass
35 35 def dragMoveEvent(self, event): pass
36 36 def dropEvent(self, event): pass
37 37
38 38
39 39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 40 """ An abstract base class for console-type widgets. This class has
41 41 functionality for:
42 42
43 43 * Maintaining a prompt and editing region
44 44 * Providing the traditional Unix-style console keyboard shortcuts
45 45 * Performing tab completion
46 46 * Paging text
47 47 * Handling ANSI escape codes
48 48
49 49 ConsoleWidget also provides a number of utility methods that will be
50 50 convenient to implementors of a console-style widget.
51 51 """
52 52 __metaclass__ = MetaQObjectHasTraits
53 53
54 54 # Whether to process ANSI escape codes.
55 55 ansi_codes = Bool(True, config=True)
56 56
57 57 # The maximum number of lines of text before truncation. Specifying a
58 58 # non-positive number disables text truncation (not recommended).
59 59 buffer_size = Int(500, config=True)
60 60
61 61 # Whether to use a list widget or plain text output for tab completion.
62 62 gui_completion = Bool(False, config=True)
63 63
64 64 # The type of underlying text widget to use. Valid values are 'plain', which
65 65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
66 66 # NOTE: this value can only be specified during initialization.
67 67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
68 68
69 69 # The type of paging to use. Valid values are:
70 70 # 'inside' : The widget pages like a traditional terminal pager.
71 71 # 'hsplit' : When paging is requested, the widget is split
72 72 # horizontally. The top pane contains the console, and the
73 73 # bottom pane contains the paged text.
74 74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
75 75 # 'custom' : No action is taken by the widget beyond emitting a
76 76 # 'custom_page_requested(str)' signal.
77 77 # 'none' : The text is written directly to the console.
78 78 # NOTE: this value can only be specified during initialization.
79 79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
80 80 default_value='inside', config=True)
81 81
82 82 # Whether to override ShortcutEvents for the keybindings defined by this
83 83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
84 84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
85 85 override_shortcuts = Bool(False)
86 86
87 87 # Signals that indicate ConsoleWidget state.
88 88 copy_available = QtCore.pyqtSignal(bool)
89 89 redo_available = QtCore.pyqtSignal(bool)
90 90 undo_available = QtCore.pyqtSignal(bool)
91 91
92 92 # Signal emitted when paging is needed and the paging style has been
93 93 # specified as 'custom'.
94 94 custom_page_requested = QtCore.pyqtSignal(object)
95 95
96 96 # Protected class variables.
97 97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
98 98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
99 99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
100 100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
101 101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
102 102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
103 103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
104 104 _shortcuts = set(_ctrl_down_remap.keys() +
105 105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V, QtCore.Qt.Key_O ])
106 106
107 107 #---------------------------------------------------------------------------
108 108 # 'QObject' interface
109 109 #---------------------------------------------------------------------------
110 110
111 111 def __init__(self, parent=None, **kw):
112 112 """ Create a ConsoleWidget.
113 113
114 114 Parameters:
115 115 -----------
116 116 parent : QWidget, optional [default None]
117 117 The parent for this widget.
118 118 """
119 119 QtGui.QWidget.__init__(self, parent)
120 120 Configurable.__init__(self, **kw)
121 121
122 122 # Create the layout and underlying text widget.
123 123 layout = QtGui.QStackedLayout(self)
124 124 layout.setContentsMargins(0, 0, 0, 0)
125 125 self._control = self._create_control()
126 126 self._page_control = None
127 127 self._splitter = None
128 128 if self.paging in ('hsplit', 'vsplit'):
129 129 self._splitter = QtGui.QSplitter()
130 130 if self.paging == 'hsplit':
131 131 self._splitter.setOrientation(QtCore.Qt.Horizontal)
132 132 else:
133 133 self._splitter.setOrientation(QtCore.Qt.Vertical)
134 134 self._splitter.addWidget(self._control)
135 135 layout.addWidget(self._splitter)
136 136 else:
137 137 layout.addWidget(self._control)
138 138
139 139 # Create the paging widget, if necessary.
140 140 if self.paging in ('inside', 'hsplit', 'vsplit'):
141 141 self._page_control = self._create_page_control()
142 142 if self._splitter:
143 143 self._page_control.hide()
144 144 self._splitter.addWidget(self._page_control)
145 145 else:
146 146 layout.addWidget(self._page_control)
147 147
148 148 # Initialize protected variables. Some variables contain useful state
149 149 # information for subclasses; they should be considered read-only.
150 150 self._ansi_processor = QtAnsiCodeProcessor()
151 151 self._completion_widget = CompletionWidget(self._control)
152 152 self._continuation_prompt = '> '
153 153 self._continuation_prompt_html = None
154 154 self._executing = False
155 155 self._prompt = ''
156 156 self._prompt_html = None
157 157 self._prompt_pos = 0
158 158 self._prompt_sep = ''
159 159 self._reading = False
160 160 self._reading_callback = None
161 161 self._tab_width = 8
162 162 self._text_completing_pos = 0
163 163
164 164 # Set a monospaced font.
165 165 self.reset_font()
166 166
167 167 def eventFilter(self, obj, event):
168 168 """ Reimplemented to ensure a console-like behavior in the underlying
169 169 text widgets.
170 170 """
171 171 etype = event.type()
172 172 if etype == QtCore.QEvent.KeyPress:
173 173
174 174 # Re-map keys for all filtered widgets.
175 175 key = event.key()
176 176 if self._control_key_down(event.modifiers()) and \
177 177 key in self._ctrl_down_remap:
178 178 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
179 179 self._ctrl_down_remap[key],
180 180 QtCore.Qt.NoModifier)
181 181 QtGui.qApp.sendEvent(obj, new_event)
182 182 return True
183 183
184 184 elif obj == self._control:
185 185 return self._event_filter_console_keypress(event)
186 186
187 187 elif obj == self._page_control:
188 188 return self._event_filter_page_keypress(event)
189 189
190 190 # Override shortucts for all filtered widgets. Note that on Mac OS it is
191 191 # always unnecessary to override shortcuts, hence the check below (users
192 192 # should just use the Control key instead of the Command key).
193 193 elif etype == QtCore.QEvent.ShortcutOverride and \
194 194 sys.platform != 'darwin' and \
195 195 self._control_key_down(event.modifiers()) and \
196 196 event.key() in self._shortcuts:
197 197 event.accept()
198 198 return False
199 199
200 200 return super(ConsoleWidget, self).eventFilter(obj, event)
201 201
202 202 #---------------------------------------------------------------------------
203 203 # 'QWidget' interface
204 204 #---------------------------------------------------------------------------
205 205
206 206 def sizeHint(self):
207 207 """ Reimplemented to suggest a size that is 80 characters wide and
208 208 25 lines high.
209 209 """
210 210 font_metrics = QtGui.QFontMetrics(self.font)
211 211 margin = (self._control.frameWidth() +
212 212 self._control.document().documentMargin()) * 2
213 213 style = self.style()
214 214 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
215 215
216 216 # Despite my best efforts to take the various margins into account, the
217 217 # width is still coming out a bit too small, so we include a fudge
218 218 # factor of one character here.
219 219 width = font_metrics.maxWidth() * 81 + margin
220 220 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
221 221 if self.paging == 'hsplit':
222 222 width = width * 2 + splitwidth
223 223
224 224 height = font_metrics.height() * 25 + margin
225 225 if self.paging == 'vsplit':
226 226 height = height * 2 + splitwidth
227 227
228 228 return QtCore.QSize(width, height)
229 229
230 230 #---------------------------------------------------------------------------
231 231 # 'ConsoleWidget' public interface
232 232 #---------------------------------------------------------------------------
233 233
234 234 def can_paste(self):
235 235 """ Returns whether text can be pasted from the clipboard.
236 236 """
237 237 # Only accept text that can be ASCII encoded.
238 238 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
239 239 text = QtGui.QApplication.clipboard().text()
240 240 if not text.isEmpty():
241 241 try:
242 242 str(text)
243 243 return True
244 244 except UnicodeEncodeError:
245 245 pass
246 246 return False
247 247
248 248 def clear(self, keep_input=True):
249 249 """ Clear the console, then write a new prompt. If 'keep_input' is set,
250 250 restores the old input buffer when the new prompt is written.
251 251 """
252 252 if keep_input:
253 253 input_buffer = self.input_buffer
254 254 self._control.clear()
255 255 self._show_prompt()
256 256 if keep_input:
257 257 self.input_buffer = input_buffer
258 258
259 259 def copy(self):
260 260 """ Copy the current selected text to the clipboard.
261 261 """
262 262 self._control.copy()
263 263
264 264 def execute(self, source=None, hidden=False, interactive=False):
265 265 """ Executes source or the input buffer, possibly prompting for more
266 266 input.
267 267
268 268 Parameters:
269 269 -----------
270 270 source : str, optional
271 271
272 272 The source to execute. If not specified, the input buffer will be
273 273 used. If specified and 'hidden' is False, the input buffer will be
274 274 replaced with the source before execution.
275 275
276 276 hidden : bool, optional (default False)
277 277
278 278 If set, no output will be shown and the prompt will not be modified.
279 279 In other words, it will be completely invisible to the user that
280 280 an execution has occurred.
281 281
282 282 interactive : bool, optional (default False)
283 283
284 284 Whether the console is to treat the source as having been manually
285 285 entered by the user. The effect of this parameter depends on the
286 286 subclass implementation.
287 287
288 288 Raises:
289 289 -------
290 290 RuntimeError
291 291 If incomplete input is given and 'hidden' is True. In this case,
292 292 it is not possible to prompt for more input.
293 293
294 294 Returns:
295 295 --------
296 296 A boolean indicating whether the source was executed.
297 297 """
298 298 # WARNING: The order in which things happen here is very particular, in
299 299 # large part because our syntax highlighting is fragile. If you change
300 300 # something, test carefully!
301 301
302 302 # Decide what to execute.
303 303 if source is None:
304 304 source = self.input_buffer
305 305 if not hidden:
306 306 # A newline is appended later, but it should be considered part
307 307 # of the input buffer.
308 308 source += '\n'
309 309 elif not hidden:
310 310 self.input_buffer = source
311 311
312 312 # Execute the source or show a continuation prompt if it is incomplete.
313 313 complete = self._is_complete(source, interactive)
314 314 if hidden:
315 315 if complete:
316 316 self._execute(source, hidden)
317 317 else:
318 318 error = 'Incomplete noninteractive input: "%s"'
319 319 raise RuntimeError(error % source)
320 320 else:
321 321 if complete:
322 322 self._append_plain_text('\n')
323 323 self._executing_input_buffer = self.input_buffer
324 324 self._executing = True
325 325 self._prompt_finished()
326 326
327 327 # The maximum block count is only in effect during execution.
328 328 # This ensures that _prompt_pos does not become invalid due to
329 329 # text truncation.
330 330 self._control.document().setMaximumBlockCount(self.buffer_size)
331 331
332 332 # Setting a positive maximum block count will automatically
333 333 # disable the undo/redo history, but just to be safe:
334 334 self._control.setUndoRedoEnabled(False)
335 335
336 336 self._execute(source, hidden)
337 337
338 338 else:
339 339 # Do this inside an edit block so continuation prompts are
340 340 # removed seamlessly via undo/redo.
341 341 cursor = self._get_end_cursor()
342 342 cursor.beginEditBlock()
343 343 cursor.insertText('\n')
344 344 self._insert_continuation_prompt(cursor)
345 345 cursor.endEditBlock()
346 346
347 347 # Do not do this inside the edit block. It works as expected
348 348 # when using a QPlainTextEdit control, but does not have an
349 349 # effect when using a QTextEdit. I believe this is a Qt bug.
350 350 self._control.moveCursor(QtGui.QTextCursor.End)
351 351
352 352 return complete
353 353
354 354 def _get_input_buffer(self):
355 355 """ The text that the user has entered entered at the current prompt.
356 356 """
357 357 # If we're executing, the input buffer may not even exist anymore due to
358 358 # the limit imposed by 'buffer_size'. Therefore, we store it.
359 359 if self._executing:
360 360 return self._executing_input_buffer
361 361
362 362 cursor = self._get_end_cursor()
363 363 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
364 364 input_buffer = str(cursor.selection().toPlainText())
365 365
366 366 # Strip out continuation prompts.
367 367 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
368 368
369 369 def _set_input_buffer(self, string):
370 370 """ Replaces the text in the input buffer with 'string'.
371 371 """
372 372 # For now, it is an error to modify the input buffer during execution.
373 373 if self._executing:
374 374 raise RuntimeError("Cannot change input buffer during execution.")
375 375
376 376 # Remove old text.
377 377 cursor = self._get_end_cursor()
378 378 cursor.beginEditBlock()
379 379 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
380 380 cursor.removeSelectedText()
381 381
382 382 # Insert new text with continuation prompts.
383 383 lines = string.splitlines(True)
384 384 if lines:
385 385 self._append_plain_text(lines[0])
386 386 for i in xrange(1, len(lines)):
387 387 if self._continuation_prompt_html is None:
388 388 self._append_plain_text(self._continuation_prompt)
389 389 else:
390 390 self._append_html(self._continuation_prompt_html)
391 391 self._append_plain_text(lines[i])
392 392 cursor.endEditBlock()
393 393 self._control.moveCursor(QtGui.QTextCursor.End)
394 394
395 395 input_buffer = property(_get_input_buffer, _set_input_buffer)
396 396
397 397 def _get_font(self):
398 398 """ The base font being used by the ConsoleWidget.
399 399 """
400 400 return self._control.document().defaultFont()
401 401
402 402 def _set_font(self, font):
403 403 """ Sets the base font for the ConsoleWidget to the specified QFont.
404 404 """
405 405 font_metrics = QtGui.QFontMetrics(font)
406 406 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
407 407
408 408 self._completion_widget.setFont(font)
409 409 self._control.document().setDefaultFont(font)
410 410 if self._page_control:
411 411 self._page_control.document().setDefaultFont(font)
412 412
413 413 font = property(_get_font, _set_font)
414 414
415 415 def paste(self):
416 416 """ Paste the contents of the clipboard into the input region.
417 417 """
418 418 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
419 419 try:
420 420 text = str(QtGui.QApplication.clipboard().text())
421 421 except UnicodeEncodeError:
422 422 pass
423 423 else:
424 424 self._insert_plain_text_into_buffer(dedent(text))
425 425
426 426 def print_(self, printer):
427 427 """ Print the contents of the ConsoleWidget to the specified QPrinter.
428 428 """
429 429 self._control.print_(printer)
430 430
431 431 def redo(self):
432 432 """ Redo the last operation. If there is no operation to redo, nothing
433 433 happens.
434 434 """
435 435 self._control.redo()
436 436
437 437 def reset_font(self):
438 438 """ Sets the font to the default fixed-width font for this platform.
439 439 """
440 440 if sys.platform == 'win32':
441 441 # FIXME: we should test whether Consolas is available and use it
442 442 # first if it is. Consolas ships by default from Vista onwards,
443 443 # it's *vastly* more readable and prettier than Courier, and is
444 444 # often installed even on XP systems. So we should first check for
445 445 # it, and only fallback to Courier if absolutely necessary.
446 446 name = 'Courier'
447 447 elif sys.platform == 'darwin':
448 448 name = 'Monaco'
449 449 else:
450 450 name = 'Monospace'
451 451 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
452 452 font.setStyleHint(QtGui.QFont.TypeWriter)
453 453 self._set_font(font)
454 454
455 455 def select_all(self):
456 456 """ Selects all the text in the buffer.
457 457 """
458 458 self._control.selectAll()
459 459
460 460 def _get_tab_width(self):
461 461 """ The width (in terms of space characters) for tab characters.
462 462 """
463 463 return self._tab_width
464 464
465 465 def _set_tab_width(self, tab_width):
466 466 """ Sets the width (in terms of space characters) for tab characters.
467 467 """
468 468 font_metrics = QtGui.QFontMetrics(self.font)
469 469 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
470 470
471 471 self._tab_width = tab_width
472 472
473 473 tab_width = property(_get_tab_width, _set_tab_width)
474 474
475 475 def undo(self):
476 476 """ Undo the last operation. If there is no operation to undo, nothing
477 477 happens.
478 478 """
479 479 self._control.undo()
480 480
481 481 #---------------------------------------------------------------------------
482 482 # 'ConsoleWidget' abstract interface
483 483 #---------------------------------------------------------------------------
484 484
485 485 def _is_complete(self, source, interactive):
486 486 """ Returns whether 'source' can be executed. When triggered by an
487 487 Enter/Return key press, 'interactive' is True; otherwise, it is
488 488 False.
489 489 """
490 490 raise NotImplementedError
491 491
492 492 def _execute(self, source, hidden):
493 493 """ Execute 'source'. If 'hidden', do not show any output.
494 494 """
495 495 raise NotImplementedError
496 496
497 497 def _prompt_started_hook(self):
498 498 """ Called immediately after a new prompt is displayed.
499 499 """
500 500 pass
501 501
502 502 def _prompt_finished_hook(self):
503 503 """ Called immediately after a prompt is finished, i.e. when some input
504 504 will be processed and a new prompt displayed.
505 505 """
506 506 pass
507 507
508 508 def _up_pressed(self):
509 509 """ Called when the up key is pressed. Returns whether to continue
510 510 processing the event.
511 511 """
512 512 return True
513 513
514 514 def _down_pressed(self):
515 515 """ Called when the down key is pressed. Returns whether to continue
516 516 processing the event.
517 517 """
518 518 return True
519 519
520 520 def _tab_pressed(self):
521 521 """ Called when the tab key is pressed. Returns whether to continue
522 522 processing the event.
523 523 """
524 524 return False
525 525
526 526 #--------------------------------------------------------------------------
527 527 # 'ConsoleWidget' protected interface
528 528 #--------------------------------------------------------------------------
529 529
530 530 def _append_html(self, html):
531 531 """ Appends html at the end of the console buffer.
532 532 """
533 533 cursor = self._get_end_cursor()
534 534 self._insert_html(cursor, html)
535 535
536 536 def _append_html_fetching_plain_text(self, html):
537 537 """ Appends 'html', then returns the plain text version of it.
538 538 """
539 539 cursor = self._get_end_cursor()
540 540 return self._insert_html_fetching_plain_text(cursor, html)
541 541
542 542 def _append_plain_text(self, text):
543 543 """ Appends plain text at the end of the console buffer, processing
544 544 ANSI codes if enabled.
545 545 """
546 546 cursor = self._get_end_cursor()
547 547 self._insert_plain_text(cursor, text)
548 548
549 549 def _append_plain_text_keeping_prompt(self, text):
550 550 """ Writes 'text' after the current prompt, then restores the old prompt
551 551 with its old input buffer.
552 552 """
553 553 input_buffer = self.input_buffer
554 554 self._append_plain_text('\n')
555 555 self._prompt_finished()
556 556
557 557 self._append_plain_text(text)
558 558 self._show_prompt()
559 559 self.input_buffer = input_buffer
560 560
561 561 def _clear_temporary_buffer(self):
562 562 """ Clears the "temporary text" buffer, i.e. all the text following
563 563 the prompt region.
564 564 """
565 565 cursor = self._get_prompt_cursor()
566 found_block = cursor.movePosition(QtGui.QTextCursor.NextBlock)
567 if found_block:
568 while found_block and \
569 cursor.block().text().startsWith(self._continuation_prompt):
570 found_block = cursor.movePosition(QtGui.QTextCursor.NextBlock)
566 if cursor.movePosition(QtGui.QTextCursor.NextBlock):
567 prompt = self._continuation_prompt.lstrip()
568 while True:
569 temp_cursor = QtGui.QTextCursor(cursor)
570 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
571 text = str(temp_cursor.selection().toPlainText()).lstrip()
572 if not text.startswith(prompt) or \
573 not cursor.movePosition(QtGui.QTextCursor.NextBlock):
574 break
571 575 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
572 576 cursor.movePosition(QtGui.QTextCursor.End,
573 577 QtGui.QTextCursor.KeepAnchor)
574 578 cursor.removeSelectedText()
575 579
576 580 def _complete_with_items(self, cursor, items):
577 581 """ Performs completion with 'items' at the specified cursor location.
578 582 """
579 583 if self._text_completing_pos:
580 584 self._clear_temporary_buffer()
581 585 self._text_completing_pos = 0
582 586
583 587 if len(items) == 1:
584 588 cursor.setPosition(self._control.textCursor().position(),
585 589 QtGui.QTextCursor.KeepAnchor)
586 590 cursor.insertText(items[0])
587 591
588 592 elif len(items) > 1:
589 593 current_pos = self._control.textCursor().position()
590 594 prefix = commonprefix(items)
591 595 if prefix:
592 596 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
593 597 cursor.insertText(prefix)
594 598 current_pos = cursor.position()
595 599
596 600 if self.gui_completion:
597 601 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
598 602 self._completion_widget.show_items(cursor, items)
599 603 else:
600 604 cursor.beginEditBlock()
601 605 self._append_plain_text('\n')
602 606 self._page(self._format_as_columns(items))
603 607 cursor.endEditBlock()
604 608
605 609 cursor.setPosition(current_pos)
606 610 self._control.moveCursor(QtGui.QTextCursor.End)
607 611 self._control.setTextCursor(cursor)
608 612 self._text_completing_pos = current_pos
609 613
610 614 def _control_key_down(self, modifiers):
611 615 """ Given a KeyboardModifiers flags object, return whether the Control
612 616 key is down (on Mac OS, treat the Command key as a synonym for
613 617 Control).
614 618 """
615 619 down = bool(modifiers & QtCore.Qt.ControlModifier)
616 620
617 621 # Note: on Mac OS, ControlModifier corresponds to the Command key while
618 622 # MetaModifier corresponds to the Control key.
619 623 if sys.platform == 'darwin':
620 624 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
621 625
622 626 return down
623 627
624 628 def _create_control(self):
625 629 """ Creates and connects the underlying text widget.
626 630 """
627 631 if self.kind == 'plain':
628 632 control = ConsolePlainTextEdit()
629 633 elif self.kind == 'rich':
630 634 control = ConsoleTextEdit()
631 635 control.setAcceptRichText(False)
632 636 control.installEventFilter(self)
633 637 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
634 638 control.cursorPositionChanged.connect(self._cursor_position_changed)
635 639 control.customContextMenuRequested.connect(self._show_context_menu)
636 640 control.copyAvailable.connect(self.copy_available)
637 641 control.redoAvailable.connect(self.redo_available)
638 642 control.undoAvailable.connect(self.undo_available)
639 643 control.setReadOnly(True)
640 644 control.setUndoRedoEnabled(False)
641 645 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
642 646 return control
643 647
644 648 def _create_page_control(self):
645 649 """ Creates and connects the underlying paging widget.
646 650 """
647 651 control = ConsolePlainTextEdit()
648 652 control.installEventFilter(self)
649 653 control.setReadOnly(True)
650 654 control.setUndoRedoEnabled(False)
651 655 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
652 656 return control
653 657
654 658 def _event_filter_console_keypress(self, event):
655 659 """ Filter key events for the underlying text widget to create a
656 660 console-like interface.
657 661 """
658 662 intercepted = False
659 663 cursor = self._control.textCursor()
660 664 position = cursor.position()
661 665 key = event.key()
662 666 ctrl_down = self._control_key_down(event.modifiers())
663 667 alt_down = event.modifiers() & QtCore.Qt.AltModifier
664 668 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
665 669
666 670 # Special handling when tab completing in text mode:
667 671 if self._text_completing_pos:
668 672 if key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
669 673 QtCore.Qt.Key_Escape):
670 674 self._clear_temporary_buffer()
671 675 self._text_completing_pos = 0
672 676
673 677 if event.matches(QtGui.QKeySequence.Paste):
674 678 # Call our paste instead of the underlying text widget's.
675 679 self.paste()
676 680 intercepted = True
677 681
678 682 elif ctrl_down:
679 683 if key == QtCore.Qt.Key_K:
680 684 if self._in_buffer(position):
681 685 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
682 686 QtGui.QTextCursor.KeepAnchor)
683 687 if not cursor.hasSelection():
684 688 # Line deletion (remove continuation prompt)
685 689 cursor.movePosition(QtGui.QTextCursor.NextBlock,
686 690 QtGui.QTextCursor.KeepAnchor)
687 691 cursor.movePosition(QtGui.QTextCursor.Right,
688 692 QtGui.QTextCursor.KeepAnchor,
689 693 len(self._continuation_prompt))
690 694 cursor.removeSelectedText()
691 695 intercepted = True
692 696
693 697 elif key == QtCore.Qt.Key_L:
694 698 # It would be better to simply move the prompt block to the top
695 699 # of the control viewport. QPlainTextEdit has a private method
696 700 # to do this (setTopBlock), but it cannot be duplicated here
697 701 # because it requires access to the QTextControl that underlies
698 702 # both QPlainTextEdit and QTextEdit. In short, this can only be
699 703 # achieved by appending newlines after the prompt, which is a
700 704 # gigantic hack and likely to cause other problems.
701 705 self.clear()
702 706 intercepted = True
703 707
704 708 elif key == QtCore.Qt.Key_O:
705 709 if self._page_control and self._page_control.isVisible():
706 710 self._page_control.setFocus()
707 711 intercept = True
708 712
709 713 elif key == QtCore.Qt.Key_X:
710 714 # FIXME: Instead of disabling cut completely, only allow it
711 715 # when safe.
712 716 intercepted = True
713 717
714 718 elif key == QtCore.Qt.Key_Y:
715 719 self.paste()
716 720 intercepted = True
717 721
718 722 elif alt_down:
719 723 if key == QtCore.Qt.Key_B:
720 724 self._set_cursor(self._get_word_start_cursor(position))
721 725 intercepted = True
722 726
723 727 elif key == QtCore.Qt.Key_F:
724 728 self._set_cursor(self._get_word_end_cursor(position))
725 729 intercepted = True
726 730
727 731 elif key == QtCore.Qt.Key_Backspace:
728 732 cursor = self._get_word_start_cursor(position)
729 733 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
730 734 cursor.removeSelectedText()
731 735 intercepted = True
732 736
733 737 elif key == QtCore.Qt.Key_D:
734 738 cursor = self._get_word_end_cursor(position)
735 739 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
736 740 cursor.removeSelectedText()
737 741 intercepted = True
738 742
739 743 elif key == QtCore.Qt.Key_Greater:
740 744 self._control.moveCursor(QtGui.QTextCursor.End)
741 745 intercepted = True
742 746
743 747 elif key == QtCore.Qt.Key_Less:
744 748 self._control.setTextCursor(self._get_prompt_cursor())
745 749 intercepted = True
746 750
747 751 else:
748 752 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
749 753 intercepted = True
750 754 if self._in_buffer(position):
751 755 if self._reading:
752 756 self._append_plain_text('\n')
753 757 self._reading = False
754 758 if self._reading_callback:
755 759 self._reading_callback()
756 760
757 761 # If there is only whitespace after the cursor, execute.
758 762 # Otherwise, split the line with a continuation prompt.
759 763 elif not self._executing:
760 764 cursor.movePosition(QtGui.QTextCursor.End,
761 765 QtGui.QTextCursor.KeepAnchor)
762 766 if cursor.selectedText().trimmed().isEmpty():
763 767 self.execute(interactive=True)
764 768 else:
765 769 # Do this inside an edit block for clean undo/redo.
766 770 cursor.beginEditBlock()
767 771 cursor.setPosition(position)
768 772 cursor.insertText('\n')
769 773 self._insert_continuation_prompt(cursor)
770 774 cursor.endEditBlock()
771 775
772 776 # Ensure that the whole input buffer is visible.
773 777 # FIXME: This will not be usable if the input buffer
774 778 # is taller than the console widget.
775 779 self._control.moveCursor(QtGui.QTextCursor.End)
776 780 self._control.setTextCursor(cursor)
777 781
778 782 elif key == QtCore.Qt.Key_Up:
779 783 if self._reading or not self._up_pressed():
780 784 intercepted = True
781 785 else:
782 786 prompt_line = self._get_prompt_cursor().blockNumber()
783 787 intercepted = cursor.blockNumber() <= prompt_line
784 788
785 789 elif key == QtCore.Qt.Key_Down:
786 790 if self._reading or not self._down_pressed():
787 791 intercepted = True
788 792 else:
789 793 end_line = self._get_end_cursor().blockNumber()
790 794 intercepted = cursor.blockNumber() == end_line
791 795
792 796 elif key == QtCore.Qt.Key_Tab:
793 797 if not self._reading:
794 798 intercepted = not self._tab_pressed()
795 799
796 800 elif key == QtCore.Qt.Key_Left:
797 801 intercepted = not self._in_buffer(position - 1)
798 802
799 803 elif key == QtCore.Qt.Key_Home:
800 804 start_line = cursor.blockNumber()
801 805 if start_line == self._get_prompt_cursor().blockNumber():
802 806 start_pos = self._prompt_pos
803 807 else:
804 808 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
805 809 QtGui.QTextCursor.KeepAnchor)
806 810 start_pos = cursor.position()
807 811 start_pos += len(self._continuation_prompt)
808 812 cursor.setPosition(position)
809 813 if shift_down and self._in_buffer(position):
810 814 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
811 815 else:
812 816 cursor.setPosition(start_pos)
813 817 self._set_cursor(cursor)
814 818 intercepted = True
815 819
816 820 elif key == QtCore.Qt.Key_Backspace:
817 821
818 822 # Line deletion (remove continuation prompt)
819 823 len_prompt = len(self._continuation_prompt)
820 824 if not self._reading and \
821 825 cursor.columnNumber() == len_prompt and \
822 826 position != self._prompt_pos:
823 827 cursor.beginEditBlock()
824 828 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
825 829 QtGui.QTextCursor.KeepAnchor)
826 830 cursor.removeSelectedText()
827 831 cursor.deletePreviousChar()
828 832 cursor.endEditBlock()
829 833 intercepted = True
830 834
831 835 # Regular backwards deletion
832 836 else:
833 837 anchor = cursor.anchor()
834 838 if anchor == position:
835 839 intercepted = not self._in_buffer(position - 1)
836 840 else:
837 841 intercepted = not self._in_buffer(min(anchor, position))
838 842
839 843 elif key == QtCore.Qt.Key_Delete:
840 844
841 845 # Line deletion (remove continuation prompt)
842 846 if not self._reading and cursor.atBlockEnd() and not \
843 847 cursor.hasSelection():
844 848 cursor.movePosition(QtGui.QTextCursor.NextBlock,
845 849 QtGui.QTextCursor.KeepAnchor)
846 850 cursor.movePosition(QtGui.QTextCursor.Right,
847 851 QtGui.QTextCursor.KeepAnchor,
848 852 len(self._continuation_prompt))
849 853 cursor.removeSelectedText()
850 854 intercepted = True
851 855
852 856 # Regular forwards deletion:
853 857 else:
854 858 anchor = cursor.anchor()
855 859 intercepted = (not self._in_buffer(anchor) or
856 860 not self._in_buffer(position))
857 861
858 862 # Don't move the cursor if control is down to allow copy-paste using
859 863 # the keyboard in any part of the buffer.
860 864 if not ctrl_down:
861 865 self._keep_cursor_in_buffer()
862 866
863 867 return intercepted
864 868
865 869 def _event_filter_page_keypress(self, event):
866 870 """ Filter key events for the paging widget to create console-like
867 871 interface.
868 872 """
869 873 key = event.key()
870 874 ctrl_down = self._control_key_down(event.modifiers())
871 875 alt_down = event.modifiers() & QtCore.Qt.AltModifier
872 876
873 877 if ctrl_down:
874 878 if key == QtCore.Qt.Key_O:
875 879 self._control.setFocus()
876 880 intercept = True
877 881
878 882 elif alt_down:
879 883 if key == QtCore.Qt.Key_Greater:
880 884 self._page_control.moveCursor(QtGui.QTextCursor.End)
881 885 intercepted = True
882 886
883 887 elif key == QtCore.Qt.Key_Less:
884 888 self._page_control.moveCursor(QtGui.QTextCursor.Start)
885 889 intercepted = True
886 890
887 891 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
888 892 if self._splitter:
889 893 self._page_control.hide()
890 894 else:
891 895 self.layout().setCurrentWidget(self._control)
892 896 return True
893 897
894 898 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
895 899 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
896 900 QtCore.Qt.Key_PageDown,
897 901 QtCore.Qt.NoModifier)
898 902 QtGui.qApp.sendEvent(self._page_control, new_event)
899 903 return True
900 904
901 905 elif key == QtCore.Qt.Key_Backspace:
902 906 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
903 907 QtCore.Qt.Key_PageUp,
904 908 QtCore.Qt.NoModifier)
905 909 QtGui.qApp.sendEvent(self._page_control, new_event)
906 910 return True
907 911
908 912 return False
909 913
910 914 def _format_as_columns(self, items, separator=' '):
911 915 """ Transform a list of strings into a single string with columns.
912 916
913 917 Parameters
914 918 ----------
915 919 items : sequence of strings
916 920 The strings to process.
917 921
918 922 separator : str, optional [default is two spaces]
919 923 The string that separates columns.
920 924
921 925 Returns
922 926 -------
923 927 The formatted string.
924 928 """
925 929 # Note: this code is adapted from columnize 0.3.2.
926 930 # See http://code.google.com/p/pycolumnize/
927 931
928 932 # Calculate the number of characters available.
929 933 width = self._control.viewport().width()
930 934 char_width = QtGui.QFontMetrics(self.font).maxWidth()
931 935 displaywidth = max(10, (width / char_width) - 1)
932 936
933 937 # Some degenerate cases.
934 938 size = len(items)
935 939 if size == 0:
936 940 return '\n'
937 941 elif size == 1:
938 942 return '%s\n' % str(items[0])
939 943
940 944 # Try every row count from 1 upwards
941 945 array_index = lambda nrows, row, col: nrows*col + row
942 946 for nrows in range(1, size):
943 947 ncols = (size + nrows - 1) // nrows
944 948 colwidths = []
945 949 totwidth = -len(separator)
946 950 for col in range(ncols):
947 951 # Get max column width for this column
948 952 colwidth = 0
949 953 for row in range(nrows):
950 954 i = array_index(nrows, row, col)
951 955 if i >= size: break
952 956 x = items[i]
953 957 colwidth = max(colwidth, len(x))
954 958 colwidths.append(colwidth)
955 959 totwidth += colwidth + len(separator)
956 960 if totwidth > displaywidth:
957 961 break
958 962 if totwidth <= displaywidth:
959 963 break
960 964
961 965 # The smallest number of rows computed and the max widths for each
962 966 # column has been obtained. Now we just have to format each of the rows.
963 967 string = ''
964 968 for row in range(nrows):
965 969 texts = []
966 970 for col in range(ncols):
967 971 i = row + nrows*col
968 972 if i >= size:
969 973 texts.append('')
970 974 else:
971 975 texts.append(items[i])
972 976 while texts and not texts[-1]:
973 977 del texts[-1]
974 978 for col in range(len(texts)):
975 979 texts[col] = texts[col].ljust(colwidths[col])
976 980 string += '%s\n' % str(separator.join(texts))
977 981 return string
978 982
979 983 def _get_block_plain_text(self, block):
980 984 """ Given a QTextBlock, return its unformatted text.
981 985 """
982 986 cursor = QtGui.QTextCursor(block)
983 987 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
984 988 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
985 989 QtGui.QTextCursor.KeepAnchor)
986 990 return str(cursor.selection().toPlainText())
987 991
988 992 def _get_cursor(self):
989 993 """ Convenience method that returns a cursor for the current position.
990 994 """
991 995 return self._control.textCursor()
992 996
993 997 def _get_end_cursor(self):
994 998 """ Convenience method that returns a cursor for the last character.
995 999 """
996 1000 cursor = self._control.textCursor()
997 1001 cursor.movePosition(QtGui.QTextCursor.End)
998 1002 return cursor
999 1003
1000 1004 def _get_input_buffer_cursor_column(self):
1001 1005 """ Returns the column of the cursor in the input buffer, excluding the
1002 1006 contribution by the prompt, or -1 if there is no such column.
1003 1007 """
1004 1008 prompt = self._get_input_buffer_cursor_prompt()
1005 1009 if prompt is None:
1006 1010 return -1
1007 1011 else:
1008 1012 cursor = self._control.textCursor()
1009 1013 return cursor.columnNumber() - len(prompt)
1010 1014
1011 1015 def _get_input_buffer_cursor_line(self):
1012 1016 """ Returns line of the input buffer that contains the cursor, or None
1013 1017 if there is no such line.
1014 1018 """
1015 1019 prompt = self._get_input_buffer_cursor_prompt()
1016 1020 if prompt is None:
1017 1021 return None
1018 1022 else:
1019 1023 cursor = self._control.textCursor()
1020 1024 text = self._get_block_plain_text(cursor.block())
1021 1025 return text[len(prompt):]
1022 1026
1023 1027 def _get_input_buffer_cursor_prompt(self):
1024 1028 """ Returns the (plain text) prompt for line of the input buffer that
1025 1029 contains the cursor, or None if there is no such line.
1026 1030 """
1027 1031 if self._executing:
1028 1032 return None
1029 1033 cursor = self._control.textCursor()
1030 1034 if cursor.position() >= self._prompt_pos:
1031 1035 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1032 1036 return self._prompt
1033 1037 else:
1034 1038 return self._continuation_prompt
1035 1039 else:
1036 1040 return None
1037 1041
1038 1042 def _get_prompt_cursor(self):
1039 1043 """ Convenience method that returns a cursor for the prompt position.
1040 1044 """
1041 1045 cursor = self._control.textCursor()
1042 1046 cursor.setPosition(self._prompt_pos)
1043 1047 return cursor
1044 1048
1045 1049 def _get_selection_cursor(self, start, end):
1046 1050 """ Convenience method that returns a cursor with text selected between
1047 1051 the positions 'start' and 'end'.
1048 1052 """
1049 1053 cursor = self._control.textCursor()
1050 1054 cursor.setPosition(start)
1051 1055 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1052 1056 return cursor
1053 1057
1054 1058 def _get_word_start_cursor(self, position):
1055 1059 """ Find the start of the word to the left the given position. If a
1056 1060 sequence of non-word characters precedes the first word, skip over
1057 1061 them. (This emulates the behavior of bash, emacs, etc.)
1058 1062 """
1059 1063 document = self._control.document()
1060 1064 position -= 1
1061 1065 while position >= self._prompt_pos and \
1062 1066 not document.characterAt(position).isLetterOrNumber():
1063 1067 position -= 1
1064 1068 while position >= self._prompt_pos and \
1065 1069 document.characterAt(position).isLetterOrNumber():
1066 1070 position -= 1
1067 1071 cursor = self._control.textCursor()
1068 1072 cursor.setPosition(position + 1)
1069 1073 return cursor
1070 1074
1071 1075 def _get_word_end_cursor(self, position):
1072 1076 """ Find the end of the word to the right the given position. If a
1073 1077 sequence of non-word characters precedes the first word, skip over
1074 1078 them. (This emulates the behavior of bash, emacs, etc.)
1075 1079 """
1076 1080 document = self._control.document()
1077 1081 end = self._get_end_cursor().position()
1078 1082 while position < end and \
1079 1083 not document.characterAt(position).isLetterOrNumber():
1080 1084 position += 1
1081 1085 while position < end and \
1082 1086 document.characterAt(position).isLetterOrNumber():
1083 1087 position += 1
1084 1088 cursor = self._control.textCursor()
1085 1089 cursor.setPosition(position)
1086 1090 return cursor
1087 1091
1088 1092 def _insert_continuation_prompt(self, cursor):
1089 1093 """ Inserts new continuation prompt using the specified cursor.
1090 1094 """
1091 1095 if self._continuation_prompt_html is None:
1092 1096 self._insert_plain_text(cursor, self._continuation_prompt)
1093 1097 else:
1094 1098 self._continuation_prompt = self._insert_html_fetching_plain_text(
1095 1099 cursor, self._continuation_prompt_html)
1096 1100
1097 1101 def _insert_html(self, cursor, html):
1098 1102 """ Inserts HTML using the specified cursor in such a way that future
1099 1103 formatting is unaffected.
1100 1104 """
1101 1105 cursor.beginEditBlock()
1102 1106 cursor.insertHtml(html)
1103 1107
1104 1108 # After inserting HTML, the text document "remembers" it's in "html
1105 1109 # mode", which means that subsequent calls adding plain text will result
1106 1110 # in unwanted formatting, lost tab characters, etc. The following code
1107 1111 # hacks around this behavior, which I consider to be a bug in Qt, by
1108 1112 # (crudely) resetting the document's style state.
1109 1113 cursor.movePosition(QtGui.QTextCursor.Left,
1110 1114 QtGui.QTextCursor.KeepAnchor)
1111 1115 if cursor.selection().toPlainText() == ' ':
1112 1116 cursor.removeSelectedText()
1113 1117 else:
1114 1118 cursor.movePosition(QtGui.QTextCursor.Right)
1115 1119 cursor.insertText(' ', QtGui.QTextCharFormat())
1116 1120 cursor.endEditBlock()
1117 1121
1118 1122 def _insert_html_fetching_plain_text(self, cursor, html):
1119 1123 """ Inserts HTML using the specified cursor, then returns its plain text
1120 1124 version.
1121 1125 """
1122 1126 cursor.beginEditBlock()
1123 1127 cursor.removeSelectedText()
1124 1128
1125 1129 start = cursor.position()
1126 1130 self._insert_html(cursor, html)
1127 1131 end = cursor.position()
1128 1132 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1129 1133 text = str(cursor.selection().toPlainText())
1130 1134
1131 1135 cursor.setPosition(end)
1132 1136 cursor.endEditBlock()
1133 1137 return text
1134 1138
1135 1139 def _insert_plain_text(self, cursor, text):
1136 1140 """ Inserts plain text using the specified cursor, processing ANSI codes
1137 1141 if enabled.
1138 1142 """
1139 1143 cursor.beginEditBlock()
1140 1144 if self.ansi_codes:
1141 1145 for substring in self._ansi_processor.split_string(text):
1142 1146 for action in self._ansi_processor.actions:
1143 1147 if action.kind == 'erase' and action.area == 'screen':
1144 1148 cursor.select(QtGui.QTextCursor.Document)
1145 1149 cursor.removeSelectedText()
1146 1150 format = self._ansi_processor.get_format()
1147 1151 cursor.insertText(substring, format)
1148 1152 else:
1149 1153 cursor.insertText(text)
1150 1154 cursor.endEditBlock()
1151 1155
1152 1156 def _insert_plain_text_into_buffer(self, text):
1153 1157 """ Inserts text into the input buffer at the current cursor position,
1154 1158 ensuring that continuation prompts are inserted as necessary.
1155 1159 """
1156 1160 lines = str(text).splitlines(True)
1157 1161 if lines:
1158 1162 self._keep_cursor_in_buffer()
1159 1163 cursor = self._control.textCursor()
1160 1164 cursor.beginEditBlock()
1161 1165 cursor.insertText(lines[0])
1162 1166 for line in lines[1:]:
1163 1167 if self._continuation_prompt_html is None:
1164 1168 cursor.insertText(self._continuation_prompt)
1165 1169 else:
1166 1170 self._continuation_prompt = \
1167 1171 self._insert_html_fetching_plain_text(
1168 1172 cursor, self._continuation_prompt_html)
1169 1173 cursor.insertText(line)
1170 1174 cursor.endEditBlock()
1171 1175 self._control.setTextCursor(cursor)
1172 1176
1173 1177 def _in_buffer(self, position=None):
1174 1178 """ Returns whether the current cursor (or, if specified, a position) is
1175 1179 inside the editing region.
1176 1180 """
1177 1181 cursor = self._control.textCursor()
1178 1182 if position is None:
1179 1183 position = cursor.position()
1180 1184 else:
1181 1185 cursor.setPosition(position)
1182 1186 line = cursor.blockNumber()
1183 1187 prompt_line = self._get_prompt_cursor().blockNumber()
1184 1188 if line == prompt_line:
1185 1189 return position >= self._prompt_pos
1186 1190 elif line > prompt_line:
1187 1191 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1188 1192 prompt_pos = cursor.position() + len(self._continuation_prompt)
1189 1193 return position >= prompt_pos
1190 1194 return False
1191 1195
1192 1196 def _keep_cursor_in_buffer(self):
1193 1197 """ Ensures that the cursor is inside the editing region. Returns
1194 1198 whether the cursor was moved.
1195 1199 """
1196 1200 moved = not self._in_buffer()
1197 1201 if moved:
1198 1202 cursor = self._control.textCursor()
1199 1203 cursor.movePosition(QtGui.QTextCursor.End)
1200 1204 self._control.setTextCursor(cursor)
1201 1205 return moved
1202 1206
1203 1207 def _page(self, text):
1204 1208 """ Displays text using the pager if it exceeds the height of the
1205 1209 visible area.
1206 1210 """
1207 1211 if self.paging == 'none':
1208 1212 self._append_plain_text(text)
1209 1213 else:
1210 1214 line_height = QtGui.QFontMetrics(self.font).height()
1211 1215 minlines = self._control.viewport().height() / line_height
1212 1216 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1213 1217 if self.paging == 'custom':
1214 1218 self.custom_page_requested.emit(text)
1215 1219 else:
1216 1220 self._page_control.clear()
1217 1221 cursor = self._page_control.textCursor()
1218 1222 self._insert_plain_text(cursor, text)
1219 1223 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1220 1224
1221 1225 self._page_control.viewport().resize(self._control.size())
1222 1226 if self._splitter:
1223 1227 self._page_control.show()
1224 1228 self._page_control.setFocus()
1225 1229 else:
1226 1230 self.layout().setCurrentWidget(self._page_control)
1227 1231 else:
1228 1232 self._append_plain_text(text)
1229 1233
1230 1234 def _prompt_started(self):
1231 1235 """ Called immediately after a new prompt is displayed.
1232 1236 """
1233 1237 # Temporarily disable the maximum block count to permit undo/redo and
1234 1238 # to ensure that the prompt position does not change due to truncation.
1235 1239 # Because setting this property clears the undo/redo history, we only
1236 1240 # set it if we have to.
1237 1241 if self._control.document().maximumBlockCount() > 0:
1238 1242 self._control.document().setMaximumBlockCount(0)
1239 1243 self._control.setUndoRedoEnabled(True)
1240 1244
1241 1245 self._control.setReadOnly(False)
1242 1246 self._control.moveCursor(QtGui.QTextCursor.End)
1243 1247
1244 1248 self._executing = False
1245 1249 self._prompt_started_hook()
1246 1250
1247 1251 def _prompt_finished(self):
1248 1252 """ Called immediately after a prompt is finished, i.e. when some input
1249 1253 will be processed and a new prompt displayed.
1250 1254 """
1251 1255 self._control.setReadOnly(True)
1252 1256 self._prompt_finished_hook()
1253 1257
1254 1258 def _readline(self, prompt='', callback=None):
1255 1259 """ Reads one line of input from the user.
1256 1260
1257 1261 Parameters
1258 1262 ----------
1259 1263 prompt : str, optional
1260 1264 The prompt to print before reading the line.
1261 1265
1262 1266 callback : callable, optional
1263 1267 A callback to execute with the read line. If not specified, input is
1264 1268 read *synchronously* and this method does not return until it has
1265 1269 been read.
1266 1270
1267 1271 Returns
1268 1272 -------
1269 1273 If a callback is specified, returns nothing. Otherwise, returns the
1270 1274 input string with the trailing newline stripped.
1271 1275 """
1272 1276 if self._reading:
1273 1277 raise RuntimeError('Cannot read a line. Widget is already reading.')
1274 1278
1275 1279 if not callback and not self.isVisible():
1276 1280 # If the user cannot see the widget, this function cannot return.
1277 1281 raise RuntimeError('Cannot synchronously read a line if the widget '
1278 1282 'is not visible!')
1279 1283
1280 1284 self._reading = True
1281 1285 self._show_prompt(prompt, newline=False)
1282 1286
1283 1287 if callback is None:
1284 1288 self._reading_callback = None
1285 1289 while self._reading:
1286 1290 QtCore.QCoreApplication.processEvents()
1287 1291 return self.input_buffer.rstrip('\n')
1288 1292
1289 1293 else:
1290 1294 self._reading_callback = lambda: \
1291 1295 callback(self.input_buffer.rstrip('\n'))
1292 1296
1293 1297 def _set_continuation_prompt(self, prompt, html=False):
1294 1298 """ Sets the continuation prompt.
1295 1299
1296 1300 Parameters
1297 1301 ----------
1298 1302 prompt : str
1299 1303 The prompt to show when more input is needed.
1300 1304
1301 1305 html : bool, optional (default False)
1302 1306 If set, the prompt will be inserted as formatted HTML. Otherwise,
1303 1307 the prompt will be treated as plain text, though ANSI color codes
1304 1308 will be handled.
1305 1309 """
1306 1310 if html:
1307 1311 self._continuation_prompt_html = prompt
1308 1312 else:
1309 1313 self._continuation_prompt = prompt
1310 1314 self._continuation_prompt_html = None
1311 1315
1312 1316 def _set_cursor(self, cursor):
1313 1317 """ Convenience method to set the current cursor.
1314 1318 """
1315 1319 self._control.setTextCursor(cursor)
1316 1320
1317 1321 def _show_context_menu(self, pos):
1318 1322 """ Shows a context menu at the given QPoint (in widget coordinates).
1319 1323 """
1320 1324 menu = QtGui.QMenu()
1321 1325
1322 1326 copy_action = menu.addAction('Copy', self.copy)
1323 1327 copy_action.setEnabled(self._get_cursor().hasSelection())
1324 1328 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1325 1329
1326 1330 paste_action = menu.addAction('Paste', self.paste)
1327 1331 paste_action.setEnabled(self.can_paste())
1328 1332 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1329 1333
1330 1334 menu.addSeparator()
1331 1335 menu.addAction('Select All', self.select_all)
1332 1336
1333 1337 menu.exec_(self._control.mapToGlobal(pos))
1334 1338
1335 1339 def _show_prompt(self, prompt=None, html=False, newline=True):
1336 1340 """ Writes a new prompt at the end of the buffer.
1337 1341
1338 1342 Parameters
1339 1343 ----------
1340 1344 prompt : str, optional
1341 1345 The prompt to show. If not specified, the previous prompt is used.
1342 1346
1343 1347 html : bool, optional (default False)
1344 1348 Only relevant when a prompt is specified. If set, the prompt will
1345 1349 be inserted as formatted HTML. Otherwise, the prompt will be treated
1346 1350 as plain text, though ANSI color codes will be handled.
1347 1351
1348 1352 newline : bool, optional (default True)
1349 1353 If set, a new line will be written before showing the prompt if
1350 1354 there is not already a newline at the end of the buffer.
1351 1355 """
1352 1356 # Insert a preliminary newline, if necessary.
1353 1357 if newline:
1354 1358 cursor = self._get_end_cursor()
1355 1359 if cursor.position() > 0:
1356 1360 cursor.movePosition(QtGui.QTextCursor.Left,
1357 1361 QtGui.QTextCursor.KeepAnchor)
1358 1362 if str(cursor.selection().toPlainText()) != '\n':
1359 1363 self._append_plain_text('\n')
1360 1364
1361 1365 # Write the prompt.
1362 1366 self._append_plain_text(self._prompt_sep)
1363 1367 if prompt is None:
1364 1368 if self._prompt_html is None:
1365 1369 self._append_plain_text(self._prompt)
1366 1370 else:
1367 1371 self._append_html(self._prompt_html)
1368 1372 else:
1369 1373 if html:
1370 1374 self._prompt = self._append_html_fetching_plain_text(prompt)
1371 1375 self._prompt_html = prompt
1372 1376 else:
1373 1377 self._append_plain_text(prompt)
1374 1378 self._prompt = prompt
1375 1379 self._prompt_html = None
1376 1380
1377 1381 self._prompt_pos = self._get_end_cursor().position()
1378 1382 self._prompt_started()
1379 1383
1380 1384 #------ Signal handlers ----------------------------------------------------
1381 1385
1382 1386 def _cursor_position_changed(self):
1383 1387 """ Clears the temporary buffer based on the cursor position.
1384 1388 """
1385 1389 if self._text_completing_pos:
1386 1390 document = self._control.document()
1387 1391 if self._text_completing_pos < document.characterCount():
1388 1392 cursor = self._control.textCursor()
1389 1393 pos = cursor.position()
1390 1394 text_cursor = self._control.textCursor()
1391 1395 text_cursor.setPosition(self._text_completing_pos)
1392 1396 if pos < self._text_completing_pos or \
1393 1397 cursor.blockNumber() > text_cursor.blockNumber():
1394 1398 self._clear_temporary_buffer()
1395 1399 self._text_completing_pos = 0
1396 1400 else:
1397 1401 self._clear_temporary_buffer()
1398 1402 self._text_completing_pos = 0
1399 1403
1400 1404
1401 1405 class HistoryConsoleWidget(ConsoleWidget):
1402 1406 """ A ConsoleWidget that keeps a history of the commands that have been
1403 1407 executed.
1404 1408 """
1405 1409
1406 1410 #---------------------------------------------------------------------------
1407 1411 # 'object' interface
1408 1412 #---------------------------------------------------------------------------
1409 1413
1410 1414 def __init__(self, *args, **kw):
1411 1415 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1412 1416 self._history = []
1413 1417 self._history_index = 0
1414 1418
1415 1419 #---------------------------------------------------------------------------
1416 1420 # 'ConsoleWidget' public interface
1417 1421 #---------------------------------------------------------------------------
1418 1422
1419 1423 def execute(self, source=None, hidden=False, interactive=False):
1420 1424 """ Reimplemented to the store history.
1421 1425 """
1422 1426 if not hidden:
1423 1427 history = self.input_buffer if source is None else source
1424 1428
1425 1429 executed = super(HistoryConsoleWidget, self).execute(
1426 1430 source, hidden, interactive)
1427 1431
1428 1432 if executed and not hidden:
1429 1433 # Save the command unless it was a blank line.
1430 1434 history = history.rstrip()
1431 1435 if history:
1432 1436 self._history.append(history)
1433 1437 self._history_index = len(self._history)
1434 1438
1435 1439 return executed
1436 1440
1437 1441 #---------------------------------------------------------------------------
1438 1442 # 'ConsoleWidget' abstract interface
1439 1443 #---------------------------------------------------------------------------
1440 1444
1441 1445 def _up_pressed(self):
1442 1446 """ Called when the up key is pressed. Returns whether to continue
1443 1447 processing the event.
1444 1448 """
1445 1449 prompt_cursor = self._get_prompt_cursor()
1446 1450 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1447 1451 self.history_previous()
1448 1452
1449 1453 # Go to the first line of prompt for seemless history scrolling.
1450 1454 cursor = self._get_prompt_cursor()
1451 1455 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1452 1456 self._set_cursor(cursor)
1453 1457
1454 1458 return False
1455 1459 return True
1456 1460
1457 1461 def _down_pressed(self):
1458 1462 """ Called when the down key is pressed. Returns whether to continue
1459 1463 processing the event.
1460 1464 """
1461 1465 end_cursor = self._get_end_cursor()
1462 1466 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1463 1467 self.history_next()
1464 1468 return False
1465 1469 return True
1466 1470
1467 1471 #---------------------------------------------------------------------------
1468 1472 # 'HistoryConsoleWidget' public interface
1469 1473 #---------------------------------------------------------------------------
1470 1474
1471 1475 def history_previous(self):
1472 1476 """ If possible, set the input buffer to the previous item in the
1473 1477 history.
1474 1478 """
1475 1479 if self._history_index > 0:
1476 1480 self._history_index -= 1
1477 1481 self.input_buffer = self._history[self._history_index]
1478 1482
1479 1483 def history_next(self):
1480 1484 """ Set the input buffer to the next item in the history, or a blank
1481 1485 line if there is no subsequent item.
1482 1486 """
1483 1487 if self._history_index < len(self._history):
1484 1488 self._history_index += 1
1485 1489 if self._history_index < len(self._history):
1486 1490 self.input_buffer = self._history[self._history_index]
1487 1491 else:
1488 1492 self.input_buffer = ''
1489 1493
1490 1494 #---------------------------------------------------------------------------
1491 1495 # 'HistoryConsoleWidget' protected interface
1492 1496 #---------------------------------------------------------------------------
1493 1497
1494 1498 def _set_history(self, history):
1495 1499 """ Replace the current history with a sequence of history items.
1496 1500 """
1497 1501 self._history = list(history)
1498 1502 self._history_index = len(self._history)
@@ -1,393 +1,402 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 # Standard library imports
14 14 from collections import namedtuple
15 import re
15 16 from subprocess import Popen
16 17
17 18 # System library imports
18 19 from PyQt4 import QtCore, QtGui
19 20
20 21 # Local imports
21 22 from IPython.core.inputsplitter import IPythonInputSplitter
22 23 from IPython.core.usage import default_banner
23 24 from IPython.utils.traitlets import Bool, Str
24 25 from frontend_widget import FrontendWidget
25 26
26 27 #-----------------------------------------------------------------------------
27 28 # Constants
28 29 #-----------------------------------------------------------------------------
29 30
30 31 # The default light style sheet: black text on a white background.
31 32 default_light_style_sheet = '''
32 33 .error { color: red; }
33 34 .in-prompt { color: navy; }
34 35 .in-prompt-number { font-weight: bold; }
35 36 .out-prompt { color: darkred; }
36 37 .out-prompt-number { font-weight: bold; }
37 38 '''
38 39 default_light_syntax_style = 'default'
39 40
40 41 # The default dark style sheet: white text on a black background.
41 42 default_dark_style_sheet = '''
42 43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
43 44 QFrame { border: 1px solid grey; }
44 45 .error { color: red; }
45 46 .in-prompt { color: lime; }
46 47 .in-prompt-number { color: lime; font-weight: bold; }
47 48 .out-prompt { color: red; }
48 49 .out-prompt-number { color: red; font-weight: bold; }
49 50 '''
50 51 default_dark_syntax_style = 'monokai'
51 52
52 53 # Default prompts.
53 54 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
54 55 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
55 56
56 57 #-----------------------------------------------------------------------------
57 58 # IPythonWidget class
58 59 #-----------------------------------------------------------------------------
59 60
60 61 class IPythonWidget(FrontendWidget):
61 62 """ A FrontendWidget for an IPython kernel.
62 63 """
63 64
64 65 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
65 66 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
66 67 # settings.
67 68 custom_edit = Bool(False)
68 69 custom_edit_requested = QtCore.pyqtSignal(object, object)
69 70
70 71 # A command for invoking a system text editor. If the string contains a
71 72 # {filename} format specifier, it will be used. Otherwise, the filename will
72 73 # be appended to the end the command.
73 74 editor = Str('default', config=True)
74 75
75 76 # The editor command to use when a specific line number is requested. The
76 77 # string should contain two format specifiers: {line} and {filename}. If
77 78 # this parameter is not specified, the line number option to the %edit magic
78 79 # will be ignored.
79 80 editor_line = Str(config=True)
80 81
81 82 # A CSS stylesheet. The stylesheet can contain classes for:
82 83 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
83 84 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
84 85 # 3. IPython: .error, .in-prompt, .out-prompt, etc
85 86 style_sheet = Str(config=True)
86 87
87 88 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
88 89 # the style sheet is queried for Pygments style information.
89 90 syntax_style = Str(config=True)
90 91
91 92 # Prompts.
92 93 in_prompt = Str(default_in_prompt, config=True)
93 94 out_prompt = Str(default_out_prompt, config=True)
94 95
95 96 # FrontendWidget protected class variables.
96 97 _input_splitter_class = IPythonInputSplitter
97 98
98 99 # IPythonWidget protected class variables.
99 100 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
100 101 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
101 102 _payload_source_page = 'IPython.zmq.page.page'
102 103
103 104 #---------------------------------------------------------------------------
104 105 # 'object' interface
105 106 #---------------------------------------------------------------------------
106 107
107 108 def __init__(self, *args, **kw):
108 109 super(IPythonWidget, self).__init__(*args, **kw)
109 110
110 111 # IPythonWidget protected variables.
111 112 self._previous_prompt_obj = None
112 113
113 114 # Initialize widget styling.
114 115 if self.style_sheet:
115 116 self._style_sheet_changed()
116 117 self._syntax_style_changed()
117 118 else:
118 119 self.set_default_style()
119 120
120 121 #---------------------------------------------------------------------------
121 122 # 'BaseFrontendMixin' abstract interface
122 123 #---------------------------------------------------------------------------
123 124
124 125 def _handle_complete_reply(self, rep):
125 126 """ Reimplemented to support IPython's improved completion machinery.
126 127 """
127 128 cursor = self._get_cursor()
128 129 if rep['parent_header']['msg_id'] == self._complete_id and \
129 130 cursor.position() == self._complete_pos:
130 # The completer tells us what text was actually used for the
131 # matching, so we must move that many characters left to apply the
132 # completions.
131 matches = rep['content']['matches']
133 132 text = rep['content']['matched_text']
133
134 # Clean up matches with '.'s and path separators.
135 parts = re.split(r'[./\\]', text)
136 sep_count = len(parts) - 1
137 if sep_count:
138 chop_length = sum(map(len, parts[:sep_count])) + sep_count
139 matches = [ match[chop_length:] for match in matches ]
140 text = text[chop_length:]
141
142 # Move the cursor to the start of the match and complete.
134 143 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
135 self._complete_with_items(cursor, rep['content']['matches'])
144 self._complete_with_items(cursor, matches)
136 145
137 146 def _handle_history_reply(self, msg):
138 147 """ Implemented to handle history replies, which are only supported by
139 148 the IPython kernel.
140 149 """
141 150 history_dict = msg['content']['history']
142 151 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
143 152 self._set_history(items)
144 153
145 154 def _handle_prompt_reply(self, msg):
146 155 """ Implemented to handle prompt number replies, which are only
147 156 supported by the IPython kernel.
148 157 """
149 158 content = msg['content']
150 159 self._show_interpreter_prompt(content['prompt_number'],
151 160 content['input_sep'])
152 161
153 162 def _handle_pyout(self, msg):
154 163 """ Reimplemented for IPython-style "display hook".
155 164 """
156 165 if not self._hidden and self._is_from_this_session(msg):
157 166 content = msg['content']
158 167 prompt_number = content['prompt_number']
159 168 self._append_plain_text(content['output_sep'])
160 169 self._append_html(self._make_out_prompt(prompt_number))
161 170 self._append_plain_text(content['data'] + '\n' +
162 171 content['output_sep2'])
163 172
164 173 def _started_channels(self):
165 174 """ Reimplemented to make a history request.
166 175 """
167 176 super(IPythonWidget, self)._started_channels()
168 177 # FIXME: Disabled until history requests are properly implemented.
169 178 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
170 179
171 180 #---------------------------------------------------------------------------
172 181 # 'FrontendWidget' interface
173 182 #---------------------------------------------------------------------------
174 183
175 184 def execute_file(self, path, hidden=False):
176 185 """ Reimplemented to use the 'run' magic.
177 186 """
178 187 self.execute('%%run %s' % path, hidden=hidden)
179 188
180 189 #---------------------------------------------------------------------------
181 190 # 'FrontendWidget' protected interface
182 191 #---------------------------------------------------------------------------
183 192
184 193 def _complete(self):
185 194 """ Reimplemented to support IPython's improved completion machinery.
186 195 """
187 196 # We let the kernel split the input line, so we *always* send an empty
188 197 # text field. Readline-based frontends do get a real text field which
189 198 # they can use.
190 199 text = ''
191 200
192 201 # Send the completion request to the kernel
193 202 self._complete_id = self.kernel_manager.xreq_channel.complete(
194 203 text, # text
195 204 self._get_input_buffer_cursor_line(), # line
196 205 self._get_input_buffer_cursor_column(), # cursor_pos
197 206 self.input_buffer) # block
198 207 self._complete_pos = self._get_cursor().position()
199 208
200 209 def _get_banner(self):
201 210 """ Reimplemented to return IPython's default banner.
202 211 """
203 212 return default_banner + '\n'
204 213
205 214 def _process_execute_error(self, msg):
206 215 """ Reimplemented for IPython-style traceback formatting.
207 216 """
208 217 content = msg['content']
209 218 traceback = '\n'.join(content['traceback']) + '\n'
210 219 if False:
211 220 # FIXME: For now, tracebacks come as plain text, so we can't use
212 221 # the html renderer yet. Once we refactor ultratb to produce
213 222 # properly styled tracebacks, this branch should be the default
214 223 traceback = traceback.replace(' ', '&nbsp;')
215 224 traceback = traceback.replace('\n', '<br/>')
216 225
217 226 ename = content['ename']
218 227 ename_styled = '<span class="error">%s</span>' % ename
219 228 traceback = traceback.replace(ename, ename_styled)
220 229
221 230 self._append_html(traceback)
222 231 else:
223 232 # This is the fallback for now, using plain text with ansi escapes
224 233 self._append_plain_text(traceback)
225 234
226 235 def _process_execute_payload(self, item):
227 236 """ Reimplemented to handle %edit and paging payloads.
228 237 """
229 238 if item['source'] == self._payload_source_edit:
230 239 self._edit(item['filename'], item['line_number'])
231 240 return True
232 241 elif item['source'] == self._payload_source_page:
233 242 self._page(item['data'])
234 243 return True
235 244 else:
236 245 return False
237 246
238 247 def _show_interpreter_prompt(self, number=None, input_sep='\n'):
239 248 """ Reimplemented for IPython-style prompts.
240 249 """
241 250 # If a number was not specified, make a prompt number request.
242 251 if number is None:
243 252 self.kernel_manager.xreq_channel.prompt()
244 253 return
245 254
246 255 # Show a new prompt and save information about it so that it can be
247 256 # updated later if the prompt number turns out to be wrong.
248 257 self._prompt_sep = input_sep
249 258 self._show_prompt(self._make_in_prompt(number), html=True)
250 259 block = self._control.document().lastBlock()
251 260 length = len(self._prompt)
252 261 self._previous_prompt_obj = self._PromptBlock(block, length, number)
253 262
254 263 # Update continuation prompt to reflect (possibly) new prompt length.
255 264 self._set_continuation_prompt(
256 265 self._make_continuation_prompt(self._prompt), html=True)
257 266
258 267 def _show_interpreter_prompt_for_reply(self, msg):
259 268 """ Reimplemented for IPython-style prompts.
260 269 """
261 270 # Update the old prompt number if necessary.
262 271 content = msg['content']
263 272 previous_prompt_number = content['prompt_number']
264 273 if self._previous_prompt_obj and \
265 274 self._previous_prompt_obj.number != previous_prompt_number:
266 275 block = self._previous_prompt_obj.block
267 276
268 277 # Make sure the prompt block has not been erased.
269 278 if block.isValid() and not block.text().isEmpty():
270 279
271 280 # Remove the old prompt and insert a new prompt.
272 281 cursor = QtGui.QTextCursor(block)
273 282 cursor.movePosition(QtGui.QTextCursor.Right,
274 283 QtGui.QTextCursor.KeepAnchor,
275 284 self._previous_prompt_obj.length)
276 285 prompt = self._make_in_prompt(previous_prompt_number)
277 286 self._prompt = self._insert_html_fetching_plain_text(
278 287 cursor, prompt)
279 288
280 289 # When the HTML is inserted, Qt blows away the syntax
281 290 # highlighting for the line, so we need to rehighlight it.
282 291 self._highlighter.rehighlightBlock(cursor.block())
283 292
284 293 self._previous_prompt_obj = None
285 294
286 295 # Show a new prompt with the kernel's estimated prompt number.
287 296 next_prompt = content['next_prompt']
288 297 self._show_interpreter_prompt(next_prompt['prompt_number'],
289 298 next_prompt['input_sep'])
290 299
291 300 #---------------------------------------------------------------------------
292 301 # 'IPythonWidget' interface
293 302 #---------------------------------------------------------------------------
294 303
295 304 def set_default_style(self, lightbg=True):
296 305 """ Sets the widget style to the class defaults.
297 306
298 307 Parameters:
299 308 -----------
300 309 lightbg : bool, optional (default True)
301 310 Whether to use the default IPython light background or dark
302 311 background style.
303 312 """
304 313 if lightbg:
305 314 self.style_sheet = default_light_style_sheet
306 315 self.syntax_style = default_light_syntax_style
307 316 else:
308 317 self.style_sheet = default_dark_style_sheet
309 318 self.syntax_style = default_dark_syntax_style
310 319
311 320 #---------------------------------------------------------------------------
312 321 # 'IPythonWidget' protected interface
313 322 #---------------------------------------------------------------------------
314 323
315 324 def _edit(self, filename, line=None):
316 325 """ Opens a Python script for editing.
317 326
318 327 Parameters:
319 328 -----------
320 329 filename : str
321 330 A path to a local system file.
322 331
323 332 line : int, optional
324 333 A line of interest in the file.
325 334 """
326 335 if self.custom_edit:
327 336 self.custom_edit_requested.emit(filename, line)
328 337 elif self.editor == 'default':
329 338 self._append_plain_text('No default editor available.\n')
330 339 else:
331 340 try:
332 341 filename = '"%s"' % filename
333 342 if line and self.editor_line:
334 343 command = self.editor_line.format(filename=filename,
335 344 line=line)
336 345 else:
337 346 try:
338 347 command = self.editor.format()
339 348 except KeyError:
340 349 command = self.editor.format(filename=filename)
341 350 else:
342 351 command += ' ' + filename
343 352 except KeyError:
344 353 self._append_plain_text('Invalid editor command.\n')
345 354 else:
346 355 try:
347 356 Popen(command, shell=True)
348 357 except OSError:
349 358 msg = 'Opening editor with command "%s" failed.\n'
350 359 self._append_plain_text(msg % command)
351 360
352 361 def _make_in_prompt(self, number):
353 362 """ Given a prompt number, returns an HTML In prompt.
354 363 """
355 364 body = self.in_prompt % number
356 365 return '<span class="in-prompt">%s</span>' % body
357 366
358 367 def _make_continuation_prompt(self, prompt):
359 368 """ Given a plain text version of an In prompt, returns an HTML
360 369 continuation prompt.
361 370 """
362 371 end_chars = '...: '
363 372 space_count = len(prompt.lstrip('\n')) - len(end_chars)
364 373 body = '&nbsp;' * space_count + end_chars
365 374 return '<span class="in-prompt">%s</span>' % body
366 375
367 376 def _make_out_prompt(self, number):
368 377 """ Given a prompt number, returns an HTML Out prompt.
369 378 """
370 379 body = self.out_prompt % number
371 380 return '<span class="out-prompt">%s</span>' % body
372 381
373 382 #------ Trait change handlers ---------------------------------------------
374 383
375 384 def _style_sheet_changed(self):
376 385 """ Set the style sheets of the underlying widgets.
377 386 """
378 387 self.setStyleSheet(self.style_sheet)
379 388 self._control.document().setDefaultStyleSheet(self.style_sheet)
380 389 if self._page_control:
381 390 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
382 391
383 392 bg_color = self._control.palette().background().color()
384 393 self._ansi_processor.set_background_color(bg_color)
385 394
386 395 def _syntax_style_changed(self):
387 396 """ Set the style for the syntax highlighter.
388 397 """
389 398 if self.syntax_style:
390 399 self._highlighter.set_style(self.syntax_style)
391 400 else:
392 401 self._highlighter.set_style_sheet(self.style_sheet)
393 402
General Comments 0
You need to be logged in to leave comments. Login now