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