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