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