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