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