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