##// END OF EJS Templates
Added preliminary editor support to IPythonWidget.
epatters -
Show More
@@ -1,1248 +1,1248 b''
1 1 # Standard library imports
2 2 import sys
3 3 from textwrap import dedent
4 4
5 5 # System library imports
6 6 from PyQt4 import QtCore, QtGui
7 7
8 8 # Local imports
9 9 from ansi_code_processor import QtAnsiCodeProcessor
10 10 from completion_widget import CompletionWidget
11 11
12 12
13 13 class ConsoleWidget(QtGui.QWidget):
14 14 """ An abstract base class for console-type widgets. This class has
15 15 functionality for:
16 16
17 17 * Maintaining a prompt and editing region
18 18 * Providing the traditional Unix-style console keyboard shortcuts
19 19 * Performing tab completion
20 20 * Paging text
21 21 * Handling ANSI escape codes
22 22
23 23 ConsoleWidget also provides a number of utility methods that will be
24 24 convenient to implementors of a console-style widget.
25 25 """
26 26
27 27 # Whether to process ANSI escape codes.
28 28 ansi_codes = True
29 29
30 30 # The maximum number of lines of text before truncation.
31 31 buffer_size = 500
32 32
33 33 # Whether to use a list widget or plain text output for tab completion.
34 34 gui_completion = True
35 35
36 36 # Whether to override ShortcutEvents for the keybindings defined by this
37 37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
38 38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
39 39 override_shortcuts = False
40 40
41 41 # Signals that indicate ConsoleWidget state.
42 42 copy_available = QtCore.pyqtSignal(bool)
43 43 redo_available = QtCore.pyqtSignal(bool)
44 44 undo_available = QtCore.pyqtSignal(bool)
45 45
46 46 # Signal emitted when paging is needed and the paging style has been
47 47 # specified as 'custom'.
48 custom_page_requested = QtCore.pyqtSignal(QtCore.QString)
48 custom_page_requested = QtCore.pyqtSignal(object)
49 49
50 50 # Protected class variables.
51 51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
52 52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
53 53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
54 54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
55 55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
56 56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
57 57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
58 58 _shortcuts = set(_ctrl_down_remap.keys() +
59 59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
60 60
61 61 #---------------------------------------------------------------------------
62 62 # 'QObject' interface
63 63 #---------------------------------------------------------------------------
64 64
65 65 def __init__(self, kind='plain', paging='inside', parent=None):
66 66 """ Create a ConsoleWidget.
67 67
68 68 Parameters
69 69 ----------
70 70 kind : str, optional [default 'plain']
71 71 The type of underlying text widget to use. Valid values are 'plain',
72 72 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 73 QTextEdit.
74 74
75 75 paging : str, optional [default 'inside']
76 76 The type of paging to use. Valid values are:
77 77 'inside' : The widget pages like a traditional terminal pager.
78 78 'hsplit' : When paging is requested, the widget is split
79 79 horizontally. The top pane contains the console,
80 80 and the bottom pane contains the paged text.
81 81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 82 used.
83 83 'custom' : No action is taken by the widget beyond emitting a
84 'custom_page_requested(QString)' signal.
84 'custom_page_requested(str)' signal.
85 85 'none' : The text is written directly to the console.
86 86
87 87 parent : QWidget, optional [default None]
88 88 The parent for this widget.
89 89 """
90 90 super(ConsoleWidget, self).__init__(parent)
91 91
92 92 # Create the layout and underlying text widget.
93 93 layout = QtGui.QStackedLayout(self)
94 94 layout.setMargin(0)
95 95 self._control = self._create_control(kind)
96 96 self._page_control = None
97 97 self._splitter = None
98 98 if paging in ('hsplit', 'vsplit'):
99 99 self._splitter = QtGui.QSplitter()
100 100 if paging == 'hsplit':
101 101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 102 else:
103 103 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 104 self._splitter.addWidget(self._control)
105 105 layout.addWidget(self._splitter)
106 106 else:
107 107 layout.addWidget(self._control)
108 108
109 109 # Create the paging widget, if necessary.
110 110 self._page_style = paging
111 111 if paging in ('inside', 'hsplit', 'vsplit'):
112 112 self._page_control = self._create_page_control()
113 113 if self._splitter:
114 114 self._page_control.hide()
115 115 self._splitter.addWidget(self._page_control)
116 116 else:
117 117 layout.addWidget(self._page_control)
118 118 elif paging not in ('custom', 'none'):
119 119 raise ValueError('Paging style %s unknown.' % repr(paging))
120 120
121 121 # Initialize protected variables. Some variables contain useful state
122 122 # information for subclasses; they should be considered read-only.
123 123 self._ansi_processor = QtAnsiCodeProcessor()
124 124 self._completion_widget = CompletionWidget(self._control)
125 125 self._continuation_prompt = '> '
126 126 self._continuation_prompt_html = None
127 127 self._executing = False
128 128 self._prompt = ''
129 129 self._prompt_html = None
130 130 self._prompt_pos = 0
131 131 self._reading = False
132 132 self._reading_callback = None
133 133 self._tab_width = 8
134 134
135 135 # Set a monospaced font.
136 136 self.reset_font()
137 137
138 138 def eventFilter(self, obj, event):
139 139 """ Reimplemented to ensure a console-like behavior in the underlying
140 140 text widget.
141 141 """
142 142 # Re-map keys for all filtered widgets.
143 143 etype = event.type()
144 144 if etype == QtCore.QEvent.KeyPress and \
145 145 self._control_key_down(event.modifiers()) and \
146 146 event.key() in self._ctrl_down_remap:
147 147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
148 148 self._ctrl_down_remap[event.key()],
149 149 QtCore.Qt.NoModifier)
150 150 QtGui.qApp.sendEvent(obj, new_event)
151 151 return True
152 152
153 153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
154 154 # always unnecessary to override shortcuts, hence the check below (users
155 155 # should just use the Control key instead of the Command key).
156 156 elif etype == QtCore.QEvent.ShortcutOverride and \
157 157 sys.platform != 'darwin' and \
158 158 self._control_key_down(event.modifiers()) and \
159 159 event.key() in self._shortcuts:
160 160 event.accept()
161 161 return False
162 162
163 163 elif obj == self._control:
164 164 # Disable moving text by drag and drop.
165 165 if etype == QtCore.QEvent.DragMove:
166 166 return True
167 167
168 168 elif etype == QtCore.QEvent.KeyPress:
169 169 return self._event_filter_console_keypress(event)
170 170
171 171 elif obj == self._page_control:
172 172 if etype == QtCore.QEvent.KeyPress:
173 173 return self._event_filter_page_keypress(event)
174 174
175 175 return super(ConsoleWidget, self).eventFilter(obj, event)
176 176
177 177 #---------------------------------------------------------------------------
178 178 # 'QWidget' interface
179 179 #---------------------------------------------------------------------------
180 180
181 181 def sizeHint(self):
182 182 """ Reimplemented to suggest a size that is 80 characters wide and
183 183 25 lines high.
184 184 """
185 185 style = self.style()
186 186 opt = QtGui.QStyleOptionHeader()
187 187 font_metrics = QtGui.QFontMetrics(self.font)
188 188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189 189
190 190 width = font_metrics.width(' ') * 80
191 191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 192 if self._page_style == 'hsplit':
193 193 width = width * 2 + splitwidth
194 194
195 195 height = font_metrics.height() * 25
196 196 if self._page_style == 'vsplit':
197 197 height = height * 2 + splitwidth
198 198
199 199 return QtCore.QSize(width, height)
200 200
201 201 #---------------------------------------------------------------------------
202 202 # 'ConsoleWidget' public interface
203 203 #---------------------------------------------------------------------------
204 204
205 205 def can_paste(self):
206 206 """ Returns whether text can be pasted from the clipboard.
207 207 """
208 208 # Accept only text that can be ASCII encoded.
209 209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
210 210 text = QtGui.QApplication.clipboard().text()
211 211 if not text.isEmpty():
212 212 try:
213 213 str(text)
214 214 return True
215 215 except UnicodeEncodeError:
216 216 pass
217 217 return False
218 218
219 219 def clear(self, keep_input=False):
220 220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
221 221 restores the old input buffer when the new prompt is written.
222 222 """
223 223 self._control.clear()
224 224 if keep_input:
225 225 input_buffer = self.input_buffer
226 226 self._show_prompt()
227 227 if keep_input:
228 228 self.input_buffer = input_buffer
229 229
230 230 def copy(self):
231 231 """ Copy the current selected text to the clipboard.
232 232 """
233 233 self._control.copy()
234 234
235 235 def execute(self, source=None, hidden=False, interactive=False):
236 236 """ Executes source or the input buffer, possibly prompting for more
237 237 input.
238 238
239 239 Parameters:
240 240 -----------
241 241 source : str, optional
242 242
243 243 The source to execute. If not specified, the input buffer will be
244 244 used. If specified and 'hidden' is False, the input buffer will be
245 245 replaced with the source before execution.
246 246
247 247 hidden : bool, optional (default False)
248 248
249 249 If set, no output will be shown and the prompt will not be modified.
250 250 In other words, it will be completely invisible to the user that
251 251 an execution has occurred.
252 252
253 253 interactive : bool, optional (default False)
254 254
255 255 Whether the console is to treat the source as having been manually
256 256 entered by the user. The effect of this parameter depends on the
257 257 subclass implementation.
258 258
259 259 Raises:
260 260 -------
261 261 RuntimeError
262 262 If incomplete input is given and 'hidden' is True. In this case,
263 263 it is not possible to prompt for more input.
264 264
265 265 Returns:
266 266 --------
267 267 A boolean indicating whether the source was executed.
268 268 """
269 269 if not hidden:
270 270 if source is not None:
271 271 self.input_buffer = source
272 272
273 273 self._append_plain_text('\n')
274 274 self._executing_input_buffer = self.input_buffer
275 275 self._executing = True
276 276 self._prompt_finished()
277 277
278 278 real_source = self.input_buffer if source is None else source
279 279 complete = self._is_complete(real_source, interactive)
280 280 if complete:
281 281 if not hidden:
282 282 # The maximum block count is only in effect during execution.
283 283 # This ensures that _prompt_pos does not become invalid due to
284 284 # text truncation.
285 285 self._control.document().setMaximumBlockCount(self.buffer_size)
286 286 self._execute(real_source, hidden)
287 287 elif hidden:
288 288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
289 289 else:
290 290 self._show_continuation_prompt()
291 291
292 292 return complete
293 293
294 294 def _get_input_buffer(self):
295 295 """ The text that the user has entered entered at the current prompt.
296 296 """
297 297 # If we're executing, the input buffer may not even exist anymore due to
298 298 # the limit imposed by 'buffer_size'. Therefore, we store it.
299 299 if self._executing:
300 300 return self._executing_input_buffer
301 301
302 302 cursor = self._get_end_cursor()
303 303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
304 304 input_buffer = str(cursor.selection().toPlainText())
305 305
306 306 # Strip out continuation prompts.
307 307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
308 308
309 309 def _set_input_buffer(self, string):
310 310 """ Replaces the text in the input buffer with 'string'.
311 311 """
312 312 # For now, it is an error to modify the input buffer during execution.
313 313 if self._executing:
314 314 raise RuntimeError("Cannot change input buffer during execution.")
315 315
316 316 # Remove old text.
317 317 cursor = self._get_end_cursor()
318 318 cursor.beginEditBlock()
319 319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
320 320 cursor.removeSelectedText()
321 321
322 322 # Insert new text with continuation prompts.
323 323 lines = string.splitlines(True)
324 324 if lines:
325 325 self._append_plain_text(lines[0])
326 326 for i in xrange(1, len(lines)):
327 327 if self._continuation_prompt_html is None:
328 328 self._append_plain_text(self._continuation_prompt)
329 329 else:
330 330 self._append_html(self._continuation_prompt_html)
331 331 self._append_plain_text(lines[i])
332 332 cursor.endEditBlock()
333 333 self._control.moveCursor(QtGui.QTextCursor.End)
334 334
335 335 input_buffer = property(_get_input_buffer, _set_input_buffer)
336 336
337 337 def _get_font(self):
338 338 """ The base font being used by the ConsoleWidget.
339 339 """
340 340 return self._control.document().defaultFont()
341 341
342 342 def _set_font(self, font):
343 343 """ Sets the base font for the ConsoleWidget to the specified QFont.
344 344 """
345 345 font_metrics = QtGui.QFontMetrics(font)
346 346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
347 347
348 348 self._completion_widget.setFont(font)
349 349 self._control.document().setDefaultFont(font)
350 350 if self._page_control:
351 351 self._page_control.document().setDefaultFont(font)
352 352
353 353 font = property(_get_font, _set_font)
354 354
355 355 def paste(self):
356 356 """ Paste the contents of the clipboard into the input region.
357 357 """
358 358 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
359 359 try:
360 360 text = str(QtGui.QApplication.clipboard().text())
361 361 except UnicodeEncodeError:
362 362 pass
363 363 else:
364 364 self._insert_plain_text_into_buffer(dedent(text))
365 365
366 366 def print_(self, printer):
367 367 """ Print the contents of the ConsoleWidget to the specified QPrinter.
368 368 """
369 369 self._control.print_(printer)
370 370
371 371 def redo(self):
372 372 """ Redo the last operation. If there is no operation to redo, nothing
373 373 happens.
374 374 """
375 375 self._control.redo()
376 376
377 377 def reset_font(self):
378 378 """ Sets the font to the default fixed-width font for this platform.
379 379 """
380 380 if sys.platform == 'win32':
381 381 name = 'Courier'
382 382 elif sys.platform == 'darwin':
383 383 name = 'Monaco'
384 384 else:
385 385 name = 'Monospace'
386 386 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
387 387 font.setStyleHint(QtGui.QFont.TypeWriter)
388 388 self._set_font(font)
389 389
390 390 def select_all(self):
391 391 """ Selects all the text in the buffer.
392 392 """
393 393 self._control.selectAll()
394 394
395 395 def _get_tab_width(self):
396 396 """ The width (in terms of space characters) for tab characters.
397 397 """
398 398 return self._tab_width
399 399
400 400 def _set_tab_width(self, tab_width):
401 401 """ Sets the width (in terms of space characters) for tab characters.
402 402 """
403 403 font_metrics = QtGui.QFontMetrics(self.font)
404 404 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
405 405
406 406 self._tab_width = tab_width
407 407
408 408 tab_width = property(_get_tab_width, _set_tab_width)
409 409
410 410 def undo(self):
411 411 """ Undo the last operation. If there is no operation to undo, nothing
412 412 happens.
413 413 """
414 414 self._control.undo()
415 415
416 416 #---------------------------------------------------------------------------
417 417 # 'ConsoleWidget' abstract interface
418 418 #---------------------------------------------------------------------------
419 419
420 420 def _is_complete(self, source, interactive):
421 421 """ Returns whether 'source' can be executed. When triggered by an
422 422 Enter/Return key press, 'interactive' is True; otherwise, it is
423 423 False.
424 424 """
425 425 raise NotImplementedError
426 426
427 427 def _execute(self, source, hidden):
428 428 """ Execute 'source'. If 'hidden', do not show any output.
429 429 """
430 430 raise NotImplementedError
431 431
432 432 def _execute_interrupt(self):
433 433 """ Attempts to stop execution. Returns whether this method has an
434 434 implementation.
435 435 """
436 436 return False
437 437
438 438 def _prompt_started_hook(self):
439 439 """ Called immediately after a new prompt is displayed.
440 440 """
441 441 pass
442 442
443 443 def _prompt_finished_hook(self):
444 444 """ Called immediately after a prompt is finished, i.e. when some input
445 445 will be processed and a new prompt displayed.
446 446 """
447 447 pass
448 448
449 449 def _up_pressed(self):
450 450 """ Called when the up key is pressed. Returns whether to continue
451 451 processing the event.
452 452 """
453 453 return True
454 454
455 455 def _down_pressed(self):
456 456 """ Called when the down key is pressed. Returns whether to continue
457 457 processing the event.
458 458 """
459 459 return True
460 460
461 461 def _tab_pressed(self):
462 462 """ Called when the tab key is pressed. Returns whether to continue
463 463 processing the event.
464 464 """
465 465 return False
466 466
467 467 #--------------------------------------------------------------------------
468 468 # 'ConsoleWidget' protected interface
469 469 #--------------------------------------------------------------------------
470 470
471 471 def _append_html(self, html):
472 472 """ Appends html at the end of the console buffer.
473 473 """
474 474 cursor = self._get_end_cursor()
475 475 self._insert_html(cursor, html)
476 476
477 477 def _append_html_fetching_plain_text(self, html):
478 478 """ Appends 'html', then returns the plain text version of it.
479 479 """
480 480 cursor = self._get_end_cursor()
481 481 return self._insert_html_fetching_plain_text(cursor, html)
482 482
483 483 def _append_plain_text(self, text):
484 484 """ Appends plain text at the end of the console buffer, processing
485 485 ANSI codes if enabled.
486 486 """
487 487 cursor = self._get_end_cursor()
488 488 self._insert_plain_text(cursor, text)
489 489
490 490 def _append_plain_text_keeping_prompt(self, text):
491 491 """ Writes 'text' after the current prompt, then restores the old prompt
492 492 with its old input buffer.
493 493 """
494 494 input_buffer = self.input_buffer
495 495 self._append_plain_text('\n')
496 496 self._prompt_finished()
497 497
498 498 self._append_plain_text(text)
499 499 self._show_prompt()
500 500 self.input_buffer = input_buffer
501 501
502 502 def _complete_with_items(self, cursor, items):
503 503 """ Performs completion with 'items' at the specified cursor location.
504 504 """
505 505 if len(items) == 1:
506 506 cursor.setPosition(self._control.textCursor().position(),
507 507 QtGui.QTextCursor.KeepAnchor)
508 508 cursor.insertText(items[0])
509 509 elif len(items) > 1:
510 510 if self.gui_completion:
511 511 self._completion_widget.show_items(cursor, items)
512 512 else:
513 513 text = self._format_as_columns(items)
514 514 self._append_plain_text_keeping_prompt(text)
515 515
516 516 def _control_key_down(self, modifiers):
517 517 """ Given a KeyboardModifiers flags object, return whether the Control
518 518 key is down (on Mac OS, treat the Command key as a synonym for
519 519 Control).
520 520 """
521 521 down = bool(modifiers & QtCore.Qt.ControlModifier)
522 522
523 523 # Note: on Mac OS, ControlModifier corresponds to the Command key while
524 524 # MetaModifier corresponds to the Control key.
525 525 if sys.platform == 'darwin':
526 526 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
527 527
528 528 return down
529 529
530 530 def _create_control(self, kind):
531 531 """ Creates and connects the underlying text widget.
532 532 """
533 533 if kind == 'plain':
534 534 control = QtGui.QPlainTextEdit()
535 535 elif kind == 'rich':
536 536 control = QtGui.QTextEdit()
537 537 control.setAcceptRichText(False)
538 538 else:
539 539 raise ValueError("Kind %s unknown." % repr(kind))
540 540 control.installEventFilter(self)
541 541 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
542 542 control.customContextMenuRequested.connect(self._show_context_menu)
543 543 control.copyAvailable.connect(self.copy_available)
544 544 control.redoAvailable.connect(self.redo_available)
545 545 control.undoAvailable.connect(self.undo_available)
546 546 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
547 547 return control
548 548
549 549 def _create_page_control(self):
550 550 """ Creates and connects the underlying paging widget.
551 551 """
552 552 control = QtGui.QPlainTextEdit()
553 553 control.installEventFilter(self)
554 554 control.setReadOnly(True)
555 555 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
556 556 return control
557 557
558 558 def _event_filter_console_keypress(self, event):
559 559 """ Filter key events for the underlying text widget to create a
560 560 console-like interface.
561 561 """
562 562 intercepted = False
563 563 cursor = self._control.textCursor()
564 564 position = cursor.position()
565 565 key = event.key()
566 566 ctrl_down = self._control_key_down(event.modifiers())
567 567 alt_down = event.modifiers() & QtCore.Qt.AltModifier
568 568 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
569 569
570 570 if event.matches(QtGui.QKeySequence.Paste):
571 571 # Call our paste instead of the underlying text widget's.
572 572 self.paste()
573 573 intercepted = True
574 574
575 575 elif ctrl_down:
576 576 if key == QtCore.Qt.Key_C:
577 577 intercepted = self._executing and self._execute_interrupt()
578 578
579 579 elif key == QtCore.Qt.Key_K:
580 580 if self._in_buffer(position):
581 581 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
582 582 QtGui.QTextCursor.KeepAnchor)
583 583 cursor.removeSelectedText()
584 584 intercepted = True
585 585
586 586 elif key == QtCore.Qt.Key_X:
587 587 intercepted = True
588 588
589 589 elif key == QtCore.Qt.Key_Y:
590 590 self.paste()
591 591 intercepted = True
592 592
593 593 elif alt_down:
594 594 if key == QtCore.Qt.Key_B:
595 595 self._set_cursor(self._get_word_start_cursor(position))
596 596 intercepted = True
597 597
598 598 elif key == QtCore.Qt.Key_F:
599 599 self._set_cursor(self._get_word_end_cursor(position))
600 600 intercepted = True
601 601
602 602 elif key == QtCore.Qt.Key_Backspace:
603 603 cursor = self._get_word_start_cursor(position)
604 604 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
605 605 cursor.removeSelectedText()
606 606 intercepted = True
607 607
608 608 elif key == QtCore.Qt.Key_D:
609 609 cursor = self._get_word_end_cursor(position)
610 610 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
611 611 cursor.removeSelectedText()
612 612 intercepted = True
613 613
614 614 else:
615 615 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
616 616 if self._reading:
617 617 self._append_plain_text('\n')
618 618 self._reading = False
619 619 if self._reading_callback:
620 620 self._reading_callback()
621 621 elif not self._executing:
622 622 self.execute(interactive=True)
623 623 intercepted = True
624 624
625 625 elif key == QtCore.Qt.Key_Up:
626 626 if self._reading or not self._up_pressed():
627 627 intercepted = True
628 628 else:
629 629 prompt_line = self._get_prompt_cursor().blockNumber()
630 630 intercepted = cursor.blockNumber() <= prompt_line
631 631
632 632 elif key == QtCore.Qt.Key_Down:
633 633 if self._reading or not self._down_pressed():
634 634 intercepted = True
635 635 else:
636 636 end_line = self._get_end_cursor().blockNumber()
637 637 intercepted = cursor.blockNumber() == end_line
638 638
639 639 elif key == QtCore.Qt.Key_Tab:
640 640 if self._reading:
641 641 intercepted = False
642 642 else:
643 643 intercepted = not self._tab_pressed()
644 644
645 645 elif key == QtCore.Qt.Key_Left:
646 646 intercepted = not self._in_buffer(position - 1)
647 647
648 648 elif key == QtCore.Qt.Key_Home:
649 649 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
650 650 start_line = cursor.blockNumber()
651 651 if start_line == self._get_prompt_cursor().blockNumber():
652 652 start_pos = self._prompt_pos
653 653 else:
654 654 start_pos = cursor.position()
655 655 start_pos += len(self._continuation_prompt)
656 656 if shift_down and self._in_buffer(position):
657 657 self._set_selection(position, start_pos)
658 658 else:
659 659 self._set_position(start_pos)
660 660 intercepted = True
661 661
662 662 elif key == QtCore.Qt.Key_Backspace:
663 663
664 664 # Line deletion (remove continuation prompt)
665 665 len_prompt = len(self._continuation_prompt)
666 666 if not self._reading and \
667 667 cursor.columnNumber() == len_prompt and \
668 668 position != self._prompt_pos:
669 669 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
670 670 QtGui.QTextCursor.KeepAnchor)
671 671 cursor.removeSelectedText()
672 672 cursor.deletePreviousChar()
673 673 intercepted = True
674 674
675 675 # Regular backwards deletion
676 676 else:
677 677 anchor = cursor.anchor()
678 678 if anchor == position:
679 679 intercepted = not self._in_buffer(position - 1)
680 680 else:
681 681 intercepted = not self._in_buffer(min(anchor, position))
682 682
683 683 elif key == QtCore.Qt.Key_Delete:
684 684 anchor = cursor.anchor()
685 685 intercepted = not self._in_buffer(min(anchor, position))
686 686
687 687 # Don't move the cursor if control is down to allow copy-paste using
688 688 # the keyboard in any part of the buffer.
689 689 if not ctrl_down:
690 690 self._keep_cursor_in_buffer()
691 691
692 692 return intercepted
693 693
694 694 def _event_filter_page_keypress(self, event):
695 695 """ Filter key events for the paging widget to create console-like
696 696 interface.
697 697 """
698 698 key = event.key()
699 699
700 700 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
701 701 if self._splitter:
702 702 self._page_control.hide()
703 703 else:
704 704 self.layout().setCurrentWidget(self._control)
705 705 return True
706 706
707 707 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
708 708 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
709 709 QtCore.Qt.Key_Down,
710 710 QtCore.Qt.NoModifier)
711 711 QtGui.qApp.sendEvent(self._page_control, new_event)
712 712 return True
713 713
714 714 return False
715 715
716 716 def _format_as_columns(self, items, separator=' '):
717 717 """ Transform a list of strings into a single string with columns.
718 718
719 719 Parameters
720 720 ----------
721 721 items : sequence of strings
722 722 The strings to process.
723 723
724 724 separator : str, optional [default is two spaces]
725 725 The string that separates columns.
726 726
727 727 Returns
728 728 -------
729 729 The formatted string.
730 730 """
731 731 # Note: this code is adapted from columnize 0.3.2.
732 732 # See http://code.google.com/p/pycolumnize/
733 733
734 734 width = self._control.viewport().width()
735 735 char_width = QtGui.QFontMetrics(self.font).width(' ')
736 736 displaywidth = max(5, width / char_width)
737 737
738 738 # Some degenerate cases.
739 739 size = len(items)
740 740 if size == 0:
741 741 return '\n'
742 742 elif size == 1:
743 743 return '%s\n' % str(items[0])
744 744
745 745 # Try every row count from 1 upwards
746 746 array_index = lambda nrows, row, col: nrows*col + row
747 747 for nrows in range(1, size):
748 748 ncols = (size + nrows - 1) // nrows
749 749 colwidths = []
750 750 totwidth = -len(separator)
751 751 for col in range(ncols):
752 752 # Get max column width for this column
753 753 colwidth = 0
754 754 for row in range(nrows):
755 755 i = array_index(nrows, row, col)
756 756 if i >= size: break
757 757 x = items[i]
758 758 colwidth = max(colwidth, len(x))
759 759 colwidths.append(colwidth)
760 760 totwidth += colwidth + len(separator)
761 761 if totwidth > displaywidth:
762 762 break
763 763 if totwidth <= displaywidth:
764 764 break
765 765
766 766 # The smallest number of rows computed and the max widths for each
767 767 # column has been obtained. Now we just have to format each of the rows.
768 768 string = ''
769 769 for row in range(nrows):
770 770 texts = []
771 771 for col in range(ncols):
772 772 i = row + nrows*col
773 773 if i >= size:
774 774 texts.append('')
775 775 else:
776 776 texts.append(items[i])
777 777 while texts and not texts[-1]:
778 778 del texts[-1]
779 779 for col in range(len(texts)):
780 780 texts[col] = texts[col].ljust(colwidths[col])
781 781 string += '%s\n' % str(separator.join(texts))
782 782 return string
783 783
784 784 def _get_block_plain_text(self, block):
785 785 """ Given a QTextBlock, return its unformatted text.
786 786 """
787 787 cursor = QtGui.QTextCursor(block)
788 788 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
789 789 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
790 790 QtGui.QTextCursor.KeepAnchor)
791 791 return str(cursor.selection().toPlainText())
792 792
793 793 def _get_cursor(self):
794 794 """ Convenience method that returns a cursor for the current position.
795 795 """
796 796 return self._control.textCursor()
797 797
798 798 def _get_end_cursor(self):
799 799 """ Convenience method that returns a cursor for the last character.
800 800 """
801 801 cursor = self._control.textCursor()
802 802 cursor.movePosition(QtGui.QTextCursor.End)
803 803 return cursor
804 804
805 805 def _get_input_buffer_cursor_line(self):
806 806 """ The text in the line of the input buffer in which the user's cursor
807 807 rests. Returns a string if there is such a line; otherwise, None.
808 808 """
809 809 if self._executing:
810 810 return None
811 811 cursor = self._control.textCursor()
812 812 if cursor.position() >= self._prompt_pos:
813 813 text = self._get_block_plain_text(cursor.block())
814 814 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
815 815 return text[len(self._prompt):]
816 816 else:
817 817 return text[len(self._continuation_prompt):]
818 818 else:
819 819 return None
820 820
821 821 def _get_prompt_cursor(self):
822 822 """ Convenience method that returns a cursor for the prompt position.
823 823 """
824 824 cursor = self._control.textCursor()
825 825 cursor.setPosition(self._prompt_pos)
826 826 return cursor
827 827
828 828 def _get_selection_cursor(self, start, end):
829 829 """ Convenience method that returns a cursor with text selected between
830 830 the positions 'start' and 'end'.
831 831 """
832 832 cursor = self._control.textCursor()
833 833 cursor.setPosition(start)
834 834 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
835 835 return cursor
836 836
837 837 def _get_word_start_cursor(self, position):
838 838 """ Find the start of the word to the left the given position. If a
839 839 sequence of non-word characters precedes the first word, skip over
840 840 them. (This emulates the behavior of bash, emacs, etc.)
841 841 """
842 842 document = self._control.document()
843 843 position -= 1
844 844 while position >= self._prompt_pos and \
845 845 not document.characterAt(position).isLetterOrNumber():
846 846 position -= 1
847 847 while position >= self._prompt_pos and \
848 848 document.characterAt(position).isLetterOrNumber():
849 849 position -= 1
850 850 cursor = self._control.textCursor()
851 851 cursor.setPosition(position + 1)
852 852 return cursor
853 853
854 854 def _get_word_end_cursor(self, position):
855 855 """ Find the end of the word to the right the given position. If a
856 856 sequence of non-word characters precedes the first word, skip over
857 857 them. (This emulates the behavior of bash, emacs, etc.)
858 858 """
859 859 document = self._control.document()
860 860 end = self._get_end_cursor().position()
861 861 while position < end and \
862 862 not document.characterAt(position).isLetterOrNumber():
863 863 position += 1
864 864 while position < end and \
865 865 document.characterAt(position).isLetterOrNumber():
866 866 position += 1
867 867 cursor = self._control.textCursor()
868 868 cursor.setPosition(position)
869 869 return cursor
870 870
871 871 def _insert_html(self, cursor, html):
872 872 """ Inserts HTML using the specified cursor in such a way that future
873 873 formatting is unaffected.
874 874 """
875 875 cursor.beginEditBlock()
876 876 cursor.insertHtml(html)
877 877
878 878 # After inserting HTML, the text document "remembers" it's in "html
879 879 # mode", which means that subsequent calls adding plain text will result
880 880 # in unwanted formatting, lost tab characters, etc. The following code
881 881 # hacks around this behavior, which I consider to be a bug in Qt.
882 882 cursor.movePosition(QtGui.QTextCursor.Left,
883 883 QtGui.QTextCursor.KeepAnchor)
884 884 if cursor.selection().toPlainText() == ' ':
885 885 cursor.removeSelectedText()
886 886 cursor.movePosition(QtGui.QTextCursor.Right)
887 887 cursor.insertText(' ', QtGui.QTextCharFormat())
888 888 cursor.endEditBlock()
889 889
890 890 def _insert_html_fetching_plain_text(self, cursor, html):
891 891 """ Inserts HTML using the specified cursor, then returns its plain text
892 892 version.
893 893 """
894 894 start = cursor.position()
895 895 self._insert_html(cursor, html)
896 896 end = cursor.position()
897 897 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
898 898 text = str(cursor.selection().toPlainText())
899 899 cursor.setPosition(end)
900 900 return text
901 901
902 902 def _insert_plain_text(self, cursor, text):
903 903 """ Inserts plain text using the specified cursor, processing ANSI codes
904 904 if enabled.
905 905 """
906 906 cursor.beginEditBlock()
907 907 if self.ansi_codes:
908 908 for substring in self._ansi_processor.split_string(text):
909 909 for action in self._ansi_processor.actions:
910 910 if action.kind == 'erase' and action.area == 'screen':
911 911 cursor.select(QtGui.QTextCursor.Document)
912 912 cursor.removeSelectedText()
913 913 format = self._ansi_processor.get_format()
914 914 cursor.insertText(substring, format)
915 915 else:
916 916 cursor.insertText(text)
917 917 cursor.endEditBlock()
918 918
919 919 def _insert_plain_text_into_buffer(self, text):
920 920 """ Inserts text into the input buffer at the current cursor position,
921 921 ensuring that continuation prompts are inserted as necessary.
922 922 """
923 923 lines = str(text).splitlines(True)
924 924 if lines:
925 925 self._keep_cursor_in_buffer()
926 926 cursor = self._control.textCursor()
927 927 cursor.beginEditBlock()
928 928 cursor.insertText(lines[0])
929 929 for line in lines[1:]:
930 930 if self._continuation_prompt_html is None:
931 931 cursor.insertText(self._continuation_prompt)
932 932 else:
933 933 self._continuation_prompt = \
934 934 self._insert_html_fetching_plain_text(
935 935 cursor, self._continuation_prompt_html)
936 936 cursor.insertText(line)
937 937 cursor.endEditBlock()
938 938 self._control.setTextCursor(cursor)
939 939
940 940 def _in_buffer(self, position):
941 941 """ Returns whether the given position is inside the editing region.
942 942 """
943 943 cursor = self._control.textCursor()
944 944 cursor.setPosition(position)
945 945 line = cursor.blockNumber()
946 946 prompt_line = self._get_prompt_cursor().blockNumber()
947 947 if line == prompt_line:
948 948 return position >= self._prompt_pos
949 949 elif line > prompt_line:
950 950 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
951 951 prompt_pos = cursor.position() + len(self._continuation_prompt)
952 952 return position >= prompt_pos
953 953 return False
954 954
955 955 def _keep_cursor_in_buffer(self):
956 956 """ Ensures that the cursor is inside the editing region. Returns
957 957 whether the cursor was moved.
958 958 """
959 959 cursor = self._control.textCursor()
960 960 if self._in_buffer(cursor.position()):
961 961 return False
962 962 else:
963 963 cursor.movePosition(QtGui.QTextCursor.End)
964 964 self._control.setTextCursor(cursor)
965 965 return True
966 966
967 967 def _page(self, text):
968 968 """ Displays text using the pager.
969 969 """
970 970 if self._page_style == 'custom':
971 971 self.custom_page_requested.emit(text)
972 972 elif self._page_style == 'none':
973 973 self._append_plain_text(text)
974 974 else:
975 975 self._page_control.clear()
976 976 cursor = self._page_control.textCursor()
977 977 self._insert_plain_text(cursor, text)
978 978 self._page_control.moveCursor(QtGui.QTextCursor.Start)
979 979
980 980 self._page_control.viewport().resize(self._control.size())
981 981 if self._splitter:
982 982 self._page_control.show()
983 983 self._page_control.setFocus()
984 984 else:
985 985 self.layout().setCurrentWidget(self._page_control)
986 986
987 987 def _prompt_started(self):
988 988 """ Called immediately after a new prompt is displayed.
989 989 """
990 990 # Temporarily disable the maximum block count to permit undo/redo and
991 991 # to ensure that the prompt position does not change due to truncation.
992 992 self._control.document().setMaximumBlockCount(0)
993 993 self._control.setUndoRedoEnabled(True)
994 994
995 995 self._control.setReadOnly(False)
996 996 self._control.moveCursor(QtGui.QTextCursor.End)
997 997
998 998 self._executing = False
999 999 self._prompt_started_hook()
1000 1000
1001 1001 def _prompt_finished(self):
1002 1002 """ Called immediately after a prompt is finished, i.e. when some input
1003 1003 will be processed and a new prompt displayed.
1004 1004 """
1005 1005 self._control.setUndoRedoEnabled(False)
1006 1006 self._control.setReadOnly(True)
1007 1007 self._prompt_finished_hook()
1008 1008
1009 1009 def _readline(self, prompt='', callback=None):
1010 1010 """ Reads one line of input from the user.
1011 1011
1012 1012 Parameters
1013 1013 ----------
1014 1014 prompt : str, optional
1015 1015 The prompt to print before reading the line.
1016 1016
1017 1017 callback : callable, optional
1018 1018 A callback to execute with the read line. If not specified, input is
1019 1019 read *synchronously* and this method does not return until it has
1020 1020 been read.
1021 1021
1022 1022 Returns
1023 1023 -------
1024 1024 If a callback is specified, returns nothing. Otherwise, returns the
1025 1025 input string with the trailing newline stripped.
1026 1026 """
1027 1027 if self._reading:
1028 1028 raise RuntimeError('Cannot read a line. Widget is already reading.')
1029 1029
1030 1030 if not callback and not self.isVisible():
1031 1031 # If the user cannot see the widget, this function cannot return.
1032 1032 raise RuntimeError('Cannot synchronously read a line if the widget'
1033 1033 'is not visible!')
1034 1034
1035 1035 self._reading = True
1036 1036 self._show_prompt(prompt, newline=False)
1037 1037
1038 1038 if callback is None:
1039 1039 self._reading_callback = None
1040 1040 while self._reading:
1041 1041 QtCore.QCoreApplication.processEvents()
1042 1042 return self.input_buffer.rstrip('\n')
1043 1043
1044 1044 else:
1045 1045 self._reading_callback = lambda: \
1046 1046 callback(self.input_buffer.rstrip('\n'))
1047 1047
1048 1048 def _reset(self):
1049 1049 """ Clears the console and resets internal state variables.
1050 1050 """
1051 1051 self._control.clear()
1052 1052 self._executing = self._reading = False
1053 1053
1054 1054 def _set_continuation_prompt(self, prompt, html=False):
1055 1055 """ Sets the continuation prompt.
1056 1056
1057 1057 Parameters
1058 1058 ----------
1059 1059 prompt : str
1060 1060 The prompt to show when more input is needed.
1061 1061
1062 1062 html : bool, optional (default False)
1063 1063 If set, the prompt will be inserted as formatted HTML. Otherwise,
1064 1064 the prompt will be treated as plain text, though ANSI color codes
1065 1065 will be handled.
1066 1066 """
1067 1067 if html:
1068 1068 self._continuation_prompt_html = prompt
1069 1069 else:
1070 1070 self._continuation_prompt = prompt
1071 1071 self._continuation_prompt_html = None
1072 1072
1073 1073 def _set_cursor(self, cursor):
1074 1074 """ Convenience method to set the current cursor.
1075 1075 """
1076 1076 self._control.setTextCursor(cursor)
1077 1077
1078 1078 def _set_position(self, position):
1079 1079 """ Convenience method to set the position of the cursor.
1080 1080 """
1081 1081 cursor = self._control.textCursor()
1082 1082 cursor.setPosition(position)
1083 1083 self._control.setTextCursor(cursor)
1084 1084
1085 1085 def _set_selection(self, start, end):
1086 1086 """ Convenience method to set the current selected text.
1087 1087 """
1088 1088 self._control.setTextCursor(self._get_selection_cursor(start, end))
1089 1089
1090 1090 def _show_context_menu(self, pos):
1091 1091 """ Shows a context menu at the given QPoint (in widget coordinates).
1092 1092 """
1093 1093 menu = QtGui.QMenu()
1094 1094
1095 1095 copy_action = menu.addAction('Copy', self.copy)
1096 1096 copy_action.setEnabled(self._get_cursor().hasSelection())
1097 1097 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1098 1098
1099 1099 paste_action = menu.addAction('Paste', self.paste)
1100 1100 paste_action.setEnabled(self.can_paste())
1101 1101 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1102 1102
1103 1103 menu.addSeparator()
1104 1104 menu.addAction('Select All', self.select_all)
1105 1105
1106 1106 menu.exec_(self._control.mapToGlobal(pos))
1107 1107
1108 1108 def _show_prompt(self, prompt=None, html=False, newline=True):
1109 1109 """ Writes a new prompt at the end of the buffer.
1110 1110
1111 1111 Parameters
1112 1112 ----------
1113 1113 prompt : str, optional
1114 1114 The prompt to show. If not specified, the previous prompt is used.
1115 1115
1116 1116 html : bool, optional (default False)
1117 1117 Only relevant when a prompt is specified. If set, the prompt will
1118 1118 be inserted as formatted HTML. Otherwise, the prompt will be treated
1119 1119 as plain text, though ANSI color codes will be handled.
1120 1120
1121 1121 newline : bool, optional (default True)
1122 1122 If set, a new line will be written before showing the prompt if
1123 1123 there is not already a newline at the end of the buffer.
1124 1124 """
1125 1125 # Insert a preliminary newline, if necessary.
1126 1126 if newline:
1127 1127 cursor = self._get_end_cursor()
1128 1128 if cursor.position() > 0:
1129 1129 cursor.movePosition(QtGui.QTextCursor.Left,
1130 1130 QtGui.QTextCursor.KeepAnchor)
1131 1131 if str(cursor.selection().toPlainText()) != '\n':
1132 1132 self._append_plain_text('\n')
1133 1133
1134 1134 # Write the prompt.
1135 1135 if prompt is None:
1136 1136 if self._prompt_html is None:
1137 1137 self._append_plain_text(self._prompt)
1138 1138 else:
1139 1139 self._append_html(self._prompt_html)
1140 1140 else:
1141 1141 if html:
1142 1142 self._prompt = self._append_html_fetching_plain_text(prompt)
1143 1143 self._prompt_html = prompt
1144 1144 else:
1145 1145 self._append_plain_text(prompt)
1146 1146 self._prompt = prompt
1147 1147 self._prompt_html = None
1148 1148
1149 1149 self._prompt_pos = self._get_end_cursor().position()
1150 1150 self._prompt_started()
1151 1151
1152 1152 def _show_continuation_prompt(self):
1153 1153 """ Writes a new continuation prompt at the end of the buffer.
1154 1154 """
1155 1155 if self._continuation_prompt_html is None:
1156 1156 self._append_plain_text(self._continuation_prompt)
1157 1157 else:
1158 1158 self._continuation_prompt = self._append_html_fetching_plain_text(
1159 1159 self._continuation_prompt_html)
1160 1160
1161 1161 self._prompt_started()
1162 1162
1163 1163
1164 1164 class HistoryConsoleWidget(ConsoleWidget):
1165 1165 """ A ConsoleWidget that keeps a history of the commands that have been
1166 1166 executed.
1167 1167 """
1168 1168
1169 1169 #---------------------------------------------------------------------------
1170 1170 # 'object' interface
1171 1171 #---------------------------------------------------------------------------
1172 1172
1173 1173 def __init__(self, *args, **kw):
1174 1174 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1175 1175 self._history = []
1176 1176 self._history_index = 0
1177 1177
1178 1178 #---------------------------------------------------------------------------
1179 1179 # 'ConsoleWidget' public interface
1180 1180 #---------------------------------------------------------------------------
1181 1181
1182 1182 def execute(self, source=None, hidden=False, interactive=False):
1183 1183 """ Reimplemented to the store history.
1184 1184 """
1185 1185 if not hidden:
1186 1186 history = self.input_buffer if source is None else source
1187 1187
1188 1188 executed = super(HistoryConsoleWidget, self).execute(
1189 1189 source, hidden, interactive)
1190 1190
1191 1191 if executed and not hidden:
1192 1192 self._history.append(history.rstrip())
1193 1193 self._history_index = len(self._history)
1194 1194
1195 1195 return executed
1196 1196
1197 1197 #---------------------------------------------------------------------------
1198 1198 # 'ConsoleWidget' abstract interface
1199 1199 #---------------------------------------------------------------------------
1200 1200
1201 1201 def _up_pressed(self):
1202 1202 """ Called when the up key is pressed. Returns whether to continue
1203 1203 processing the event.
1204 1204 """
1205 1205 prompt_cursor = self._get_prompt_cursor()
1206 1206 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1207 1207 self.history_previous()
1208 1208
1209 1209 # Go to the first line of prompt for seemless history scrolling.
1210 1210 cursor = self._get_prompt_cursor()
1211 1211 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1212 1212 self._set_cursor(cursor)
1213 1213
1214 1214 return False
1215 1215 return True
1216 1216
1217 1217 def _down_pressed(self):
1218 1218 """ Called when the down key is pressed. Returns whether to continue
1219 1219 processing the event.
1220 1220 """
1221 1221 end_cursor = self._get_end_cursor()
1222 1222 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1223 1223 self.history_next()
1224 1224 return False
1225 1225 return True
1226 1226
1227 1227 #---------------------------------------------------------------------------
1228 1228 # 'HistoryConsoleWidget' interface
1229 1229 #---------------------------------------------------------------------------
1230 1230
1231 1231 def history_previous(self):
1232 1232 """ If possible, set the input buffer to the previous item in the
1233 1233 history.
1234 1234 """
1235 1235 if self._history_index > 0:
1236 1236 self._history_index -= 1
1237 1237 self.input_buffer = self._history[self._history_index]
1238 1238
1239 1239 def history_next(self):
1240 1240 """ Set the input buffer to the next item in the history, or a blank
1241 1241 line if there is no subsequent item.
1242 1242 """
1243 1243 if self._history_index < len(self._history):
1244 1244 self._history_index += 1
1245 1245 if self._history_index < len(self._history):
1246 1246 self.input_buffer = self._history[self._history_index]
1247 1247 else:
1248 1248 self.input_buffer = ''
@@ -1,192 +1,247 b''
1 # Standard library imports
2 from subprocess import Popen
3
1 4 # System library imports
2 5 from PyQt4 import QtCore, QtGui
3 6
4 7 # Local imports
5 8 from IPython.core.inputsplitter import IPythonInputSplitter
6 9 from IPython.core.usage import default_banner
7 10 from frontend_widget import FrontendWidget
8 11
9 12
10 13 class IPythonWidget(FrontendWidget):
11 14 """ A FrontendWidget for an IPython kernel.
12 15 """
13 16
17 # Signal emitted when an editor is needed for a file and the editor has been
18 # specified as 'custom'.
19 custom_edit_requested = QtCore.pyqtSignal(object)
20
14 21 # The default stylesheet: black text on a white background.
15 22 default_stylesheet = """
16 23 .error { color: red; }
17 24 .in-prompt { color: navy; }
18 25 .in-prompt-number { font-weight: bold; }
19 26 .out-prompt { color: darkred; }
20 27 .out-prompt-number { font-weight: bold; }
21 28 """
22 29
23 30 # A dark stylesheet: white text on a black background.
24 31 dark_stylesheet = """
25 32 QPlainTextEdit { background-color: black; color: white }
26 33 QFrame { border: 1px solid grey; }
27 34 .error { color: red; }
28 35 .in-prompt { color: lime; }
29 36 .in-prompt-number { color: lime; font-weight: bold; }
30 37 .out-prompt { color: red; }
31 38 .out-prompt-number { color: red; font-weight: bold; }
32 39 """
33 40
34 41 # Default prompts.
35 42 in_prompt = '<br/>In [<span class="in-prompt-number">%i</span>]: '
36 43 out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
37 44
38 45 #---------------------------------------------------------------------------
39 46 # 'object' interface
40 47 #---------------------------------------------------------------------------
41 48
42 49 def __init__(self, *args, **kw):
43 50 super(IPythonWidget, self).__init__(*args, **kw)
44 51
45 52 # FrontendWidget protected variables.
46 self._input_splitter = IPythonInputSplitter(input_mode='replace')
53 #self._input_splitter = IPythonInputSplitter(input_mode='replace')
47 54
48 55 # IPythonWidget protected variables.
49 56 self._previous_prompt_blocks = []
50 57 self._prompt_count = 0
51 58
52 # Set a default stylesheet.
59 # Set a default editor and stylesheet.
60 self.set_editor('default')
53 61 self.reset_styling()
54 62
55 63 #---------------------------------------------------------------------------
56 64 # 'BaseFrontendMixin' abstract interface
57 65 #---------------------------------------------------------------------------
58 66
59 67 def _handle_pyout(self, msg):
60 68 """ Reimplemented for IPython-style "display hook".
61 69 """
62 70 self._append_html(self._make_out_prompt(self._prompt_count))
63 71 self._save_prompt_block()
64 72
65 73 self._append_plain_text(msg['content']['data'] + '\n')
66 74
67 75 #---------------------------------------------------------------------------
68 76 # 'FrontendWidget' interface
69 77 #---------------------------------------------------------------------------
70 78
71 79 def execute_file(self, path, hidden=False):
72 80 """ Reimplemented to use the 'run' magic.
73 81 """
74 82 self.execute('run %s' % path, hidden=hidden)
75 83
76 84 #---------------------------------------------------------------------------
77 85 # 'FrontendWidget' protected interface
78 86 #---------------------------------------------------------------------------
79 87
80 88 def _get_banner(self):
81 89 """ Reimplemented to return IPython's default banner.
82 90 """
83 91 return default_banner
84 92
85 93 def _process_execute_error(self, msg):
86 94 """ Reimplemented for IPython-style traceback formatting.
87 95 """
88 96 content = msg['content']
89 97 traceback_lines = content['traceback'][:]
90 98 traceback = ''.join(traceback_lines)
91 99 traceback = traceback.replace(' ', '&nbsp;')
92 100 traceback = traceback.replace('\n', '<br/>')
93 101
94 102 ename = content['ename']
95 103 ename_styled = '<span class="error">%s</span>' % ename
96 104 traceback = traceback.replace(ename, ename_styled)
97 105
98 106 self._append_html(traceback)
99 107
100 108 def _show_interpreter_prompt(self):
101 109 """ Reimplemented for IPython-style prompts.
102 110 """
103 111 # Update old prompt numbers if necessary.
104 112 previous_prompt_number = self._prompt_count
105 113 if previous_prompt_number != self._prompt_count:
106 114 for i, (block, length) in enumerate(self._previous_prompt_blocks):
107 115 if block.isValid():
108 116 cursor = QtGui.QTextCursor(block)
109 117 cursor.movePosition(QtGui.QTextCursor.Right,
110 118 QtGui.QTextCursor.KeepAnchor, length-1)
111 119 if i == 0:
112 120 prompt = self._make_in_prompt(previous_prompt_number)
113 121 else:
114 122 prompt = self._make_out_prompt(previous_prompt_number)
115 123 self._insert_html(cursor, prompt)
116 124 self._previous_prompt_blocks = []
117 125
118 126 # Show a new prompt.
119 127 self._prompt_count += 1
120 128 self._show_prompt(self._make_in_prompt(self._prompt_count), html=True)
121 129 self._save_prompt_block()
122 130
123 131 # Update continuation prompt to reflect (possibly) new prompt length.
124 132 self._set_continuation_prompt(
125 133 self._make_continuation_prompt(self._prompt), html=True)
126 134
127 135 #---------------------------------------------------------------------------
128 136 # 'IPythonWidget' interface
129 137 #---------------------------------------------------------------------------
130 138
139 def edit(self, filename):
140 """ Opens a Python script for editing.
141
142 Parameters:
143 -----------
144 filename : str
145 A path to a local system file.
146
147 Raises:
148 -------
149 OSError
150 If the editor command cannot be executed.
151 """
152 if self._editor == 'default':
153 url = QtCore.QUrl.fromLocalFile(filename)
154 if not QtGui.QDesktopServices.openUrl(url):
155 message = 'Failed to open %s with the default application'
156 raise OSError(message % repr(filename))
157 elif self._editor is None:
158 self.custom_edit_requested.emit(filename)
159 else:
160 Popen(self._editor + [filename])
161
131 162 def reset_styling(self):
132 163 """ Restores the default IPythonWidget styling.
133 164 """
134 165 self.set_styling(self.default_stylesheet, syntax_style='default')
135 166 #self.set_styling(self.dark_stylesheet, syntax_style='monokai')
136 167
168 def set_editor(self, editor):
169 """ Sets the editor to use with the %edit magic.
170
171 Parameters:
172 -----------
173 editor : str or sequence of str
174 A command suitable for use with Popen. This command will be executed
175 with a single argument--a filename--when editing is requested.
176
177 This parameter also takes two special values:
178 'default' : Files will be edited with the system default
179 application for Python files.
180 'custom' : Emit a 'custom_edit_requested(str)' signal instead
181 of opening an editor.
182 """
183 if editor == 'default':
184 self._editor = 'default'
185 elif editor == 'custom':
186 self._editor = None
187 elif isinstance(editor, basestring):
188 self._editor = [ editor ]
189 else:
190 self._editor = list(editor)
191
137 192 def set_styling(self, stylesheet, syntax_style=None):
138 193 """ Sets the IPythonWidget styling.
139 194
140 195 Parameters:
141 196 -----------
142 197 stylesheet : str
143 198 A CSS stylesheet. The stylesheet can contain classes for:
144 199 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
145 200 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
146 201 3. IPython: .error, .in-prompt, .out-prompt, etc.
147 202
148 203 syntax_style : str or None [default None]
149 204 If specified, use the Pygments style with given name. Otherwise,
150 205 the stylesheet is queried for Pygments style information.
151 206 """
152 207 self.setStyleSheet(stylesheet)
153 208 self._control.document().setDefaultStyleSheet(stylesheet)
154 209 if self._page_control:
155 210 self._page_control.document().setDefaultStyleSheet(stylesheet)
156 211
157 212 if syntax_style is None:
158 213 self._highlighter.set_style_sheet(stylesheet)
159 214 else:
160 215 self._highlighter.set_style(syntax_style)
161 216
162 217 #---------------------------------------------------------------------------
163 218 # 'IPythonWidget' protected interface
164 219 #---------------------------------------------------------------------------
165 220
166 221 def _make_in_prompt(self, number):
167 222 """ Given a prompt number, returns an HTML In prompt.
168 223 """
169 224 body = self.in_prompt % number
170 225 return '<span class="in-prompt">%s</span>' % body
171 226
172 227 def _make_continuation_prompt(self, prompt):
173 228 """ Given a plain text version of an In prompt, returns an HTML
174 229 continuation prompt.
175 230 """
176 231 end_chars = '...: '
177 232 space_count = len(prompt.lstrip('\n')) - len(end_chars)
178 233 body = '&nbsp;' * space_count + end_chars
179 234 return '<span class="in-prompt">%s</span>' % body
180 235
181 236 def _make_out_prompt(self, number):
182 237 """ Given a prompt number, returns an HTML Out prompt.
183 238 """
184 239 body = self.out_prompt % number
185 240 return '<span class="out-prompt">%s</span>' % body
186 241
187 242 def _save_prompt_block(self):
188 243 """ Assuming a prompt has just been written at the end of the buffer,
189 244 store the QTextBlock that contains it and its length.
190 245 """
191 246 block = self._control.document().lastBlock()
192 247 self._previous_prompt_blocks.append((block, block.length()))
General Comments 0
You need to be logged in to leave comments. Login now