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