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