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