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