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