##// END OF EJS Templates
Merge pull request #1363 from minrk/qtstyle...
Min RK -
r6095:2b3bc441 merge
parent child Browse files
Show More
@@ -1,1852 +1,1855 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 import os
9 9 from os.path import commonprefix
10 10 import re
11 11 import sys
12 12 from textwrap import dedent
13 13 from unicodedata import category
14 14
15 15 # System library imports
16 16 from IPython.external.qt import QtCore, QtGui
17 17
18 18 # Local imports
19 19 from IPython.config.configurable import LoggingConfigurable
20 20 from IPython.frontend.qt.rich_text import HtmlExporter
21 21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 22 from IPython.utils.text import columnize
23 23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 24 from ansi_code_processor import QtAnsiCodeProcessor
25 25 from completion_widget import CompletionWidget
26 26 from kill_ring import QtKillRing
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Functions
30 30 #-----------------------------------------------------------------------------
31 31
32 32 def is_letter_or_number(char):
33 33 """ Returns whether the specified unicode character is a letter or a number.
34 34 """
35 35 cat = category(char)
36 36 return cat.startswith('L') or cat.startswith('N')
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 43 """ An abstract base class for console-type widgets. This class has
44 44 functionality for:
45 45
46 46 * Maintaining a prompt and editing region
47 47 * Providing the traditional Unix-style console keyboard shortcuts
48 48 * Performing tab completion
49 49 * Paging text
50 50 * Handling ANSI escape codes
51 51
52 52 ConsoleWidget also provides a number of utility methods that will be
53 53 convenient to implementors of a console-style widget.
54 54 """
55 55 __metaclass__ = MetaQObjectHasTraits
56 56
57 57 #------ Configuration ------------------------------------------------------
58 58
59 59 ansi_codes = Bool(True, config=True,
60 60 help="Whether to process ANSI escape codes."
61 61 )
62 62 buffer_size = Integer(500, config=True,
63 63 help="""
64 64 The maximum number of lines of text before truncation. Specifying a
65 65 non-positive number disables text truncation (not recommended).
66 66 """
67 67 )
68 68 gui_completion = Bool(False, config=True,
69 69 help="""
70 70 Use a list widget instead of plain text output for tab completion.
71 71 """
72 72 )
73 73 # NOTE: this value can only be specified during initialization.
74 74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 75 help="""
76 76 The type of underlying text widget to use. Valid values are 'plain',
77 77 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 78 QTextEdit.
79 79 """
80 80 )
81 81 # NOTE: this value can only be specified during initialization.
82 82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 83 default_value='inside', config=True,
84 84 help="""
85 85 The type of paging to use. Valid values are:
86 86
87 87 'inside' : The widget pages like a traditional terminal.
88 88 'hsplit' : When paging is requested, the widget is split
89 89 horizontally. The top pane contains the console, and the
90 90 bottom pane contains the paged text.
91 91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 92 used.
93 93 'custom' : No action is taken by the widget beyond emitting a
94 94 'custom_page_requested(str)' signal.
95 95 'none' : The text is written directly to the console.
96 96 """)
97 97
98 98 font_family = Unicode(config=True,
99 99 help="""The font family to use for the console.
100 100 On OSX this defaults to Monaco, on Windows the default is
101 101 Consolas with fallback of Courier, and on other platforms
102 102 the default is Monospace.
103 103 """)
104 104 def _font_family_default(self):
105 105 if sys.platform == 'win32':
106 106 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 107 return 'Consolas'
108 108 elif sys.platform == 'darwin':
109 109 # OSX always has Monaco, no need for a fallback
110 110 return 'Monaco'
111 111 else:
112 112 # Monospace should always exist, no need for a fallback
113 113 return 'Monospace'
114 114
115 115 font_size = Integer(config=True,
116 116 help="""The font size. If unconfigured, Qt will be entrusted
117 117 with the size of the font.
118 118 """)
119 119
120 120 # Whether to override ShortcutEvents for the keybindings defined by this
121 121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 123 override_shortcuts = Bool(False)
124 124
125 125 #------ Signals ------------------------------------------------------------
126 126
127 127 # Signals that indicate ConsoleWidget state.
128 128 copy_available = QtCore.Signal(bool)
129 129 redo_available = QtCore.Signal(bool)
130 130 undo_available = QtCore.Signal(bool)
131 131
132 132 # Signal emitted when paging is needed and the paging style has been
133 133 # specified as 'custom'.
134 134 custom_page_requested = QtCore.Signal(object)
135 135
136 136 # Signal emitted when the font is changed.
137 137 font_changed = QtCore.Signal(QtGui.QFont)
138 138
139 139 #------ Protected class variables ------------------------------------------
140
140
141 # control handles
142 _control = None
143 _page_control = None
144 _splitter = None
145
141 146 # When the control key is down, these keys are mapped.
142 147 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 148 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 149 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 150 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 151 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 152 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
148 153 if not sys.platform == 'darwin':
149 154 # On OS X, Ctrl-E already does the right thing, whereas End moves the
150 155 # cursor to the bottom of the buffer.
151 156 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
152 157
153 158 # The shortcuts defined by this widget. We need to keep track of these to
154 159 # support 'override_shortcuts' above.
155 160 _shortcuts = set(_ctrl_down_remap.keys() +
156 161 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
157 162 QtCore.Qt.Key_V ])
158 163
159 164 #---------------------------------------------------------------------------
160 165 # 'QObject' interface
161 166 #---------------------------------------------------------------------------
162 167
163 168 def __init__(self, parent=None, **kw):
164 169 """ Create a ConsoleWidget.
165 170
166 171 Parameters:
167 172 -----------
168 173 parent : QWidget, optional [default None]
169 174 The parent for this widget.
170 175 """
171 176 QtGui.QWidget.__init__(self, parent)
172 177 LoggingConfigurable.__init__(self, **kw)
173 178
174 179 # While scrolling the pager on Mac OS X, it tears badly. The
175 180 # NativeGesture is platform and perhaps build-specific hence
176 181 # we take adequate precautions here.
177 182 self._pager_scroll_events = [QtCore.QEvent.Wheel]
178 183 if hasattr(QtCore.QEvent, 'NativeGesture'):
179 184 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
180 185
181 186 # Create the layout and underlying text widget.
182 187 layout = QtGui.QStackedLayout(self)
183 188 layout.setContentsMargins(0, 0, 0, 0)
184 189 self._control = self._create_control()
185 self._page_control = None
186 self._splitter = None
187 190 if self.paging in ('hsplit', 'vsplit'):
188 191 self._splitter = QtGui.QSplitter()
189 192 if self.paging == 'hsplit':
190 193 self._splitter.setOrientation(QtCore.Qt.Horizontal)
191 194 else:
192 195 self._splitter.setOrientation(QtCore.Qt.Vertical)
193 196 self._splitter.addWidget(self._control)
194 197 layout.addWidget(self._splitter)
195 198 else:
196 199 layout.addWidget(self._control)
197 200
198 201 # Create the paging widget, if necessary.
199 202 if self.paging in ('inside', 'hsplit', 'vsplit'):
200 203 self._page_control = self._create_page_control()
201 204 if self._splitter:
202 205 self._page_control.hide()
203 206 self._splitter.addWidget(self._page_control)
204 207 else:
205 208 layout.addWidget(self._page_control)
206 209
207 210 # Initialize protected variables. Some variables contain useful state
208 211 # information for subclasses; they should be considered read-only.
209 212 self._append_before_prompt_pos = 0
210 213 self._ansi_processor = QtAnsiCodeProcessor()
211 214 self._completion_widget = CompletionWidget(self._control)
212 215 self._continuation_prompt = '> '
213 216 self._continuation_prompt_html = None
214 217 self._executing = False
215 218 self._filter_drag = False
216 219 self._filter_resize = False
217 220 self._html_exporter = HtmlExporter(self._control)
218 221 self._input_buffer_executing = ''
219 222 self._input_buffer_pending = ''
220 223 self._kill_ring = QtKillRing(self._control)
221 224 self._prompt = ''
222 225 self._prompt_html = None
223 226 self._prompt_pos = 0
224 227 self._prompt_sep = ''
225 228 self._reading = False
226 229 self._reading_callback = None
227 230 self._tab_width = 8
228 231 self._text_completing_pos = 0
229 232
230 233 # Set a monospaced font.
231 234 self.reset_font()
232 235
233 236 # Configure actions.
234 237 action = QtGui.QAction('Print', None)
235 238 action.setEnabled(True)
236 239 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
237 240 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
238 241 # Only override the default if there is a collision.
239 242 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
240 243 printkey = "Ctrl+Shift+P"
241 244 action.setShortcut(printkey)
242 245 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
243 246 action.triggered.connect(self.print_)
244 247 self.addAction(action)
245 248 self.print_action = action
246 249
247 250 action = QtGui.QAction('Save as HTML/XML', None)
248 251 action.setShortcut(QtGui.QKeySequence.Save)
249 252 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
250 253 action.triggered.connect(self.export_html)
251 254 self.addAction(action)
252 255 self.export_action = action
253 256
254 257 action = QtGui.QAction('Select All', None)
255 258 action.setEnabled(True)
256 259 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
257 260 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
258 261 # Only override the default if there is a collision.
259 262 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
260 263 selectall = "Ctrl+Shift+A"
261 264 action.setShortcut(selectall)
262 265 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
263 266 action.triggered.connect(self.select_all)
264 267 self.addAction(action)
265 268 self.select_all_action = action
266 269
267 270 self.increase_font_size = QtGui.QAction("Bigger Font",
268 271 self,
269 272 shortcut=QtGui.QKeySequence.ZoomIn,
270 273 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
271 274 statusTip="Increase the font size by one point",
272 275 triggered=self._increase_font_size)
273 276 self.addAction(self.increase_font_size)
274 277
275 278 self.decrease_font_size = QtGui.QAction("Smaller Font",
276 279 self,
277 280 shortcut=QtGui.QKeySequence.ZoomOut,
278 281 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
279 282 statusTip="Decrease the font size by one point",
280 283 triggered=self._decrease_font_size)
281 284 self.addAction(self.decrease_font_size)
282 285
283 286 self.reset_font_size = QtGui.QAction("Normal Font",
284 287 self,
285 288 shortcut="Ctrl+0",
286 289 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
287 290 statusTip="Restore the Normal font size",
288 291 triggered=self.reset_font)
289 292 self.addAction(self.reset_font_size)
290 293
291 294
292 295
293 296 def eventFilter(self, obj, event):
294 297 """ Reimplemented to ensure a console-like behavior in the underlying
295 298 text widgets.
296 299 """
297 300 etype = event.type()
298 301 if etype == QtCore.QEvent.KeyPress:
299 302
300 303 # Re-map keys for all filtered widgets.
301 304 key = event.key()
302 305 if self._control_key_down(event.modifiers()) and \
303 306 key in self._ctrl_down_remap:
304 307 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
305 308 self._ctrl_down_remap[key],
306 309 QtCore.Qt.NoModifier)
307 310 QtGui.qApp.sendEvent(obj, new_event)
308 311 return True
309 312
310 313 elif obj == self._control:
311 314 return self._event_filter_console_keypress(event)
312 315
313 316 elif obj == self._page_control:
314 317 return self._event_filter_page_keypress(event)
315 318
316 319 # Make middle-click paste safe.
317 320 elif etype == QtCore.QEvent.MouseButtonRelease and \
318 321 event.button() == QtCore.Qt.MidButton and \
319 322 obj == self._control.viewport():
320 323 cursor = self._control.cursorForPosition(event.pos())
321 324 self._control.setTextCursor(cursor)
322 325 self.paste(QtGui.QClipboard.Selection)
323 326 return True
324 327
325 328 # Manually adjust the scrollbars *after* a resize event is dispatched.
326 329 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
327 330 self._filter_resize = True
328 331 QtGui.qApp.sendEvent(obj, event)
329 332 self._adjust_scrollbars()
330 333 self._filter_resize = False
331 334 return True
332 335
333 336 # Override shortcuts for all filtered widgets.
334 337 elif etype == QtCore.QEvent.ShortcutOverride and \
335 338 self.override_shortcuts and \
336 339 self._control_key_down(event.modifiers()) and \
337 340 event.key() in self._shortcuts:
338 341 event.accept()
339 342
340 343 # Ensure that drags are safe. The problem is that the drag starting
341 344 # logic, which determines whether the drag is a Copy or Move, is locked
342 345 # down in QTextControl. If the widget is editable, which it must be if
343 346 # we're not executing, the drag will be a Move. The following hack
344 347 # prevents QTextControl from deleting the text by clearing the selection
345 348 # when a drag leave event originating from this widget is dispatched.
346 349 # The fact that we have to clear the user's selection is unfortunate,
347 350 # but the alternative--trying to prevent Qt from using its hardwired
348 351 # drag logic and writing our own--is worse.
349 352 elif etype == QtCore.QEvent.DragEnter and \
350 353 obj == self._control.viewport() and \
351 354 event.source() == self._control.viewport():
352 355 self._filter_drag = True
353 356 elif etype == QtCore.QEvent.DragLeave and \
354 357 obj == self._control.viewport() and \
355 358 self._filter_drag:
356 359 cursor = self._control.textCursor()
357 360 cursor.clearSelection()
358 361 self._control.setTextCursor(cursor)
359 362 self._filter_drag = False
360 363
361 364 # Ensure that drops are safe.
362 365 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
363 366 cursor = self._control.cursorForPosition(event.pos())
364 367 if self._in_buffer(cursor.position()):
365 368 text = event.mimeData().text()
366 369 self._insert_plain_text_into_buffer(cursor, text)
367 370
368 371 # Qt is expecting to get something here--drag and drop occurs in its
369 372 # own event loop. Send a DragLeave event to end it.
370 373 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
371 374 return True
372 375
373 376 # Handle scrolling of the vsplit pager. This hack attempts to solve
374 377 # problems with tearing of the help text inside the pager window. This
375 378 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
376 379 # perfect but makes the pager more usable.
377 380 elif etype in self._pager_scroll_events and \
378 381 obj == self._page_control:
379 382 self._page_control.repaint()
380 383 return True
381 384 return super(ConsoleWidget, self).eventFilter(obj, event)
382 385
383 386 #---------------------------------------------------------------------------
384 387 # 'QWidget' interface
385 388 #---------------------------------------------------------------------------
386 389
387 390 def sizeHint(self):
388 391 """ Reimplemented to suggest a size that is 80 characters wide and
389 392 25 lines high.
390 393 """
391 394 font_metrics = QtGui.QFontMetrics(self.font)
392 395 margin = (self._control.frameWidth() +
393 396 self._control.document().documentMargin()) * 2
394 397 style = self.style()
395 398 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
396 399
397 400 # Note 1: Despite my best efforts to take the various margins into
398 401 # account, the width is still coming out a bit too small, so we include
399 402 # a fudge factor of one character here.
400 403 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
401 404 # to a Qt bug on certain Mac OS systems where it returns 0.
402 405 width = font_metrics.width(' ') * 81 + margin
403 406 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
404 407 if self.paging == 'hsplit':
405 408 width = width * 2 + splitwidth
406 409
407 410 height = font_metrics.height() * 25 + margin
408 411 if self.paging == 'vsplit':
409 412 height = height * 2 + splitwidth
410 413
411 414 return QtCore.QSize(width, height)
412 415
413 416 #---------------------------------------------------------------------------
414 417 # 'ConsoleWidget' public interface
415 418 #---------------------------------------------------------------------------
416 419
417 420 def can_copy(self):
418 421 """ Returns whether text can be copied to the clipboard.
419 422 """
420 423 return self._control.textCursor().hasSelection()
421 424
422 425 def can_cut(self):
423 426 """ Returns whether text can be cut to the clipboard.
424 427 """
425 428 cursor = self._control.textCursor()
426 429 return (cursor.hasSelection() and
427 430 self._in_buffer(cursor.anchor()) and
428 431 self._in_buffer(cursor.position()))
429 432
430 433 def can_paste(self):
431 434 """ Returns whether text can be pasted from the clipboard.
432 435 """
433 436 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
434 437 return bool(QtGui.QApplication.clipboard().text())
435 438 return False
436 439
437 440 def clear(self, keep_input=True):
438 441 """ Clear the console.
439 442
440 443 Parameters:
441 444 -----------
442 445 keep_input : bool, optional (default True)
443 446 If set, restores the old input buffer if a new prompt is written.
444 447 """
445 448 if self._executing:
446 449 self._control.clear()
447 450 else:
448 451 if keep_input:
449 452 input_buffer = self.input_buffer
450 453 self._control.clear()
451 454 self._show_prompt()
452 455 if keep_input:
453 456 self.input_buffer = input_buffer
454 457
455 458 def copy(self):
456 459 """ Copy the currently selected text to the clipboard.
457 460 """
458 461 self.layout().currentWidget().copy()
459 462
460 463 def cut(self):
461 464 """ Copy the currently selected text to the clipboard and delete it
462 465 if it's inside the input buffer.
463 466 """
464 467 self.copy()
465 468 if self.can_cut():
466 469 self._control.textCursor().removeSelectedText()
467 470
468 471 def execute(self, source=None, hidden=False, interactive=False):
469 472 """ Executes source or the input buffer, possibly prompting for more
470 473 input.
471 474
472 475 Parameters:
473 476 -----------
474 477 source : str, optional
475 478
476 479 The source to execute. If not specified, the input buffer will be
477 480 used. If specified and 'hidden' is False, the input buffer will be
478 481 replaced with the source before execution.
479 482
480 483 hidden : bool, optional (default False)
481 484
482 485 If set, no output will be shown and the prompt will not be modified.
483 486 In other words, it will be completely invisible to the user that
484 487 an execution has occurred.
485 488
486 489 interactive : bool, optional (default False)
487 490
488 491 Whether the console is to treat the source as having been manually
489 492 entered by the user. The effect of this parameter depends on the
490 493 subclass implementation.
491 494
492 495 Raises:
493 496 -------
494 497 RuntimeError
495 498 If incomplete input is given and 'hidden' is True. In this case,
496 499 it is not possible to prompt for more input.
497 500
498 501 Returns:
499 502 --------
500 503 A boolean indicating whether the source was executed.
501 504 """
502 505 # WARNING: The order in which things happen here is very particular, in
503 506 # large part because our syntax highlighting is fragile. If you change
504 507 # something, test carefully!
505 508
506 509 # Decide what to execute.
507 510 if source is None:
508 511 source = self.input_buffer
509 512 if not hidden:
510 513 # A newline is appended later, but it should be considered part
511 514 # of the input buffer.
512 515 source += '\n'
513 516 elif not hidden:
514 517 self.input_buffer = source
515 518
516 519 # Execute the source or show a continuation prompt if it is incomplete.
517 520 complete = self._is_complete(source, interactive)
518 521 if hidden:
519 522 if complete:
520 523 self._execute(source, hidden)
521 524 else:
522 525 error = 'Incomplete noninteractive input: "%s"'
523 526 raise RuntimeError(error % source)
524 527 else:
525 528 if complete:
526 529 self._append_plain_text('\n')
527 530 self._input_buffer_executing = self.input_buffer
528 531 self._executing = True
529 532 self._prompt_finished()
530 533
531 534 # The maximum block count is only in effect during execution.
532 535 # This ensures that _prompt_pos does not become invalid due to
533 536 # text truncation.
534 537 self._control.document().setMaximumBlockCount(self.buffer_size)
535 538
536 539 # Setting a positive maximum block count will automatically
537 540 # disable the undo/redo history, but just to be safe:
538 541 self._control.setUndoRedoEnabled(False)
539 542
540 543 # Perform actual execution.
541 544 self._execute(source, hidden)
542 545
543 546 else:
544 547 # Do this inside an edit block so continuation prompts are
545 548 # removed seamlessly via undo/redo.
546 549 cursor = self._get_end_cursor()
547 550 cursor.beginEditBlock()
548 551 cursor.insertText('\n')
549 552 self._insert_continuation_prompt(cursor)
550 553 cursor.endEditBlock()
551 554
552 555 # Do not do this inside the edit block. It works as expected
553 556 # when using a QPlainTextEdit control, but does not have an
554 557 # effect when using a QTextEdit. I believe this is a Qt bug.
555 558 self._control.moveCursor(QtGui.QTextCursor.End)
556 559
557 560 return complete
558 561
559 562 def export_html(self):
560 563 """ Shows a dialog to export HTML/XML in various formats.
561 564 """
562 565 self._html_exporter.export()
563 566
564 567 def _get_input_buffer(self, force=False):
565 568 """ The text that the user has entered entered at the current prompt.
566 569
567 570 If the console is currently executing, the text that is executing will
568 571 always be returned.
569 572 """
570 573 # If we're executing, the input buffer may not even exist anymore due to
571 574 # the limit imposed by 'buffer_size'. Therefore, we store it.
572 575 if self._executing and not force:
573 576 return self._input_buffer_executing
574 577
575 578 cursor = self._get_end_cursor()
576 579 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
577 580 input_buffer = cursor.selection().toPlainText()
578 581
579 582 # Strip out continuation prompts.
580 583 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
581 584
582 585 def _set_input_buffer(self, string):
583 586 """ Sets the text in the input buffer.
584 587
585 588 If the console is currently executing, this call has no *immediate*
586 589 effect. When the execution is finished, the input buffer will be updated
587 590 appropriately.
588 591 """
589 592 # If we're executing, store the text for later.
590 593 if self._executing:
591 594 self._input_buffer_pending = string
592 595 return
593 596
594 597 # Remove old text.
595 598 cursor = self._get_end_cursor()
596 599 cursor.beginEditBlock()
597 600 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
598 601 cursor.removeSelectedText()
599 602
600 603 # Insert new text with continuation prompts.
601 604 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
602 605 cursor.endEditBlock()
603 606 self._control.moveCursor(QtGui.QTextCursor.End)
604 607
605 608 input_buffer = property(_get_input_buffer, _set_input_buffer)
606 609
607 610 def _get_font(self):
608 611 """ The base font being used by the ConsoleWidget.
609 612 """
610 613 return self._control.document().defaultFont()
611 614
612 615 def _set_font(self, font):
613 616 """ Sets the base font for the ConsoleWidget to the specified QFont.
614 617 """
615 618 font_metrics = QtGui.QFontMetrics(font)
616 619 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
617 620
618 621 self._completion_widget.setFont(font)
619 622 self._control.document().setDefaultFont(font)
620 623 if self._page_control:
621 624 self._page_control.document().setDefaultFont(font)
622 625
623 626 self.font_changed.emit(font)
624 627
625 628 font = property(_get_font, _set_font)
626 629
627 630 def paste(self, mode=QtGui.QClipboard.Clipboard):
628 631 """ Paste the contents of the clipboard into the input region.
629 632
630 633 Parameters:
631 634 -----------
632 635 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
633 636
634 637 Controls which part of the system clipboard is used. This can be
635 638 used to access the selection clipboard in X11 and the Find buffer
636 639 in Mac OS. By default, the regular clipboard is used.
637 640 """
638 641 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
639 642 # Make sure the paste is safe.
640 643 self._keep_cursor_in_buffer()
641 644 cursor = self._control.textCursor()
642 645
643 646 # Remove any trailing newline, which confuses the GUI and forces the
644 647 # user to backspace.
645 648 text = QtGui.QApplication.clipboard().text(mode).rstrip()
646 649 self._insert_plain_text_into_buffer(cursor, dedent(text))
647 650
648 651 def print_(self, printer = None):
649 652 """ Print the contents of the ConsoleWidget to the specified QPrinter.
650 653 """
651 654 if (not printer):
652 655 printer = QtGui.QPrinter()
653 656 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
654 657 return
655 658 self._control.print_(printer)
656 659
657 660 def prompt_to_top(self):
658 661 """ Moves the prompt to the top of the viewport.
659 662 """
660 663 if not self._executing:
661 664 prompt_cursor = self._get_prompt_cursor()
662 665 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
663 666 self._set_cursor(prompt_cursor)
664 667 self._set_top_cursor(prompt_cursor)
665 668
666 669 def redo(self):
667 670 """ Redo the last operation. If there is no operation to redo, nothing
668 671 happens.
669 672 """
670 673 self._control.redo()
671 674
672 675 def reset_font(self):
673 676 """ Sets the font to the default fixed-width font for this platform.
674 677 """
675 678 if sys.platform == 'win32':
676 679 # Consolas ships with Vista/Win7, fallback to Courier if needed
677 680 fallback = 'Courier'
678 681 elif sys.platform == 'darwin':
679 682 # OSX always has Monaco
680 683 fallback = 'Monaco'
681 684 else:
682 685 # Monospace should always exist
683 686 fallback = 'Monospace'
684 687 font = get_font(self.font_family, fallback)
685 688 if self.font_size:
686 689 font.setPointSize(self.font_size)
687 690 else:
688 691 font.setPointSize(QtGui.qApp.font().pointSize())
689 692 font.setStyleHint(QtGui.QFont.TypeWriter)
690 693 self._set_font(font)
691 694
692 695 def change_font_size(self, delta):
693 696 """Change the font size by the specified amount (in points).
694 697 """
695 698 font = self.font
696 699 size = max(font.pointSize() + delta, 1) # minimum 1 point
697 700 font.setPointSize(size)
698 701 self._set_font(font)
699 702
700 703 def _increase_font_size(self):
701 704 self.change_font_size(1)
702 705
703 706 def _decrease_font_size(self):
704 707 self.change_font_size(-1)
705 708
706 709 def select_all(self):
707 710 """ Selects all the text in the buffer.
708 711 """
709 712 self._control.selectAll()
710 713
711 714 def _get_tab_width(self):
712 715 """ The width (in terms of space characters) for tab characters.
713 716 """
714 717 return self._tab_width
715 718
716 719 def _set_tab_width(self, tab_width):
717 720 """ Sets the width (in terms of space characters) for tab characters.
718 721 """
719 722 font_metrics = QtGui.QFontMetrics(self.font)
720 723 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
721 724
722 725 self._tab_width = tab_width
723 726
724 727 tab_width = property(_get_tab_width, _set_tab_width)
725 728
726 729 def undo(self):
727 730 """ Undo the last operation. If there is no operation to undo, nothing
728 731 happens.
729 732 """
730 733 self._control.undo()
731 734
732 735 #---------------------------------------------------------------------------
733 736 # 'ConsoleWidget' abstract interface
734 737 #---------------------------------------------------------------------------
735 738
736 739 def _is_complete(self, source, interactive):
737 740 """ Returns whether 'source' can be executed. When triggered by an
738 741 Enter/Return key press, 'interactive' is True; otherwise, it is
739 742 False.
740 743 """
741 744 raise NotImplementedError
742 745
743 746 def _execute(self, source, hidden):
744 747 """ Execute 'source'. If 'hidden', do not show any output.
745 748 """
746 749 raise NotImplementedError
747 750
748 751 def _prompt_started_hook(self):
749 752 """ Called immediately after a new prompt is displayed.
750 753 """
751 754 pass
752 755
753 756 def _prompt_finished_hook(self):
754 757 """ Called immediately after a prompt is finished, i.e. when some input
755 758 will be processed and a new prompt displayed.
756 759 """
757 760 pass
758 761
759 762 def _up_pressed(self, shift_modifier):
760 763 """ Called when the up key is pressed. Returns whether to continue
761 764 processing the event.
762 765 """
763 766 return True
764 767
765 768 def _down_pressed(self, shift_modifier):
766 769 """ Called when the down key is pressed. Returns whether to continue
767 770 processing the event.
768 771 """
769 772 return True
770 773
771 774 def _tab_pressed(self):
772 775 """ Called when the tab key is pressed. Returns whether to continue
773 776 processing the event.
774 777 """
775 778 return False
776 779
777 780 #--------------------------------------------------------------------------
778 781 # 'ConsoleWidget' protected interface
779 782 #--------------------------------------------------------------------------
780 783
781 784 def _append_custom(self, insert, input, before_prompt=False):
782 785 """ A low-level method for appending content to the end of the buffer.
783 786
784 787 If 'before_prompt' is enabled, the content will be inserted before the
785 788 current prompt, if there is one.
786 789 """
787 790 # Determine where to insert the content.
788 791 cursor = self._control.textCursor()
789 792 if before_prompt and (self._reading or not self._executing):
790 793 cursor.setPosition(self._append_before_prompt_pos)
791 794 else:
792 795 cursor.movePosition(QtGui.QTextCursor.End)
793 796 start_pos = cursor.position()
794 797
795 798 # Perform the insertion.
796 799 result = insert(cursor, input)
797 800
798 801 # Adjust the prompt position if we have inserted before it. This is safe
799 802 # because buffer truncation is disabled when not executing.
800 803 if before_prompt and not self._executing:
801 804 diff = cursor.position() - start_pos
802 805 self._append_before_prompt_pos += diff
803 806 self._prompt_pos += diff
804 807
805 808 return result
806 809
807 810 def _append_html(self, html, before_prompt=False):
808 811 """ Appends HTML at the end of the console buffer.
809 812 """
810 813 self._append_custom(self._insert_html, html, before_prompt)
811 814
812 815 def _append_html_fetching_plain_text(self, html, before_prompt=False):
813 816 """ Appends HTML, then returns the plain text version of it.
814 817 """
815 818 return self._append_custom(self._insert_html_fetching_plain_text,
816 819 html, before_prompt)
817 820
818 821 def _append_plain_text(self, text, before_prompt=False):
819 822 """ Appends plain text, processing ANSI codes if enabled.
820 823 """
821 824 self._append_custom(self._insert_plain_text, text, before_prompt)
822 825
823 826 def _cancel_text_completion(self):
824 827 """ If text completion is progress, cancel it.
825 828 """
826 829 if self._text_completing_pos:
827 830 self._clear_temporary_buffer()
828 831 self._text_completing_pos = 0
829 832
830 833 def _clear_temporary_buffer(self):
831 834 """ Clears the "temporary text" buffer, i.e. all the text following
832 835 the prompt region.
833 836 """
834 837 # Select and remove all text below the input buffer.
835 838 cursor = self._get_prompt_cursor()
836 839 prompt = self._continuation_prompt.lstrip()
837 840 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
838 841 temp_cursor = QtGui.QTextCursor(cursor)
839 842 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
840 843 text = temp_cursor.selection().toPlainText().lstrip()
841 844 if not text.startswith(prompt):
842 845 break
843 846 else:
844 847 # We've reached the end of the input buffer and no text follows.
845 848 return
846 849 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
847 850 cursor.movePosition(QtGui.QTextCursor.End,
848 851 QtGui.QTextCursor.KeepAnchor)
849 852 cursor.removeSelectedText()
850 853
851 854 # After doing this, we have no choice but to clear the undo/redo
852 855 # history. Otherwise, the text is not "temporary" at all, because it
853 856 # can be recalled with undo/redo. Unfortunately, Qt does not expose
854 857 # fine-grained control to the undo/redo system.
855 858 if self._control.isUndoRedoEnabled():
856 859 self._control.setUndoRedoEnabled(False)
857 860 self._control.setUndoRedoEnabled(True)
858 861
859 862 def _complete_with_items(self, cursor, items):
860 863 """ Performs completion with 'items' at the specified cursor location.
861 864 """
862 865 self._cancel_text_completion()
863 866
864 867 if len(items) == 1:
865 868 cursor.setPosition(self._control.textCursor().position(),
866 869 QtGui.QTextCursor.KeepAnchor)
867 870 cursor.insertText(items[0])
868 871
869 872 elif len(items) > 1:
870 873 current_pos = self._control.textCursor().position()
871 874 prefix = commonprefix(items)
872 875 if prefix:
873 876 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
874 877 cursor.insertText(prefix)
875 878 current_pos = cursor.position()
876 879
877 880 if self.gui_completion:
878 881 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
879 882 self._completion_widget.show_items(cursor, items)
880 883 else:
881 884 cursor.beginEditBlock()
882 885 self._append_plain_text('\n')
883 886 self._page(self._format_as_columns(items))
884 887 cursor.endEditBlock()
885 888
886 889 cursor.setPosition(current_pos)
887 890 self._control.moveCursor(QtGui.QTextCursor.End)
888 891 self._control.setTextCursor(cursor)
889 892 self._text_completing_pos = current_pos
890 893
891 894 def _context_menu_make(self, pos):
892 895 """ Creates a context menu for the given QPoint (in widget coordinates).
893 896 """
894 897 menu = QtGui.QMenu(self)
895 898
896 899 self.cut_action = menu.addAction('Cut', self.cut)
897 900 self.cut_action.setEnabled(self.can_cut())
898 901 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
899 902
900 903 self.copy_action = menu.addAction('Copy', self.copy)
901 904 self.copy_action.setEnabled(self.can_copy())
902 905 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
903 906
904 907 self.paste_action = menu.addAction('Paste', self.paste)
905 908 self.paste_action.setEnabled(self.can_paste())
906 909 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
907 910
908 911 menu.addSeparator()
909 912 menu.addAction(self.select_all_action)
910 913
911 914 menu.addSeparator()
912 915 menu.addAction(self.export_action)
913 916 menu.addAction(self.print_action)
914 917
915 918 return menu
916 919
917 920 def _control_key_down(self, modifiers, include_command=False):
918 921 """ Given a KeyboardModifiers flags object, return whether the Control
919 922 key is down.
920 923
921 924 Parameters:
922 925 -----------
923 926 include_command : bool, optional (default True)
924 927 Whether to treat the Command key as a (mutually exclusive) synonym
925 928 for Control when in Mac OS.
926 929 """
927 930 # Note that on Mac OS, ControlModifier corresponds to the Command key
928 931 # while MetaModifier corresponds to the Control key.
929 932 if sys.platform == 'darwin':
930 933 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
931 934 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
932 935 else:
933 936 return bool(modifiers & QtCore.Qt.ControlModifier)
934 937
935 938 def _create_control(self):
936 939 """ Creates and connects the underlying text widget.
937 940 """
938 941 # Create the underlying control.
939 942 if self.kind == 'plain':
940 943 control = QtGui.QPlainTextEdit()
941 944 elif self.kind == 'rich':
942 945 control = QtGui.QTextEdit()
943 946 control.setAcceptRichText(False)
944 947
945 948 # Install event filters. The filter on the viewport is needed for
946 949 # mouse events and drag events.
947 950 control.installEventFilter(self)
948 951 control.viewport().installEventFilter(self)
949 952
950 953 # Connect signals.
951 954 control.cursorPositionChanged.connect(self._cursor_position_changed)
952 955 control.customContextMenuRequested.connect(
953 956 self._custom_context_menu_requested)
954 957 control.copyAvailable.connect(self.copy_available)
955 958 control.redoAvailable.connect(self.redo_available)
956 959 control.undoAvailable.connect(self.undo_available)
957 960
958 961 # Hijack the document size change signal to prevent Qt from adjusting
959 962 # the viewport's scrollbar. We are relying on an implementation detail
960 963 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
961 964 # this functionality we cannot create a nice terminal interface.
962 965 layout = control.document().documentLayout()
963 966 layout.documentSizeChanged.disconnect()
964 967 layout.documentSizeChanged.connect(self._adjust_scrollbars)
965 968
966 969 # Configure the control.
967 970 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
968 971 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
969 972 control.setReadOnly(True)
970 973 control.setUndoRedoEnabled(False)
971 974 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
972 975 return control
973 976
974 977 def _create_page_control(self):
975 978 """ Creates and connects the underlying paging widget.
976 979 """
977 980 if self.kind == 'plain':
978 981 control = QtGui.QPlainTextEdit()
979 982 elif self.kind == 'rich':
980 983 control = QtGui.QTextEdit()
981 984 control.installEventFilter(self)
982 985 viewport = control.viewport()
983 986 viewport.installEventFilter(self)
984 987 control.setReadOnly(True)
985 988 control.setUndoRedoEnabled(False)
986 989 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
987 990 return control
988 991
989 992 def _event_filter_console_keypress(self, event):
990 993 """ Filter key events for the underlying text widget to create a
991 994 console-like interface.
992 995 """
993 996 intercepted = False
994 997 cursor = self._control.textCursor()
995 998 position = cursor.position()
996 999 key = event.key()
997 1000 ctrl_down = self._control_key_down(event.modifiers())
998 1001 alt_down = event.modifiers() & QtCore.Qt.AltModifier
999 1002 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1000 1003
1001 1004 #------ Special sequences ----------------------------------------------
1002 1005
1003 1006 if event.matches(QtGui.QKeySequence.Copy):
1004 1007 self.copy()
1005 1008 intercepted = True
1006 1009
1007 1010 elif event.matches(QtGui.QKeySequence.Cut):
1008 1011 self.cut()
1009 1012 intercepted = True
1010 1013
1011 1014 elif event.matches(QtGui.QKeySequence.Paste):
1012 1015 self.paste()
1013 1016 intercepted = True
1014 1017
1015 1018 #------ Special modifier logic -----------------------------------------
1016 1019
1017 1020 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1018 1021 intercepted = True
1019 1022
1020 1023 # Special handling when tab completing in text mode.
1021 1024 self._cancel_text_completion()
1022 1025
1023 1026 if self._in_buffer(position):
1024 1027 # Special handling when a reading a line of raw input.
1025 1028 if self._reading:
1026 1029 self._append_plain_text('\n')
1027 1030 self._reading = False
1028 1031 if self._reading_callback:
1029 1032 self._reading_callback()
1030 1033
1031 1034 # If the input buffer is a single line or there is only
1032 1035 # whitespace after the cursor, execute. Otherwise, split the
1033 1036 # line with a continuation prompt.
1034 1037 elif not self._executing:
1035 1038 cursor.movePosition(QtGui.QTextCursor.End,
1036 1039 QtGui.QTextCursor.KeepAnchor)
1037 1040 at_end = len(cursor.selectedText().strip()) == 0
1038 1041 single_line = (self._get_end_cursor().blockNumber() ==
1039 1042 self._get_prompt_cursor().blockNumber())
1040 1043 if (at_end or shift_down or single_line) and not ctrl_down:
1041 1044 self.execute(interactive = not shift_down)
1042 1045 else:
1043 1046 # Do this inside an edit block for clean undo/redo.
1044 1047 cursor.beginEditBlock()
1045 1048 cursor.setPosition(position)
1046 1049 cursor.insertText('\n')
1047 1050 self._insert_continuation_prompt(cursor)
1048 1051 cursor.endEditBlock()
1049 1052
1050 1053 # Ensure that the whole input buffer is visible.
1051 1054 # FIXME: This will not be usable if the input buffer is
1052 1055 # taller than the console widget.
1053 1056 self._control.moveCursor(QtGui.QTextCursor.End)
1054 1057 self._control.setTextCursor(cursor)
1055 1058
1056 1059 #------ Control/Cmd modifier -------------------------------------------
1057 1060
1058 1061 elif ctrl_down:
1059 1062 if key == QtCore.Qt.Key_G:
1060 1063 self._keyboard_quit()
1061 1064 intercepted = True
1062 1065
1063 1066 elif key == QtCore.Qt.Key_K:
1064 1067 if self._in_buffer(position):
1065 1068 cursor.clearSelection()
1066 1069 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1067 1070 QtGui.QTextCursor.KeepAnchor)
1068 1071 if not cursor.hasSelection():
1069 1072 # Line deletion (remove continuation prompt)
1070 1073 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1071 1074 QtGui.QTextCursor.KeepAnchor)
1072 1075 cursor.movePosition(QtGui.QTextCursor.Right,
1073 1076 QtGui.QTextCursor.KeepAnchor,
1074 1077 len(self._continuation_prompt))
1075 1078 self._kill_ring.kill_cursor(cursor)
1076 1079 self._set_cursor(cursor)
1077 1080 intercepted = True
1078 1081
1079 1082 elif key == QtCore.Qt.Key_L:
1080 1083 self.prompt_to_top()
1081 1084 intercepted = True
1082 1085
1083 1086 elif key == QtCore.Qt.Key_O:
1084 1087 if self._page_control and self._page_control.isVisible():
1085 1088 self._page_control.setFocus()
1086 1089 intercepted = True
1087 1090
1088 1091 elif key == QtCore.Qt.Key_U:
1089 1092 if self._in_buffer(position):
1090 1093 cursor.clearSelection()
1091 1094 start_line = cursor.blockNumber()
1092 1095 if start_line == self._get_prompt_cursor().blockNumber():
1093 1096 offset = len(self._prompt)
1094 1097 else:
1095 1098 offset = len(self._continuation_prompt)
1096 1099 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1097 1100 QtGui.QTextCursor.KeepAnchor)
1098 1101 cursor.movePosition(QtGui.QTextCursor.Right,
1099 1102 QtGui.QTextCursor.KeepAnchor, offset)
1100 1103 self._kill_ring.kill_cursor(cursor)
1101 1104 self._set_cursor(cursor)
1102 1105 intercepted = True
1103 1106
1104 1107 elif key == QtCore.Qt.Key_Y:
1105 1108 self._keep_cursor_in_buffer()
1106 1109 self._kill_ring.yank()
1107 1110 intercepted = True
1108 1111
1109 1112 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1110 1113 if key == QtCore.Qt.Key_Backspace:
1111 1114 cursor = self._get_word_start_cursor(position)
1112 1115 else: # key == QtCore.Qt.Key_Delete
1113 1116 cursor = self._get_word_end_cursor(position)
1114 1117 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1115 1118 self._kill_ring.kill_cursor(cursor)
1116 1119 intercepted = True
1117 1120
1118 1121 elif key == QtCore.Qt.Key_D:
1119 1122 if len(self.input_buffer) == 0:
1120 1123 self.exit_requested.emit(self)
1121 1124 else:
1122 1125 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1123 1126 QtCore.Qt.Key_Delete,
1124 1127 QtCore.Qt.NoModifier)
1125 1128 QtGui.qApp.sendEvent(self._control, new_event)
1126 1129 intercepted = True
1127 1130
1128 1131 #------ Alt modifier ---------------------------------------------------
1129 1132
1130 1133 elif alt_down:
1131 1134 if key == QtCore.Qt.Key_B:
1132 1135 self._set_cursor(self._get_word_start_cursor(position))
1133 1136 intercepted = True
1134 1137
1135 1138 elif key == QtCore.Qt.Key_F:
1136 1139 self._set_cursor(self._get_word_end_cursor(position))
1137 1140 intercepted = True
1138 1141
1139 1142 elif key == QtCore.Qt.Key_Y:
1140 1143 self._kill_ring.rotate()
1141 1144 intercepted = True
1142 1145
1143 1146 elif key == QtCore.Qt.Key_Backspace:
1144 1147 cursor = self._get_word_start_cursor(position)
1145 1148 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1146 1149 self._kill_ring.kill_cursor(cursor)
1147 1150 intercepted = True
1148 1151
1149 1152 elif key == QtCore.Qt.Key_D:
1150 1153 cursor = self._get_word_end_cursor(position)
1151 1154 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1152 1155 self._kill_ring.kill_cursor(cursor)
1153 1156 intercepted = True
1154 1157
1155 1158 elif key == QtCore.Qt.Key_Delete:
1156 1159 intercepted = True
1157 1160
1158 1161 elif key == QtCore.Qt.Key_Greater:
1159 1162 self._control.moveCursor(QtGui.QTextCursor.End)
1160 1163 intercepted = True
1161 1164
1162 1165 elif key == QtCore.Qt.Key_Less:
1163 1166 self._control.setTextCursor(self._get_prompt_cursor())
1164 1167 intercepted = True
1165 1168
1166 1169 #------ No modifiers ---------------------------------------------------
1167 1170
1168 1171 else:
1169 1172 if shift_down:
1170 1173 anchormode = QtGui.QTextCursor.KeepAnchor
1171 1174 else:
1172 1175 anchormode = QtGui.QTextCursor.MoveAnchor
1173 1176
1174 1177 if key == QtCore.Qt.Key_Escape:
1175 1178 self._keyboard_quit()
1176 1179 intercepted = True
1177 1180
1178 1181 elif key == QtCore.Qt.Key_Up:
1179 1182 if self._reading or not self._up_pressed(shift_down):
1180 1183 intercepted = True
1181 1184 else:
1182 1185 prompt_line = self._get_prompt_cursor().blockNumber()
1183 1186 intercepted = cursor.blockNumber() <= prompt_line
1184 1187
1185 1188 elif key == QtCore.Qt.Key_Down:
1186 1189 if self._reading or not self._down_pressed(shift_down):
1187 1190 intercepted = True
1188 1191 else:
1189 1192 end_line = self._get_end_cursor().blockNumber()
1190 1193 intercepted = cursor.blockNumber() == end_line
1191 1194
1192 1195 elif key == QtCore.Qt.Key_Tab:
1193 1196 if not self._reading:
1194 1197 if self._tab_pressed():
1195 1198 # real tab-key, insert four spaces
1196 1199 cursor.insertText(' '*4)
1197 1200 intercepted = True
1198 1201
1199 1202 elif key == QtCore.Qt.Key_Left:
1200 1203
1201 1204 # Move to the previous line
1202 1205 line, col = cursor.blockNumber(), cursor.columnNumber()
1203 1206 if line > self._get_prompt_cursor().blockNumber() and \
1204 1207 col == len(self._continuation_prompt):
1205 1208 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1206 1209 mode=anchormode)
1207 1210 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1208 1211 mode=anchormode)
1209 1212 intercepted = True
1210 1213
1211 1214 # Regular left movement
1212 1215 else:
1213 1216 intercepted = not self._in_buffer(position - 1)
1214 1217
1215 1218 elif key == QtCore.Qt.Key_Right:
1216 1219 original_block_number = cursor.blockNumber()
1217 1220 cursor.movePosition(QtGui.QTextCursor.Right,
1218 1221 mode=anchormode)
1219 1222 if cursor.blockNumber() != original_block_number:
1220 1223 cursor.movePosition(QtGui.QTextCursor.Right,
1221 1224 n=len(self._continuation_prompt),
1222 1225 mode=anchormode)
1223 1226 self._set_cursor(cursor)
1224 1227 intercepted = True
1225 1228
1226 1229 elif key == QtCore.Qt.Key_Home:
1227 1230 start_line = cursor.blockNumber()
1228 1231 if start_line == self._get_prompt_cursor().blockNumber():
1229 1232 start_pos = self._prompt_pos
1230 1233 else:
1231 1234 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1232 1235 QtGui.QTextCursor.KeepAnchor)
1233 1236 start_pos = cursor.position()
1234 1237 start_pos += len(self._continuation_prompt)
1235 1238 cursor.setPosition(position)
1236 1239 if shift_down and self._in_buffer(position):
1237 1240 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1238 1241 else:
1239 1242 cursor.setPosition(start_pos)
1240 1243 self._set_cursor(cursor)
1241 1244 intercepted = True
1242 1245
1243 1246 elif key == QtCore.Qt.Key_Backspace:
1244 1247
1245 1248 # Line deletion (remove continuation prompt)
1246 1249 line, col = cursor.blockNumber(), cursor.columnNumber()
1247 1250 if not self._reading and \
1248 1251 col == len(self._continuation_prompt) and \
1249 1252 line > self._get_prompt_cursor().blockNumber():
1250 1253 cursor.beginEditBlock()
1251 1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1252 1255 QtGui.QTextCursor.KeepAnchor)
1253 1256 cursor.removeSelectedText()
1254 1257 cursor.deletePreviousChar()
1255 1258 cursor.endEditBlock()
1256 1259 intercepted = True
1257 1260
1258 1261 # Regular backwards deletion
1259 1262 else:
1260 1263 anchor = cursor.anchor()
1261 1264 if anchor == position:
1262 1265 intercepted = not self._in_buffer(position - 1)
1263 1266 else:
1264 1267 intercepted = not self._in_buffer(min(anchor, position))
1265 1268
1266 1269 elif key == QtCore.Qt.Key_Delete:
1267 1270
1268 1271 # Line deletion (remove continuation prompt)
1269 1272 if not self._reading and self._in_buffer(position) and \
1270 1273 cursor.atBlockEnd() and not cursor.hasSelection():
1271 1274 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1272 1275 QtGui.QTextCursor.KeepAnchor)
1273 1276 cursor.movePosition(QtGui.QTextCursor.Right,
1274 1277 QtGui.QTextCursor.KeepAnchor,
1275 1278 len(self._continuation_prompt))
1276 1279 cursor.removeSelectedText()
1277 1280 intercepted = True
1278 1281
1279 1282 # Regular forwards deletion:
1280 1283 else:
1281 1284 anchor = cursor.anchor()
1282 1285 intercepted = (not self._in_buffer(anchor) or
1283 1286 not self._in_buffer(position))
1284 1287
1285 1288 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1286 1289 # using the keyboard in any part of the buffer. Also, permit scrolling
1287 1290 # with Page Up/Down keys. Finally, if we're executing, don't move the
1288 1291 # cursor (if even this made sense, we can't guarantee that the prompt
1289 1292 # position is still valid due to text truncation).
1290 1293 if not (self._control_key_down(event.modifiers(), include_command=True)
1291 1294 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1292 1295 or (self._executing and not self._reading)):
1293 1296 self._keep_cursor_in_buffer()
1294 1297
1295 1298 return intercepted
1296 1299
1297 1300 def _event_filter_page_keypress(self, event):
1298 1301 """ Filter key events for the paging widget to create console-like
1299 1302 interface.
1300 1303 """
1301 1304 key = event.key()
1302 1305 ctrl_down = self._control_key_down(event.modifiers())
1303 1306 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1304 1307
1305 1308 if ctrl_down:
1306 1309 if key == QtCore.Qt.Key_O:
1307 1310 self._control.setFocus()
1308 1311 intercept = True
1309 1312
1310 1313 elif alt_down:
1311 1314 if key == QtCore.Qt.Key_Greater:
1312 1315 self._page_control.moveCursor(QtGui.QTextCursor.End)
1313 1316 intercepted = True
1314 1317
1315 1318 elif key == QtCore.Qt.Key_Less:
1316 1319 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1317 1320 intercepted = True
1318 1321
1319 1322 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1320 1323 if self._splitter:
1321 1324 self._page_control.hide()
1322 1325 self._control.setFocus()
1323 1326 else:
1324 1327 self.layout().setCurrentWidget(self._control)
1325 1328 return True
1326 1329
1327 1330 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1328 1331 QtCore.Qt.Key_Tab):
1329 1332 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1330 1333 QtCore.Qt.Key_PageDown,
1331 1334 QtCore.Qt.NoModifier)
1332 1335 QtGui.qApp.sendEvent(self._page_control, new_event)
1333 1336 return True
1334 1337
1335 1338 elif key == QtCore.Qt.Key_Backspace:
1336 1339 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1337 1340 QtCore.Qt.Key_PageUp,
1338 1341 QtCore.Qt.NoModifier)
1339 1342 QtGui.qApp.sendEvent(self._page_control, new_event)
1340 1343 return True
1341 1344
1342 1345 return False
1343 1346
1344 1347 def _format_as_columns(self, items, separator=' '):
1345 1348 """ Transform a list of strings into a single string with columns.
1346 1349
1347 1350 Parameters
1348 1351 ----------
1349 1352 items : sequence of strings
1350 1353 The strings to process.
1351 1354
1352 1355 separator : str, optional [default is two spaces]
1353 1356 The string that separates columns.
1354 1357
1355 1358 Returns
1356 1359 -------
1357 1360 The formatted string.
1358 1361 """
1359 1362 # Calculate the number of characters available.
1360 1363 width = self._control.viewport().width()
1361 1364 char_width = QtGui.QFontMetrics(self.font).width(' ')
1362 1365 displaywidth = max(10, (width / char_width) - 1)
1363 1366
1364 1367 return columnize(items, separator, displaywidth)
1365 1368
1366 1369 def _get_block_plain_text(self, block):
1367 1370 """ Given a QTextBlock, return its unformatted text.
1368 1371 """
1369 1372 cursor = QtGui.QTextCursor(block)
1370 1373 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1371 1374 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1372 1375 QtGui.QTextCursor.KeepAnchor)
1373 1376 return cursor.selection().toPlainText()
1374 1377
1375 1378 def _get_cursor(self):
1376 1379 """ Convenience method that returns a cursor for the current position.
1377 1380 """
1378 1381 return self._control.textCursor()
1379 1382
1380 1383 def _get_end_cursor(self):
1381 1384 """ Convenience method that returns a cursor for the last character.
1382 1385 """
1383 1386 cursor = self._control.textCursor()
1384 1387 cursor.movePosition(QtGui.QTextCursor.End)
1385 1388 return cursor
1386 1389
1387 1390 def _get_input_buffer_cursor_column(self):
1388 1391 """ Returns the column of the cursor in the input buffer, excluding the
1389 1392 contribution by the prompt, or -1 if there is no such column.
1390 1393 """
1391 1394 prompt = self._get_input_buffer_cursor_prompt()
1392 1395 if prompt is None:
1393 1396 return -1
1394 1397 else:
1395 1398 cursor = self._control.textCursor()
1396 1399 return cursor.columnNumber() - len(prompt)
1397 1400
1398 1401 def _get_input_buffer_cursor_line(self):
1399 1402 """ Returns the text of the line of the input buffer that contains the
1400 1403 cursor, or None if there is no such line.
1401 1404 """
1402 1405 prompt = self._get_input_buffer_cursor_prompt()
1403 1406 if prompt is None:
1404 1407 return None
1405 1408 else:
1406 1409 cursor = self._control.textCursor()
1407 1410 text = self._get_block_plain_text(cursor.block())
1408 1411 return text[len(prompt):]
1409 1412
1410 1413 def _get_input_buffer_cursor_prompt(self):
1411 1414 """ Returns the (plain text) prompt for line of the input buffer that
1412 1415 contains the cursor, or None if there is no such line.
1413 1416 """
1414 1417 if self._executing:
1415 1418 return None
1416 1419 cursor = self._control.textCursor()
1417 1420 if cursor.position() >= self._prompt_pos:
1418 1421 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1419 1422 return self._prompt
1420 1423 else:
1421 1424 return self._continuation_prompt
1422 1425 else:
1423 1426 return None
1424 1427
1425 1428 def _get_prompt_cursor(self):
1426 1429 """ Convenience method that returns a cursor for the prompt position.
1427 1430 """
1428 1431 cursor = self._control.textCursor()
1429 1432 cursor.setPosition(self._prompt_pos)
1430 1433 return cursor
1431 1434
1432 1435 def _get_selection_cursor(self, start, end):
1433 1436 """ Convenience method that returns a cursor with text selected between
1434 1437 the positions 'start' and 'end'.
1435 1438 """
1436 1439 cursor = self._control.textCursor()
1437 1440 cursor.setPosition(start)
1438 1441 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1439 1442 return cursor
1440 1443
1441 1444 def _get_word_start_cursor(self, position):
1442 1445 """ Find the start of the word to the left the given position. If a
1443 1446 sequence of non-word characters precedes the first word, skip over
1444 1447 them. (This emulates the behavior of bash, emacs, etc.)
1445 1448 """
1446 1449 document = self._control.document()
1447 1450 position -= 1
1448 1451 while position >= self._prompt_pos and \
1449 1452 not is_letter_or_number(document.characterAt(position)):
1450 1453 position -= 1
1451 1454 while position >= self._prompt_pos and \
1452 1455 is_letter_or_number(document.characterAt(position)):
1453 1456 position -= 1
1454 1457 cursor = self._control.textCursor()
1455 1458 cursor.setPosition(position + 1)
1456 1459 return cursor
1457 1460
1458 1461 def _get_word_end_cursor(self, position):
1459 1462 """ Find the end of the word to the right the given position. If a
1460 1463 sequence of non-word characters precedes the first word, skip over
1461 1464 them. (This emulates the behavior of bash, emacs, etc.)
1462 1465 """
1463 1466 document = self._control.document()
1464 1467 end = self._get_end_cursor().position()
1465 1468 while position < end and \
1466 1469 not is_letter_or_number(document.characterAt(position)):
1467 1470 position += 1
1468 1471 while position < end and \
1469 1472 is_letter_or_number(document.characterAt(position)):
1470 1473 position += 1
1471 1474 cursor = self._control.textCursor()
1472 1475 cursor.setPosition(position)
1473 1476 return cursor
1474 1477
1475 1478 def _insert_continuation_prompt(self, cursor):
1476 1479 """ Inserts new continuation prompt using the specified cursor.
1477 1480 """
1478 1481 if self._continuation_prompt_html is None:
1479 1482 self._insert_plain_text(cursor, self._continuation_prompt)
1480 1483 else:
1481 1484 self._continuation_prompt = self._insert_html_fetching_plain_text(
1482 1485 cursor, self._continuation_prompt_html)
1483 1486
1484 1487 def _insert_html(self, cursor, html):
1485 1488 """ Inserts HTML using the specified cursor in such a way that future
1486 1489 formatting is unaffected.
1487 1490 """
1488 1491 cursor.beginEditBlock()
1489 1492 cursor.insertHtml(html)
1490 1493
1491 1494 # After inserting HTML, the text document "remembers" it's in "html
1492 1495 # mode", which means that subsequent calls adding plain text will result
1493 1496 # in unwanted formatting, lost tab characters, etc. The following code
1494 1497 # hacks around this behavior, which I consider to be a bug in Qt, by
1495 1498 # (crudely) resetting the document's style state.
1496 1499 cursor.movePosition(QtGui.QTextCursor.Left,
1497 1500 QtGui.QTextCursor.KeepAnchor)
1498 1501 if cursor.selection().toPlainText() == ' ':
1499 1502 cursor.removeSelectedText()
1500 1503 else:
1501 1504 cursor.movePosition(QtGui.QTextCursor.Right)
1502 1505 cursor.insertText(' ', QtGui.QTextCharFormat())
1503 1506 cursor.endEditBlock()
1504 1507
1505 1508 def _insert_html_fetching_plain_text(self, cursor, html):
1506 1509 """ Inserts HTML using the specified cursor, then returns its plain text
1507 1510 version.
1508 1511 """
1509 1512 cursor.beginEditBlock()
1510 1513 cursor.removeSelectedText()
1511 1514
1512 1515 start = cursor.position()
1513 1516 self._insert_html(cursor, html)
1514 1517 end = cursor.position()
1515 1518 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1516 1519 text = cursor.selection().toPlainText()
1517 1520
1518 1521 cursor.setPosition(end)
1519 1522 cursor.endEditBlock()
1520 1523 return text
1521 1524
1522 1525 def _insert_plain_text(self, cursor, text):
1523 1526 """ Inserts plain text using the specified cursor, processing ANSI codes
1524 1527 if enabled.
1525 1528 """
1526 1529 cursor.beginEditBlock()
1527 1530 if self.ansi_codes:
1528 1531 for substring in self._ansi_processor.split_string(text):
1529 1532 for act in self._ansi_processor.actions:
1530 1533
1531 1534 # Unlike real terminal emulators, we don't distinguish
1532 1535 # between the screen and the scrollback buffer. A screen
1533 1536 # erase request clears everything.
1534 1537 if act.action == 'erase' and act.area == 'screen':
1535 1538 cursor.select(QtGui.QTextCursor.Document)
1536 1539 cursor.removeSelectedText()
1537 1540
1538 1541 # Simulate a form feed by scrolling just past the last line.
1539 1542 elif act.action == 'scroll' and act.unit == 'page':
1540 1543 cursor.insertText('\n')
1541 1544 cursor.endEditBlock()
1542 1545 self._set_top_cursor(cursor)
1543 1546 cursor.joinPreviousEditBlock()
1544 1547 cursor.deletePreviousChar()
1545 1548
1546 1549 elif act.action == 'carriage-return':
1547 1550 cursor.movePosition(
1548 1551 cursor.StartOfLine, cursor.KeepAnchor)
1549 1552
1550 1553 elif act.action == 'beep':
1551 1554 QtGui.qApp.beep()
1552 1555
1553 1556 format = self._ansi_processor.get_format()
1554 1557 cursor.insertText(substring, format)
1555 1558 else:
1556 1559 cursor.insertText(text)
1557 1560 cursor.endEditBlock()
1558 1561
1559 1562 def _insert_plain_text_into_buffer(self, cursor, text):
1560 1563 """ Inserts text into the input buffer using the specified cursor (which
1561 1564 must be in the input buffer), ensuring that continuation prompts are
1562 1565 inserted as necessary.
1563 1566 """
1564 1567 lines = text.splitlines(True)
1565 1568 if lines:
1566 1569 cursor.beginEditBlock()
1567 1570 cursor.insertText(lines[0])
1568 1571 for line in lines[1:]:
1569 1572 if self._continuation_prompt_html is None:
1570 1573 cursor.insertText(self._continuation_prompt)
1571 1574 else:
1572 1575 self._continuation_prompt = \
1573 1576 self._insert_html_fetching_plain_text(
1574 1577 cursor, self._continuation_prompt_html)
1575 1578 cursor.insertText(line)
1576 1579 cursor.endEditBlock()
1577 1580
1578 1581 def _in_buffer(self, position=None):
1579 1582 """ Returns whether the current cursor (or, if specified, a position) is
1580 1583 inside the editing region.
1581 1584 """
1582 1585 cursor = self._control.textCursor()
1583 1586 if position is None:
1584 1587 position = cursor.position()
1585 1588 else:
1586 1589 cursor.setPosition(position)
1587 1590 line = cursor.blockNumber()
1588 1591 prompt_line = self._get_prompt_cursor().blockNumber()
1589 1592 if line == prompt_line:
1590 1593 return position >= self._prompt_pos
1591 1594 elif line > prompt_line:
1592 1595 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1593 1596 prompt_pos = cursor.position() + len(self._continuation_prompt)
1594 1597 return position >= prompt_pos
1595 1598 return False
1596 1599
1597 1600 def _keep_cursor_in_buffer(self):
1598 1601 """ Ensures that the cursor is inside the editing region. Returns
1599 1602 whether the cursor was moved.
1600 1603 """
1601 1604 moved = not self._in_buffer()
1602 1605 if moved:
1603 1606 cursor = self._control.textCursor()
1604 1607 cursor.movePosition(QtGui.QTextCursor.End)
1605 1608 self._control.setTextCursor(cursor)
1606 1609 return moved
1607 1610
1608 1611 def _keyboard_quit(self):
1609 1612 """ Cancels the current editing task ala Ctrl-G in Emacs.
1610 1613 """
1611 1614 if self._text_completing_pos:
1612 1615 self._cancel_text_completion()
1613 1616 else:
1614 1617 self.input_buffer = ''
1615 1618
1616 1619 def _page(self, text, html=False):
1617 1620 """ Displays text using the pager if it exceeds the height of the
1618 1621 viewport.
1619 1622
1620 1623 Parameters:
1621 1624 -----------
1622 1625 html : bool, optional (default False)
1623 1626 If set, the text will be interpreted as HTML instead of plain text.
1624 1627 """
1625 1628 line_height = QtGui.QFontMetrics(self.font).height()
1626 1629 minlines = self._control.viewport().height() / line_height
1627 1630 if self.paging != 'none' and \
1628 1631 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1629 1632 if self.paging == 'custom':
1630 1633 self.custom_page_requested.emit(text)
1631 1634 else:
1632 1635 self._page_control.clear()
1633 1636 cursor = self._page_control.textCursor()
1634 1637 if html:
1635 1638 self._insert_html(cursor, text)
1636 1639 else:
1637 1640 self._insert_plain_text(cursor, text)
1638 1641 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1639 1642
1640 1643 self._page_control.viewport().resize(self._control.size())
1641 1644 if self._splitter:
1642 1645 self._page_control.show()
1643 1646 self._page_control.setFocus()
1644 1647 else:
1645 1648 self.layout().setCurrentWidget(self._page_control)
1646 1649 elif html:
1647 1650 self._append_plain_html(text)
1648 1651 else:
1649 1652 self._append_plain_text(text)
1650 1653
1651 1654 def _prompt_finished(self):
1652 1655 """ Called immediately after a prompt is finished, i.e. when some input
1653 1656 will be processed and a new prompt displayed.
1654 1657 """
1655 1658 self._control.setReadOnly(True)
1656 1659 self._prompt_finished_hook()
1657 1660
1658 1661 def _prompt_started(self):
1659 1662 """ Called immediately after a new prompt is displayed.
1660 1663 """
1661 1664 # Temporarily disable the maximum block count to permit undo/redo and
1662 1665 # to ensure that the prompt position does not change due to truncation.
1663 1666 self._control.document().setMaximumBlockCount(0)
1664 1667 self._control.setUndoRedoEnabled(True)
1665 1668
1666 1669 # Work around bug in QPlainTextEdit: input method is not re-enabled
1667 1670 # when read-only is disabled.
1668 1671 self._control.setReadOnly(False)
1669 1672 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1670 1673
1671 1674 if not self._reading:
1672 1675 self._executing = False
1673 1676 self._prompt_started_hook()
1674 1677
1675 1678 # If the input buffer has changed while executing, load it.
1676 1679 if self._input_buffer_pending:
1677 1680 self.input_buffer = self._input_buffer_pending
1678 1681 self._input_buffer_pending = ''
1679 1682
1680 1683 self._control.moveCursor(QtGui.QTextCursor.End)
1681 1684
1682 1685 def _readline(self, prompt='', callback=None):
1683 1686 """ Reads one line of input from the user.
1684 1687
1685 1688 Parameters
1686 1689 ----------
1687 1690 prompt : str, optional
1688 1691 The prompt to print before reading the line.
1689 1692
1690 1693 callback : callable, optional
1691 1694 A callback to execute with the read line. If not specified, input is
1692 1695 read *synchronously* and this method does not return until it has
1693 1696 been read.
1694 1697
1695 1698 Returns
1696 1699 -------
1697 1700 If a callback is specified, returns nothing. Otherwise, returns the
1698 1701 input string with the trailing newline stripped.
1699 1702 """
1700 1703 if self._reading:
1701 1704 raise RuntimeError('Cannot read a line. Widget is already reading.')
1702 1705
1703 1706 if not callback and not self.isVisible():
1704 1707 # If the user cannot see the widget, this function cannot return.
1705 1708 raise RuntimeError('Cannot synchronously read a line if the widget '
1706 1709 'is not visible!')
1707 1710
1708 1711 self._reading = True
1709 1712 self._show_prompt(prompt, newline=False)
1710 1713
1711 1714 if callback is None:
1712 1715 self._reading_callback = None
1713 1716 while self._reading:
1714 1717 QtCore.QCoreApplication.processEvents()
1715 1718 return self._get_input_buffer(force=True).rstrip('\n')
1716 1719
1717 1720 else:
1718 1721 self._reading_callback = lambda: \
1719 1722 callback(self._get_input_buffer(force=True).rstrip('\n'))
1720 1723
1721 1724 def _set_continuation_prompt(self, prompt, html=False):
1722 1725 """ Sets the continuation prompt.
1723 1726
1724 1727 Parameters
1725 1728 ----------
1726 1729 prompt : str
1727 1730 The prompt to show when more input is needed.
1728 1731
1729 1732 html : bool, optional (default False)
1730 1733 If set, the prompt will be inserted as formatted HTML. Otherwise,
1731 1734 the prompt will be treated as plain text, though ANSI color codes
1732 1735 will be handled.
1733 1736 """
1734 1737 if html:
1735 1738 self._continuation_prompt_html = prompt
1736 1739 else:
1737 1740 self._continuation_prompt = prompt
1738 1741 self._continuation_prompt_html = None
1739 1742
1740 1743 def _set_cursor(self, cursor):
1741 1744 """ Convenience method to set the current cursor.
1742 1745 """
1743 1746 self._control.setTextCursor(cursor)
1744 1747
1745 1748 def _set_top_cursor(self, cursor):
1746 1749 """ Scrolls the viewport so that the specified cursor is at the top.
1747 1750 """
1748 1751 scrollbar = self._control.verticalScrollBar()
1749 1752 scrollbar.setValue(scrollbar.maximum())
1750 1753 original_cursor = self._control.textCursor()
1751 1754 self._control.setTextCursor(cursor)
1752 1755 self._control.ensureCursorVisible()
1753 1756 self._control.setTextCursor(original_cursor)
1754 1757
1755 1758 def _show_prompt(self, prompt=None, html=False, newline=True):
1756 1759 """ Writes a new prompt at the end of the buffer.
1757 1760
1758 1761 Parameters
1759 1762 ----------
1760 1763 prompt : str, optional
1761 1764 The prompt to show. If not specified, the previous prompt is used.
1762 1765
1763 1766 html : bool, optional (default False)
1764 1767 Only relevant when a prompt is specified. If set, the prompt will
1765 1768 be inserted as formatted HTML. Otherwise, the prompt will be treated
1766 1769 as plain text, though ANSI color codes will be handled.
1767 1770
1768 1771 newline : bool, optional (default True)
1769 1772 If set, a new line will be written before showing the prompt if
1770 1773 there is not already a newline at the end of the buffer.
1771 1774 """
1772 1775 # Save the current end position to support _append*(before_prompt=True).
1773 1776 cursor = self._get_end_cursor()
1774 1777 self._append_before_prompt_pos = cursor.position()
1775 1778
1776 1779 # Insert a preliminary newline, if necessary.
1777 1780 if newline and cursor.position() > 0:
1778 1781 cursor.movePosition(QtGui.QTextCursor.Left,
1779 1782 QtGui.QTextCursor.KeepAnchor)
1780 1783 if cursor.selection().toPlainText() != '\n':
1781 1784 self._append_plain_text('\n')
1782 1785
1783 1786 # Write the prompt.
1784 1787 self._append_plain_text(self._prompt_sep)
1785 1788 if prompt is None:
1786 1789 if self._prompt_html is None:
1787 1790 self._append_plain_text(self._prompt)
1788 1791 else:
1789 1792 self._append_html(self._prompt_html)
1790 1793 else:
1791 1794 if html:
1792 1795 self._prompt = self._append_html_fetching_plain_text(prompt)
1793 1796 self._prompt_html = prompt
1794 1797 else:
1795 1798 self._append_plain_text(prompt)
1796 1799 self._prompt = prompt
1797 1800 self._prompt_html = None
1798 1801
1799 1802 self._prompt_pos = self._get_end_cursor().position()
1800 1803 self._prompt_started()
1801 1804
1802 1805 #------ Signal handlers ----------------------------------------------------
1803 1806
1804 1807 def _adjust_scrollbars(self):
1805 1808 """ Expands the vertical scrollbar beyond the range set by Qt.
1806 1809 """
1807 1810 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1808 1811 # and qtextedit.cpp.
1809 1812 document = self._control.document()
1810 1813 scrollbar = self._control.verticalScrollBar()
1811 1814 viewport_height = self._control.viewport().height()
1812 1815 if isinstance(self._control, QtGui.QPlainTextEdit):
1813 1816 maximum = max(0, document.lineCount() - 1)
1814 1817 step = viewport_height / self._control.fontMetrics().lineSpacing()
1815 1818 else:
1816 1819 # QTextEdit does not do line-based layout and blocks will not in
1817 1820 # general have the same height. Therefore it does not make sense to
1818 1821 # attempt to scroll in line height increments.
1819 1822 maximum = document.size().height()
1820 1823 step = viewport_height
1821 1824 diff = maximum - scrollbar.maximum()
1822 1825 scrollbar.setRange(0, maximum)
1823 1826 scrollbar.setPageStep(step)
1824 1827
1825 1828 # Compensate for undesirable scrolling that occurs automatically due to
1826 1829 # maximumBlockCount() text truncation.
1827 1830 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1828 1831 scrollbar.setValue(scrollbar.value() + diff)
1829 1832
1830 1833 def _cursor_position_changed(self):
1831 1834 """ Clears the temporary buffer based on the cursor position.
1832 1835 """
1833 1836 if self._text_completing_pos:
1834 1837 document = self._control.document()
1835 1838 if self._text_completing_pos < document.characterCount():
1836 1839 cursor = self._control.textCursor()
1837 1840 pos = cursor.position()
1838 1841 text_cursor = self._control.textCursor()
1839 1842 text_cursor.setPosition(self._text_completing_pos)
1840 1843 if pos < self._text_completing_pos or \
1841 1844 cursor.blockNumber() > text_cursor.blockNumber():
1842 1845 self._clear_temporary_buffer()
1843 1846 self._text_completing_pos = 0
1844 1847 else:
1845 1848 self._clear_temporary_buffer()
1846 1849 self._text_completing_pos = 0
1847 1850
1848 1851 def _custom_context_menu_requested(self, pos):
1849 1852 """ Shows a context menu at the given QPoint (in widget coordinates).
1850 1853 """
1851 1854 menu = self._context_menu_make(pos)
1852 1855 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,559 +1,561 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3 """
4 4
5 5 #-----------------------------------------------------------------------------
6 6 # Imports
7 7 #-----------------------------------------------------------------------------
8 8
9 9 # Standard library imports
10 10 from collections import namedtuple
11 11 import os.path
12 12 import re
13 13 from subprocess import Popen
14 14 import sys
15 15 import time
16 16 from textwrap import dedent
17 17
18 18 # System library imports
19 19 from IPython.external.qt import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 23 transform_ipy_prompt
24 24 from IPython.utils.traitlets import Bool, Unicode
25 25 from frontend_widget import FrontendWidget
26 26 import styles
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Constants
30 30 #-----------------------------------------------------------------------------
31 31
32 32 # Default strings to build and display input and output prompts (and separators
33 33 # in between)
34 34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 36 default_input_sep = '\n'
37 37 default_output_sep = ''
38 38 default_output_sep2 = ''
39 39
40 40 # Base path for most payload sources.
41 41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42 42
43 43 if sys.platform.startswith('win'):
44 44 default_editor = 'notepad'
45 45 else:
46 46 default_editor = ''
47 47
48 48 #-----------------------------------------------------------------------------
49 49 # IPythonWidget class
50 50 #-----------------------------------------------------------------------------
51 51
52 52 class IPythonWidget(FrontendWidget):
53 53 """ A FrontendWidget for an IPython kernel.
54 54 """
55 55
56 56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 58 # settings.
59 59 custom_edit = Bool(False)
60 60 custom_edit_requested = QtCore.Signal(object, object)
61 61
62 62 editor = Unicode(default_editor, config=True,
63 63 help="""
64 64 A command for invoking a system text editor. If the string contains a
65 65 {filename} format specifier, it will be used. Otherwise, the filename
66 66 will be appended to the end the command.
67 67 """)
68 68
69 69 editor_line = Unicode(config=True,
70 70 help="""
71 71 The editor command to use when a specific line number is requested. The
72 72 string should contain two format specifiers: {line} and {filename}. If
73 73 this parameter is not specified, the line number option to the %edit
74 74 magic will be ignored.
75 75 """)
76 76
77 77 style_sheet = Unicode(config=True,
78 78 help="""
79 79 A CSS stylesheet. The stylesheet can contain classes for:
80 80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 83 """)
84 84
85 85 syntax_style = Unicode(config=True,
86 86 help="""
87 87 If not empty, use this Pygments style for syntax highlighting.
88 88 Otherwise, the style sheet is queried for Pygments style
89 89 information.
90 90 """)
91 91
92 92 # Prompts.
93 93 in_prompt = Unicode(default_in_prompt, config=True)
94 94 out_prompt = Unicode(default_out_prompt, config=True)
95 95 input_sep = Unicode(default_input_sep, config=True)
96 96 output_sep = Unicode(default_output_sep, config=True)
97 97 output_sep2 = Unicode(default_output_sep2, config=True)
98 98
99 99 # FrontendWidget protected class variables.
100 100 _input_splitter_class = IPythonInputSplitter
101 101 _transform_prompt = staticmethod(transform_ipy_prompt)
102 102
103 103 # IPythonWidget protected class variables.
104 104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 105 _payload_source_edit = zmq_shell_source + '.edit_magic'
106 106 _payload_source_exit = zmq_shell_source + '.ask_exit'
107 107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
108 108 _payload_source_page = 'IPython.zmq.page.page'
109 109 _retrying_history_request = False
110 110
111 111 #---------------------------------------------------------------------------
112 112 # 'object' interface
113 113 #---------------------------------------------------------------------------
114 114
115 115 def __init__(self, *args, **kw):
116 116 super(IPythonWidget, self).__init__(*args, **kw)
117 117
118 118 # IPythonWidget protected variables.
119 119 self._payload_handlers = {
120 120 self._payload_source_edit : self._handle_payload_edit,
121 121 self._payload_source_exit : self._handle_payload_exit,
122 122 self._payload_source_page : self._handle_payload_page,
123 123 self._payload_source_next_input : self._handle_payload_next_input }
124 124 self._previous_prompt_obj = None
125 125 self._keep_kernel_on_exit = None
126 126
127 127 # Initialize widget styling.
128 128 if self.style_sheet:
129 129 self._style_sheet_changed()
130 130 self._syntax_style_changed()
131 131 else:
132 132 self.set_default_style()
133 133
134 134 #---------------------------------------------------------------------------
135 135 # 'BaseFrontendMixin' abstract interface
136 136 #---------------------------------------------------------------------------
137 137
138 138 def _handle_complete_reply(self, rep):
139 139 """ Reimplemented to support IPython's improved completion machinery.
140 140 """
141 141 self.log.debug("complete: %s", rep.get('content', ''))
142 142 cursor = self._get_cursor()
143 143 info = self._request_info.get('complete')
144 144 if info and info.id == rep['parent_header']['msg_id'] and \
145 145 info.pos == cursor.position():
146 146 matches = rep['content']['matches']
147 147 text = rep['content']['matched_text']
148 148 offset = len(text)
149 149
150 150 # Clean up matches with period and path separators if the matched
151 151 # text has not been transformed. This is done by truncating all
152 152 # but the last component and then suitably decreasing the offset
153 153 # between the current cursor position and the start of completion.
154 154 if len(matches) > 1 and matches[0][:offset] == text:
155 155 parts = re.split(r'[./\\]', text)
156 156 sep_count = len(parts) - 1
157 157 if sep_count:
158 158 chop_length = sum(map(len, parts[:sep_count])) + sep_count
159 159 matches = [ match[chop_length:] for match in matches ]
160 160 offset -= chop_length
161 161
162 162 # Move the cursor to the start of the match and complete.
163 163 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
164 164 self._complete_with_items(cursor, matches)
165 165
166 166 def _handle_execute_reply(self, msg):
167 167 """ Reimplemented to support prompt requests.
168 168 """
169 169 msg_id = msg['parent_header'].get('msg_id')
170 170 info = self._request_info['execute'].get(msg_id)
171 171 if info and info.kind == 'prompt':
172 172 number = msg['content']['execution_count'] + 1
173 173 self._show_interpreter_prompt(number)
174 174 self._request_info['execute'].pop(msg_id)
175 175 else:
176 176 super(IPythonWidget, self)._handle_execute_reply(msg)
177 177
178 178 def _handle_history_reply(self, msg):
179 179 """ Implemented to handle history tail replies, which are only supported
180 180 by the IPython kernel.
181 181 """
182 182 content = msg['content']
183 183 if 'history' not in content:
184 184 self.log.error("History request failed: %r"%content)
185 185 if content.get('status', '') == 'aborted' and \
186 186 not self._retrying_history_request:
187 187 # a *different* action caused this request to be aborted, so
188 188 # we should try again.
189 189 self.log.error("Retrying aborted history request")
190 190 # prevent multiple retries of aborted requests:
191 191 self._retrying_history_request = True
192 192 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 193 time.sleep(0.25)
194 194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 195 else:
196 196 self._retrying_history_request = False
197 197 return
198 198 # reset retry flag
199 199 self._retrying_history_request = False
200 200 history_items = content['history']
201 201 self.log.debug("Received history reply with %i entries", len(history_items))
202 202 items = []
203 203 last_cell = u""
204 204 for _, _, cell in history_items:
205 205 cell = cell.rstrip()
206 206 if cell != last_cell:
207 207 items.append(cell)
208 208 last_cell = cell
209 209 self._set_history(items)
210 210
211 211 def _handle_pyout(self, msg):
212 212 """ Reimplemented for IPython-style "display hook".
213 213 """
214 214 self.log.debug("pyout: %s", msg.get('content', ''))
215 215 if not self._hidden and self._is_from_this_session(msg):
216 216 content = msg['content']
217 217 prompt_number = content['execution_count']
218 218 data = content['data']
219 219 if data.has_key('text/html'):
220 220 self._append_plain_text(self.output_sep, True)
221 221 self._append_html(self._make_out_prompt(prompt_number), True)
222 222 html = data['text/html']
223 223 self._append_plain_text('\n', True)
224 224 self._append_html(html + self.output_sep2, True)
225 225 elif data.has_key('text/plain'):
226 226 self._append_plain_text(self.output_sep, True)
227 227 self._append_html(self._make_out_prompt(prompt_number), True)
228 228 text = data['text/plain']
229 229 # If the repr is multiline, make sure we start on a new line,
230 230 # so that its lines are aligned.
231 231 if "\n" in text and not self.output_sep.endswith("\n"):
232 232 self._append_plain_text('\n', True)
233 233 self._append_plain_text(text + self.output_sep2, True)
234 234
235 235 def _handle_display_data(self, msg):
236 236 """ The base handler for the ``display_data`` message.
237 237 """
238 238 self.log.debug("display: %s", msg.get('content', ''))
239 239 # For now, we don't display data from other frontends, but we
240 240 # eventually will as this allows all frontends to monitor the display
241 241 # data. But we need to figure out how to handle this in the GUI.
242 242 if not self._hidden and self._is_from_this_session(msg):
243 243 source = msg['content']['source']
244 244 data = msg['content']['data']
245 245 metadata = msg['content']['metadata']
246 246 # In the regular IPythonWidget, we simply print the plain text
247 247 # representation.
248 248 if data.has_key('text/html'):
249 249 html = data['text/html']
250 250 self._append_html(html, True)
251 251 elif data.has_key('text/plain'):
252 252 text = data['text/plain']
253 253 self._append_plain_text(text, True)
254 254 # This newline seems to be needed for text and html output.
255 255 self._append_plain_text(u'\n', True)
256 256
257 257 def _started_channels(self):
258 258 """ Reimplemented to make a history request.
259 259 """
260 260 super(IPythonWidget, self)._started_channels()
261 261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
262 262 n=1000)
263 263 #---------------------------------------------------------------------------
264 264 # 'ConsoleWidget' public interface
265 265 #---------------------------------------------------------------------------
266 266
267 267 #---------------------------------------------------------------------------
268 268 # 'FrontendWidget' public interface
269 269 #---------------------------------------------------------------------------
270 270
271 271 def execute_file(self, path, hidden=False):
272 272 """ Reimplemented to use the 'run' magic.
273 273 """
274 274 # Use forward slashes on Windows to avoid escaping each separator.
275 275 if sys.platform == 'win32':
276 276 path = os.path.normpath(path).replace('\\', '/')
277 277
278 278 # Perhaps we should not be using %run directly, but while we
279 279 # are, it is necessary to quote or escape filenames containing spaces
280 280 # or quotes.
281 281
282 282 # In earlier code here, to minimize escaping, we sometimes quoted the
283 283 # filename with single quotes. But to do this, this code must be
284 284 # platform-aware, because run uses shlex rather than python string
285 285 # parsing, so that:
286 286 # * In Win: single quotes can be used in the filename without quoting,
287 287 # and we cannot use single quotes to quote the filename.
288 288 # * In *nix: we can escape double quotes in a double quoted filename,
289 289 # but can't escape single quotes in a single quoted filename.
290 290
291 291 # So to keep this code non-platform-specific and simple, we now only
292 292 # use double quotes to quote filenames, and escape when needed:
293 293 if ' ' in path or "'" in path or '"' in path:
294 294 path = '"%s"' % path.replace('"', '\\"')
295 295 self.execute('%%run %s' % path, hidden=hidden)
296 296
297 297 #---------------------------------------------------------------------------
298 298 # 'FrontendWidget' protected interface
299 299 #---------------------------------------------------------------------------
300 300
301 301 def _complete(self):
302 302 """ Reimplemented to support IPython's improved completion machinery.
303 303 """
304 304 # We let the kernel split the input line, so we *always* send an empty
305 305 # text field. Readline-based frontends do get a real text field which
306 306 # they can use.
307 307 text = ''
308 308
309 309 # Send the completion request to the kernel
310 310 msg_id = self.kernel_manager.shell_channel.complete(
311 311 text, # text
312 312 self._get_input_buffer_cursor_line(), # line
313 313 self._get_input_buffer_cursor_column(), # cursor_pos
314 314 self.input_buffer) # block
315 315 pos = self._get_cursor().position()
316 316 info = self._CompletionRequest(msg_id, pos)
317 317 self._request_info['complete'] = info
318 318
319 319 def _process_execute_error(self, msg):
320 320 """ Reimplemented for IPython-style traceback formatting.
321 321 """
322 322 content = msg['content']
323 323 traceback = '\n'.join(content['traceback']) + '\n'
324 324 if False:
325 325 # FIXME: For now, tracebacks come as plain text, so we can't use
326 326 # the html renderer yet. Once we refactor ultratb to produce
327 327 # properly styled tracebacks, this branch should be the default
328 328 traceback = traceback.replace(' ', '&nbsp;')
329 329 traceback = traceback.replace('\n', '<br/>')
330 330
331 331 ename = content['ename']
332 332 ename_styled = '<span class="error">%s</span>' % ename
333 333 traceback = traceback.replace(ename, ename_styled)
334 334
335 335 self._append_html(traceback)
336 336 else:
337 337 # This is the fallback for now, using plain text with ansi escapes
338 338 self._append_plain_text(traceback)
339 339
340 340 def _process_execute_payload(self, item):
341 341 """ Reimplemented to dispatch payloads to handler methods.
342 342 """
343 343 handler = self._payload_handlers.get(item['source'])
344 344 if handler is None:
345 345 # We have no handler for this type of payload, simply ignore it
346 346 return False
347 347 else:
348 348 handler(item)
349 349 return True
350 350
351 351 def _show_interpreter_prompt(self, number=None):
352 352 """ Reimplemented for IPython-style prompts.
353 353 """
354 354 # If a number was not specified, make a prompt number request.
355 355 if number is None:
356 356 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
357 357 info = self._ExecutionRequest(msg_id, 'prompt')
358 358 self._request_info['execute'][msg_id] = info
359 359 return
360 360
361 361 # Show a new prompt and save information about it so that it can be
362 362 # updated later if the prompt number turns out to be wrong.
363 363 self._prompt_sep = self.input_sep
364 364 self._show_prompt(self._make_in_prompt(number), html=True)
365 365 block = self._control.document().lastBlock()
366 366 length = len(self._prompt)
367 367 self._previous_prompt_obj = self._PromptBlock(block, length, number)
368 368
369 369 # Update continuation prompt to reflect (possibly) new prompt length.
370 370 self._set_continuation_prompt(
371 371 self._make_continuation_prompt(self._prompt), html=True)
372 372
373 373 def _show_interpreter_prompt_for_reply(self, msg):
374 374 """ Reimplemented for IPython-style prompts.
375 375 """
376 376 # Update the old prompt number if necessary.
377 377 content = msg['content']
378 378 # abort replies do not have any keys:
379 379 if content['status'] == 'aborted':
380 380 if self._previous_prompt_obj:
381 381 previous_prompt_number = self._previous_prompt_obj.number
382 382 else:
383 383 previous_prompt_number = 0
384 384 else:
385 385 previous_prompt_number = content['execution_count']
386 386 if self._previous_prompt_obj and \
387 387 self._previous_prompt_obj.number != previous_prompt_number:
388 388 block = self._previous_prompt_obj.block
389 389
390 390 # Make sure the prompt block has not been erased.
391 391 if block.isValid() and block.text():
392 392
393 393 # Remove the old prompt and insert a new prompt.
394 394 cursor = QtGui.QTextCursor(block)
395 395 cursor.movePosition(QtGui.QTextCursor.Right,
396 396 QtGui.QTextCursor.KeepAnchor,
397 397 self._previous_prompt_obj.length)
398 398 prompt = self._make_in_prompt(previous_prompt_number)
399 399 self._prompt = self._insert_html_fetching_plain_text(
400 400 cursor, prompt)
401 401
402 402 # When the HTML is inserted, Qt blows away the syntax
403 403 # highlighting for the line, so we need to rehighlight it.
404 404 self._highlighter.rehighlightBlock(cursor.block())
405 405
406 406 self._previous_prompt_obj = None
407 407
408 408 # Show a new prompt with the kernel's estimated prompt number.
409 409 self._show_interpreter_prompt(previous_prompt_number + 1)
410 410
411 411 #---------------------------------------------------------------------------
412 412 # 'IPythonWidget' interface
413 413 #---------------------------------------------------------------------------
414 414
415 415 def set_default_style(self, colors='lightbg'):
416 416 """ Sets the widget style to the class defaults.
417 417
418 418 Parameters:
419 419 -----------
420 420 colors : str, optional (default lightbg)
421 421 Whether to use the default IPython light background or dark
422 422 background or B&W style.
423 423 """
424 424 colors = colors.lower()
425 425 if colors=='lightbg':
426 426 self.style_sheet = styles.default_light_style_sheet
427 427 self.syntax_style = styles.default_light_syntax_style
428 428 elif colors=='linux':
429 429 self.style_sheet = styles.default_dark_style_sheet
430 430 self.syntax_style = styles.default_dark_syntax_style
431 431 elif colors=='nocolor':
432 432 self.style_sheet = styles.default_bw_style_sheet
433 433 self.syntax_style = styles.default_bw_syntax_style
434 434 else:
435 435 raise KeyError("No such color scheme: %s"%colors)
436 436
437 437 #---------------------------------------------------------------------------
438 438 # 'IPythonWidget' protected interface
439 439 #---------------------------------------------------------------------------
440 440
441 441 def _edit(self, filename, line=None):
442 442 """ Opens a Python script for editing.
443 443
444 444 Parameters:
445 445 -----------
446 446 filename : str
447 447 A path to a local system file.
448 448
449 449 line : int, optional
450 450 A line of interest in the file.
451 451 """
452 452 if self.custom_edit:
453 453 self.custom_edit_requested.emit(filename, line)
454 454 elif not self.editor:
455 455 self._append_plain_text('No default editor available.\n'
456 456 'Specify a GUI text editor in the `IPythonWidget.editor` '
457 457 'configurable to enable the %edit magic')
458 458 else:
459 459 try:
460 460 filename = '"%s"' % filename
461 461 if line and self.editor_line:
462 462 command = self.editor_line.format(filename=filename,
463 463 line=line)
464 464 else:
465 465 try:
466 466 command = self.editor.format()
467 467 except KeyError:
468 468 command = self.editor.format(filename=filename)
469 469 else:
470 470 command += ' ' + filename
471 471 except KeyError:
472 472 self._append_plain_text('Invalid editor command.\n')
473 473 else:
474 474 try:
475 475 Popen(command, shell=True)
476 476 except OSError:
477 477 msg = 'Opening editor with command "%s" failed.\n'
478 478 self._append_plain_text(msg % command)
479 479
480 480 def _make_in_prompt(self, number):
481 481 """ Given a prompt number, returns an HTML In prompt.
482 482 """
483 483 try:
484 484 body = self.in_prompt % number
485 485 except TypeError:
486 486 # allow in_prompt to leave out number, e.g. '>>> '
487 487 body = self.in_prompt
488 488 return '<span class="in-prompt">%s</span>' % body
489 489
490 490 def _make_continuation_prompt(self, prompt):
491 491 """ Given a plain text version of an In prompt, returns an HTML
492 492 continuation prompt.
493 493 """
494 494 end_chars = '...: '
495 495 space_count = len(prompt.lstrip('\n')) - len(end_chars)
496 496 body = '&nbsp;' * space_count + end_chars
497 497 return '<span class="in-prompt">%s</span>' % body
498 498
499 499 def _make_out_prompt(self, number):
500 500 """ Given a prompt number, returns an HTML Out prompt.
501 501 """
502 502 body = self.out_prompt % number
503 503 return '<span class="out-prompt">%s</span>' % body
504 504
505 505 #------ Payload handlers --------------------------------------------------
506 506
507 507 # Payload handlers with a generic interface: each takes the opaque payload
508 508 # dict, unpacks it and calls the underlying functions with the necessary
509 509 # arguments.
510 510
511 511 def _handle_payload_edit(self, item):
512 512 self._edit(item['filename'], item['line_number'])
513 513
514 514 def _handle_payload_exit(self, item):
515 515 self._keep_kernel_on_exit = item['keepkernel']
516 516 self.exit_requested.emit(self)
517 517
518 518 def _handle_payload_next_input(self, item):
519 519 self.input_buffer = dedent(item['text'].rstrip())
520 520
521 521 def _handle_payload_page(self, item):
522 522 # Since the plain text widget supports only a very small subset of HTML
523 523 # and we have no control over the HTML source, we only page HTML
524 524 # payloads in the rich text widget.
525 525 if item['html'] and self.kind == 'rich':
526 526 self._page(item['html'], html=True)
527 527 else:
528 528 self._page(item['text'], html=False)
529 529
530 530 #------ Trait change handlers --------------------------------------------
531 531
532 532 def _style_sheet_changed(self):
533 533 """ Set the style sheets of the underlying widgets.
534 534 """
535 535 self.setStyleSheet(self.style_sheet)
536 self._control.document().setDefaultStyleSheet(self.style_sheet)
537 if self._page_control:
536 if self._control is not None:
537 self._control.document().setDefaultStyleSheet(self.style_sheet)
538 bg_color = self._control.palette().window().color()
539 self._ansi_processor.set_background_color(bg_color)
540
541 if self._page_control is not None:
538 542 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
539 543
540 bg_color = self._control.palette().window().color()
541 self._ansi_processor.set_background_color(bg_color)
542 544
543 545
544 546 def _syntax_style_changed(self):
545 547 """ Set the style for the syntax highlighter.
546 548 """
547 549 if self._highlighter is None:
548 550 # ignore premature calls
549 551 return
550 552 if self.syntax_style:
551 553 self._highlighter.set_style(self.syntax_style)
552 554 else:
553 555 self._highlighter.set_style_sheet(self.style_sheet)
554 556
555 557 #------ Trait default initializers -----------------------------------------
556 558
557 559 def _banner_default(self):
558 560 from IPython.core.usage import default_gui_banner
559 561 return default_gui_banner
@@ -1,224 +1,224 b''
1 1 # System library imports.
2 2 from IPython.external.qt import QtGui
3 3 from pygments.formatters.html import HtmlFormatter
4 4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
5 5 from pygments.lexers import PythonLexer
6 6 from pygments.styles import get_style_by_name
7 7
8 8
9 9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 10 """ Split ``text`` into (tokentype, text) pairs.
11 11
12 12 Monkeypatched to store the final stack on the object itself.
13 13 """
14 14 pos = 0
15 15 tokendefs = self._tokens
16 16 if hasattr(self, '_saved_state_stack'):
17 17 statestack = list(self._saved_state_stack)
18 18 else:
19 19 statestack = list(stack)
20 20 statetokens = tokendefs[statestack[-1]]
21 21 while 1:
22 22 for rexmatch, action, new_state in statetokens:
23 23 m = rexmatch(text, pos)
24 24 if m:
25 25 if type(action) is _TokenType:
26 26 yield pos, action, m.group()
27 27 else:
28 28 for item in action(self, m):
29 29 yield item
30 30 pos = m.end()
31 31 if new_state is not None:
32 32 # state transition
33 33 if isinstance(new_state, tuple):
34 34 for state in new_state:
35 35 if state == '#pop':
36 36 statestack.pop()
37 37 elif state == '#push':
38 38 statestack.append(statestack[-1])
39 39 else:
40 40 statestack.append(state)
41 41 elif isinstance(new_state, int):
42 42 # pop
43 43 del statestack[new_state:]
44 44 elif new_state == '#push':
45 45 statestack.append(statestack[-1])
46 46 else:
47 47 assert False, "wrong state def: %r" % new_state
48 48 statetokens = tokendefs[statestack[-1]]
49 49 break
50 50 else:
51 51 try:
52 52 if text[pos] == '\n':
53 53 # at EOL, reset state to "root"
54 54 pos += 1
55 55 statestack = ['root']
56 56 statetokens = tokendefs['root']
57 57 yield pos, Text, u'\n'
58 58 continue
59 59 yield pos, Error, text[pos]
60 60 pos += 1
61 61 except IndexError:
62 62 break
63 63 self._saved_state_stack = list(statestack)
64 64
65 65 # Monkeypatch!
66 66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67 67
68 68
69 69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
70 70 """ Storage for the user data associated with each line.
71 71 """
72 72
73 73 syntax_stack = ('root',)
74 74
75 75 def __init__(self, **kwds):
76 76 for key, value in kwds.iteritems():
77 77 setattr(self, key, value)
78 78 QtGui.QTextBlockUserData.__init__(self)
79 79
80 80 def __repr__(self):
81 81 attrs = ['syntax_stack']
82 82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 83 for attr in attrs ])
84 84 return 'PygmentsBlockUserData(%s)' % kwds
85 85
86 86
87 87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 88 """ Syntax highlighter that uses Pygments for parsing. """
89 89
90 90 #---------------------------------------------------------------------------
91 91 # 'QSyntaxHighlighter' interface
92 92 #---------------------------------------------------------------------------
93 93
94 94 def __init__(self, parent, lexer=None):
95 95 super(PygmentsHighlighter, self).__init__(parent)
96 96
97 97 self._document = QtGui.QTextDocument()
98 98 self._formatter = HtmlFormatter(nowrap=True)
99 99 self._lexer = lexer if lexer else PythonLexer()
100 100 self.set_style('default')
101 101
102 102 def highlightBlock(self, string):
103 103 """ Highlight a block of text.
104 104 """
105 105 prev_data = self.currentBlock().previous().userData()
106 106 if prev_data is not None:
107 107 self._lexer._saved_state_stack = prev_data.syntax_stack
108 108 elif hasattr(self._lexer, '_saved_state_stack'):
109 109 del self._lexer._saved_state_stack
110 110
111 111 # Lex the text using Pygments
112 112 index = 0
113 113 for token, text in self._lexer.get_tokens(string):
114 114 length = len(text)
115 115 self.setFormat(index, length, self._get_format(token))
116 116 index += length
117 117
118 118 if hasattr(self._lexer, '_saved_state_stack'):
119 119 data = PygmentsBlockUserData(
120 120 syntax_stack=self._lexer._saved_state_stack)
121 121 self.currentBlock().setUserData(data)
122 122 # Clean up for the next go-round.
123 123 del self._lexer._saved_state_stack
124 124
125 125 #---------------------------------------------------------------------------
126 126 # 'PygmentsHighlighter' interface
127 127 #---------------------------------------------------------------------------
128 128
129 129 def set_style(self, style):
130 130 """ Sets the style to the specified Pygments style.
131 131 """
132 132 if isinstance(style, basestring):
133 133 style = get_style_by_name(style)
134 134 self._style = style
135 135 self._clear_caches()
136 136
137 137 def set_style_sheet(self, stylesheet):
138 138 """ Sets a CSS stylesheet. The classes in the stylesheet should
139 139 correspond to those generated by:
140 140
141 141 pygmentize -S <style> -f html
142 142
143 143 Note that 'set_style' and 'set_style_sheet' completely override each
144 144 other, i.e. they cannot be used in conjunction.
145 145 """
146 146 self._document.setDefaultStyleSheet(stylesheet)
147 147 self._style = None
148 148 self._clear_caches()
149 149
150 150 #---------------------------------------------------------------------------
151 151 # Protected interface
152 152 #---------------------------------------------------------------------------
153 153
154 154 def _clear_caches(self):
155 155 """ Clear caches for brushes and formats.
156 156 """
157 157 self._brushes = {}
158 158 self._formats = {}
159 159
160 160 def _get_format(self, token):
161 161 """ Returns a QTextCharFormat for token or None.
162 162 """
163 163 if token in self._formats:
164 164 return self._formats[token]
165 165
166 166 if self._style is None:
167 167 result = self._get_format_from_document(token, self._document)
168 168 else:
169 169 result = self._get_format_from_style(token, self._style)
170 170
171 171 self._formats[token] = result
172 172 return result
173 173
174 174 def _get_format_from_document(self, token, document):
175 175 """ Returns a QTextCharFormat for token by
176 176 """
177 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
177 code, html = self._formatter._format_lines([(token, u'dummy')]).next()
178 178 self._document.setHtml(html)
179 179 return QtGui.QTextCursor(self._document).charFormat()
180 180
181 181 def _get_format_from_style(self, token, style):
182 182 """ Returns a QTextCharFormat for token by reading a Pygments style.
183 183 """
184 184 result = QtGui.QTextCharFormat()
185 185 for key, value in style.style_for_token(token).items():
186 186 if value:
187 187 if key == 'color':
188 188 result.setForeground(self._get_brush(value))
189 189 elif key == 'bgcolor':
190 190 result.setBackground(self._get_brush(value))
191 191 elif key == 'bold':
192 192 result.setFontWeight(QtGui.QFont.Bold)
193 193 elif key == 'italic':
194 194 result.setFontItalic(True)
195 195 elif key == 'underline':
196 196 result.setUnderlineStyle(
197 197 QtGui.QTextCharFormat.SingleUnderline)
198 198 elif key == 'sans':
199 199 result.setFontStyleHint(QtGui.QFont.SansSerif)
200 200 elif key == 'roman':
201 201 result.setFontStyleHint(QtGui.QFont.Times)
202 202 elif key == 'mono':
203 203 result.setFontStyleHint(QtGui.QFont.TypeWriter)
204 204 return result
205 205
206 206 def _get_brush(self, color):
207 207 """ Returns a brush for the color.
208 208 """
209 209 result = self._brushes.get(color)
210 210 if result is None:
211 211 qcolor = self._get_color(color)
212 212 result = QtGui.QBrush(qcolor)
213 213 self._brushes[color] = result
214 214 return result
215 215
216 216 def _get_color(self, color):
217 217 """ Returns a QColor built from a Pygments color string.
218 218 """
219 219 qcolor = QtGui.QColor()
220 220 qcolor.setRgb(int(color[:2], base=16),
221 221 int(color[2:4], base=16),
222 222 int(color[4:6], base=16))
223 223 return qcolor
224 224
@@ -1,353 +1,356 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5
6 6 Authors:
7 7
8 8 * Evan Patterson
9 9 * Min RK
10 10 * Erik Tollerud
11 11 * Fernando Perez
12 12 * Bussonnier Matthias
13 13 * Thomas Kluyver
14 14 * Paul Ivanov
15 15
16 16 """
17 17
18 18 #-----------------------------------------------------------------------------
19 19 # Imports
20 20 #-----------------------------------------------------------------------------
21 21
22 22 # stdlib imports
23 23 import json
24 24 import os
25 25 import signal
26 26 import sys
27 27 import uuid
28 28
29 29 # System library imports
30 30 from IPython.external.qt import QtCore, QtGui
31 31
32 32 # Local imports
33 33 from IPython.config.application import boolean_flag, catch_config_error
34 34 from IPython.core.application import BaseIPythonApplication
35 35 from IPython.core.profiledir import ProfileDir
36 36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
37 37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
38 38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
39 39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
40 40 from IPython.frontend.qt.console import styles
41 41 from IPython.frontend.qt.console.mainwindow import MainWindow
42 42 from IPython.frontend.qt.kernelmanager import QtKernelManager
43 43 from IPython.utils.path import filefind
44 44 from IPython.utils.py3compat import str_to_bytes
45 45 from IPython.utils.traitlets import (
46 46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
47 47 )
48 48 from IPython.zmq.ipkernel import IPKernelApp
49 49 from IPython.zmq.session import Session, default_secure
50 50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 51
52 52 from IPython.frontend.consoleapp import (
53 53 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
54 54 )
55 55
56 56 #-----------------------------------------------------------------------------
57 57 # Network Constants
58 58 #-----------------------------------------------------------------------------
59 59
60 60 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
61 61
62 62 #-----------------------------------------------------------------------------
63 63 # Globals
64 64 #-----------------------------------------------------------------------------
65 65
66 66 _examples = """
67 67 ipython qtconsole # start the qtconsole
68 68 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
69 69 """
70 70
71 71 #-----------------------------------------------------------------------------
72 72 # Aliases and Flags
73 73 #-----------------------------------------------------------------------------
74 74
75 75 # start with copy of flags
76 76 flags = dict(flags)
77 77 qt_flags = {
78 78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 79 "Use a pure Python kernel instead of an IPython kernel."),
80 80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 81 "Disable rich text support."),
82 82 }
83 83 qt_flags.update(boolean_flag(
84 84 'gui-completion', 'ConsoleWidget.gui_completion',
85 85 "use a GUI widget for tab completion",
86 86 "use plaintext output for completion"
87 87 ))
88 88 # and app_flags from the Console Mixin
89 89 qt_flags.update(app_flags)
90 90 # add frontend flags to the full set
91 91 flags.update(qt_flags)
92 92
93 93 # start with copy of front&backend aliases list
94 94 aliases = dict(aliases)
95 95 qt_aliases = dict(
96 96
97 97 style = 'IPythonWidget.syntax_style',
98 98 stylesheet = 'IPythonQtConsoleApp.stylesheet',
99 99 colors = 'ZMQInteractiveShell.colors',
100 100
101 101 editor = 'IPythonWidget.editor',
102 102 paging = 'ConsoleWidget.paging',
103 103 )
104 104 # and app_aliases from the Console Mixin
105 105 qt_aliases.update(app_aliases)
106 106 # add frontend aliases to the full set
107 107 aliases.update(qt_aliases)
108 108
109 109 # get flags&aliases into sets, and remove a couple that
110 110 # shouldn't be scrubbed from backend flags:
111 111 qt_aliases = set(qt_aliases.keys())
112 112 qt_aliases.remove('colors')
113 113 qt_flags = set(qt_flags.keys())
114 114
115 115 #-----------------------------------------------------------------------------
116 116 # Classes
117 117 #-----------------------------------------------------------------------------
118 118
119 119 #-----------------------------------------------------------------------------
120 120 # IPythonQtConsole
121 121 #-----------------------------------------------------------------------------
122 122
123 123
124 124 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
125 125 name = 'ipython-qtconsole'
126 126
127 127 description = """
128 128 The IPython QtConsole.
129 129
130 130 This launches a Console-style application using Qt. It is not a full
131 131 console, in that launched terminal subprocesses will not be able to accept
132 132 input.
133 133
134 134 The QtConsole supports various extra features beyond the Terminal IPython
135 135 shell, such as inline plotting with matplotlib, via:
136 136
137 137 ipython qtconsole --pylab=inline
138 138
139 139 as well as saving your session as HTML, and printing the output.
140 140
141 141 """
142 142 examples = _examples
143 143
144 144 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
145 145 flags = Dict(flags)
146 146 aliases = Dict(aliases)
147 147 frontend_flags = Any(qt_flags)
148 148 frontend_aliases = Any(qt_aliases)
149 149 kernel_manager_class = QtKernelManager
150 150
151 151 stylesheet = Unicode('', config=True,
152 152 help="path to a custom CSS stylesheet")
153 153
154 154 plain = CBool(False, config=True,
155 155 help="Use a plaintext widget instead of rich text (plain can't print/save).")
156 156
157 157 def _pure_changed(self, name, old, new):
158 158 kind = 'plain' if self.plain else 'rich'
159 159 self.config.ConsoleWidget.kind = kind
160 160 if self.pure:
161 161 self.widget_factory = FrontendWidget
162 162 elif self.plain:
163 163 self.widget_factory = IPythonWidget
164 164 else:
165 165 self.widget_factory = RichIPythonWidget
166 166
167 167 _plain_changed = _pure_changed
168 168
169 169 # the factory for creating a widget
170 170 widget_factory = Any(RichIPythonWidget)
171 171
172 172 def parse_command_line(self, argv=None):
173 173 super(IPythonQtConsoleApp, self).parse_command_line(argv)
174 174 self.build_kernel_argv(argv)
175 175
176 176
177 177 def new_frontend_master(self):
178 178 """ Create and return new frontend attached to new kernel, launched on localhost.
179 179 """
180 180 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
181 181 kernel_manager = self.kernel_manager_class(
182 182 ip=ip,
183 183 connection_file=self._new_connection_file(),
184 184 config=self.config,
185 185 )
186 186 # start the kernel
187 187 kwargs = dict(ipython=not self.pure)
188 188 kwargs['extra_arguments'] = self.kernel_argv
189 189 kernel_manager.start_kernel(**kwargs)
190 190 kernel_manager.start_channels()
191 191 widget = self.widget_factory(config=self.config,
192 192 local_kernel=True)
193 self.init_colors(widget)
193 194 widget.kernel_manager = kernel_manager
194 195 widget._existing = False
195 196 widget._may_close = True
196 197 widget._confirm_exit = self.confirm_exit
197 198 return widget
198 199
199 200 def new_frontend_slave(self, current_widget):
200 201 """Create and return a new frontend attached to an existing kernel.
201 202
202 203 Parameters
203 204 ----------
204 205 current_widget : IPythonWidget
205 206 The IPythonWidget whose kernel this frontend is to share
206 207 """
207 208 kernel_manager = self.kernel_manager_class(
208 209 connection_file=current_widget.kernel_manager.connection_file,
209 210 config = self.config,
210 211 )
211 212 kernel_manager.load_connection_file()
212 213 kernel_manager.start_channels()
213 214 widget = self.widget_factory(config=self.config,
214 215 local_kernel=False)
216 self.init_colors(widget)
215 217 widget._existing = True
216 218 widget._may_close = False
217 219 widget._confirm_exit = False
218 220 widget.kernel_manager = kernel_manager
219 221 return widget
220 222
221 223 def init_qt_elements(self):
222 224 # Create the widget.
223 225 self.app = QtGui.QApplication([])
224 226
225 227 base_path = os.path.abspath(os.path.dirname(__file__))
226 228 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
227 229 self.app.icon = QtGui.QIcon(icon_path)
228 230 QtGui.QApplication.setWindowIcon(self.app.icon)
229 231
230 232 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
231 233 self.widget = self.widget_factory(config=self.config,
232 234 local_kernel=local_kernel)
235 self.init_colors(self.widget)
233 236 self.widget._existing = self.existing
234 237 self.widget._may_close = not self.existing
235 238 self.widget._confirm_exit = self.confirm_exit
236 239
237 240 self.widget.kernel_manager = self.kernel_manager
238 241 self.window = MainWindow(self.app,
239 242 confirm_exit=self.confirm_exit,
240 243 new_frontend_factory=self.new_frontend_master,
241 244 slave_frontend_factory=self.new_frontend_slave,
242 245 )
243 246 self.window.log = self.log
244 247 self.window.add_tab_with_frontend(self.widget)
245 248 self.window.init_menu_bar()
246 249
247 250 self.window.setWindowTitle('Python' if self.pure else 'IPython')
248 251
249 def init_colors(self):
252 def init_colors(self, widget):
250 253 """Configure the coloring of the widget"""
251 254 # Note: This will be dramatically simplified when colors
252 255 # are removed from the backend.
253 256
254 257 if self.pure:
255 258 # only IPythonWidget supports styling
256 259 return
257 260
258 261 # parse the colors arg down to current known labels
259 262 try:
260 263 colors = self.config.ZMQInteractiveShell.colors
261 264 except AttributeError:
262 265 colors = None
263 266 try:
264 267 style = self.config.IPythonWidget.syntax_style
265 268 except AttributeError:
266 269 style = None
270 try:
271 sheet = self.config.IPythonWidget.style_sheet
272 except AttributeError:
273 sheet = None
267 274
268 275 # find the value for colors:
269 276 if colors:
270 277 colors=colors.lower()
271 278 if colors in ('lightbg', 'light'):
272 279 colors='lightbg'
273 280 elif colors in ('dark', 'linux'):
274 281 colors='linux'
275 282 else:
276 283 colors='nocolor'
277 284 elif style:
278 285 if style=='bw':
279 286 colors='nocolor'
280 287 elif styles.dark_style(style):
281 288 colors='linux'
282 289 else:
283 290 colors='lightbg'
284 291 else:
285 292 colors=None
286 293
287 # Configure the style.
288 widget = self.widget
294 # Configure the style
289 295 if style:
290 296 widget.style_sheet = styles.sheet_from_template(style, colors)
291 297 widget.syntax_style = style
292 298 widget._syntax_style_changed()
293 299 widget._style_sheet_changed()
294 300 elif colors:
295 # use a default style
301 # use a default dark/light/bw style
296 302 widget.set_default_style(colors=colors)
297 else:
298 # this is redundant for now, but allows the widget's
299 # defaults to change
300 widget.set_default_style()
301 303
302 304 if self.stylesheet:
303 # we got an expicit stylesheet
305 # we got an explicit stylesheet
304 306 if os.path.isfile(self.stylesheet):
305 307 with open(self.stylesheet) as f:
306 308 sheet = f.read()
307 widget.style_sheet = sheet
308 widget._style_sheet_changed()
309 309 else:
310 raise IOError("Stylesheet %r not found."%self.stylesheet)
310 raise IOError("Stylesheet %r not found." % self.stylesheet)
311 if sheet:
312 widget.style_sheet = sheet
313 widget._style_sheet_changed()
314
311 315
312 316 def init_signal(self):
313 317 """allow clean shutdown on sigint"""
314 318 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
315 319 # need a timer, so that QApplication doesn't block until a real
316 320 # Qt event fires (can require mouse movement)
317 321 # timer trick from http://stackoverflow.com/q/4938723/938949
318 322 timer = QtCore.QTimer()
319 323 # Let the interpreter run each 200 ms:
320 324 timer.timeout.connect(lambda: None)
321 325 timer.start(200)
322 326 # hold onto ref, so the timer doesn't get cleaned up
323 327 self._sigint_timer = timer
324 328
325 329 @catch_config_error
326 330 def initialize(self, argv=None):
327 331 super(IPythonQtConsoleApp, self).initialize(argv)
328 332 IPythonConsoleApp.initialize(self,argv)
329 333 self.init_qt_elements()
330 self.init_colors()
331 334 self.init_signal()
332 335
333 336 def start(self):
334 337
335 338 # draw the window
336 339 self.window.show()
337 340 self.window.raise_()
338 341
339 342 # Start the application main loop.
340 343 self.app.exec_()
341 344
342 345 #-----------------------------------------------------------------------------
343 346 # Main entry point
344 347 #-----------------------------------------------------------------------------
345 348
346 349 def main():
347 350 app = IPythonQtConsoleApp()
348 351 app.initialize()
349 352 app.start()
350 353
351 354
352 355 if __name__ == '__main__':
353 356 main()
@@ -1,119 +1,119 b''
1 1 """ Style utilities, templates, and defaults for syntax highlighting widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 from colorsys import rgb_to_hls
8 8 from pygments.styles import get_style_by_name
9 9 from pygments.token import Token
10 10
11 11 #-----------------------------------------------------------------------------
12 12 # Constants
13 13 #-----------------------------------------------------------------------------
14 14
15 15 # The default light style sheet: black text on a white background.
16 16 default_light_style_template = '''
17 17 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
18 18 color: %(fgcolor)s ;
19 19 selection-background-color: %(select)s}
20 20 .error { color: red; }
21 21 .in-prompt { color: navy; }
22 22 .in-prompt-number { font-weight: bold; }
23 23 .out-prompt { color: darkred; }
24 24 .out-prompt-number { font-weight: bold; }
25 25 '''
26 26 default_light_style_sheet = default_light_style_template%dict(
27 27 bgcolor='white', fgcolor='black', select="#ccc")
28 28 default_light_syntax_style = 'default'
29 29
30 30 # The default dark style sheet: white text on a black background.
31 31 default_dark_style_template = '''
32 32 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
33 33 color: %(fgcolor)s ;
34 34 selection-background-color: %(select)s}
35 35 QFrame { border: 1px solid grey; }
36 36 .error { color: red; }
37 37 .in-prompt { color: lime; }
38 38 .in-prompt-number { color: lime; font-weight: bold; }
39 39 .out-prompt { color: red; }
40 40 .out-prompt-number { color: red; font-weight: bold; }
41 41 '''
42 42 default_dark_style_sheet = default_dark_style_template%dict(
43 43 bgcolor='black', fgcolor='white', select="#555")
44 44 default_dark_syntax_style = 'monokai'
45 45
46 46 # The default monochrome
47 47 default_bw_style_sheet = '''
48 48 QPlainTextEdit, QTextEdit { background-color: white;
49 49 color: black ;
50 50 selection-background-color: #cccccc}
51 51 .in-prompt-number { font-weight: bold; }
52 52 .out-prompt-number { font-weight: bold; }
53 53 '''
54 54 default_bw_syntax_style = 'bw'
55 55
56 56
57 57 def hex_to_rgb(color):
58 58 """Convert a hex color to rgb integer tuple."""
59 59 if color.startswith('#'):
60 60 color = color[1:]
61 61 if len(color) == 3:
62 62 color = ''.join([c*2 for c in color])
63 63 if len(color) != 6:
64 64 return False
65 65 try:
66 66 r = int(color[:2],16)
67 g = int(color[:2],16)
68 b = int(color[:2],16)
67 g = int(color[2:4],16)
68 b = int(color[4:],16)
69 69 except ValueError:
70 70 return False
71 71 else:
72 72 return r,g,b
73 73
74 74 def dark_color(color):
75 75 """Check whether a color is 'dark'.
76 76
77 77 Currently, this is simply whether the luminance is <50%"""
78 78 rgb = hex_to_rgb(color)
79 79 if rgb:
80 80 return rgb_to_hls(*rgb)[1] < 128
81 81 else: # default to False
82 82 return False
83 83
84 84 def dark_style(stylename):
85 85 """Guess whether the background of the style with name 'stylename'
86 86 counts as 'dark'."""
87 87 return dark_color(get_style_by_name(stylename).background_color)
88 88
89 89 def get_colors(stylename):
90 90 """Construct the keys to be used building the base stylesheet
91 91 from a templatee."""
92 92 style = get_style_by_name(stylename)
93 93 fgcolor = style.style_for_token(Token.Text)['color'] or ''
94 94 if len(fgcolor) in (3,6):
95 95 # could be 'abcdef' or 'ace' hex, which needs '#' prefix
96 96 try:
97 97 int(fgcolor, 16)
98 98 except TypeError:
99 99 pass
100 100 else:
101 101 fgcolor = "#"+fgcolor
102 102
103 103 return dict(
104 104 bgcolor = style.background_color,
105 105 select = style.highlight_color,
106 106 fgcolor = fgcolor
107 107 )
108 108
109 109 def sheet_from_template(name, colors='lightbg'):
110 110 """Use one of the base templates, and set bg/fg/select colors."""
111 111 colors = colors.lower()
112 112 if colors=='lightbg':
113 113 return default_light_style_template%get_colors(name)
114 114 elif colors=='linux':
115 115 return default_dark_style_template%get_colors(name)
116 116 elif colors=='nocolor':
117 117 return default_bw_style_sheet
118 118 else:
119 119 raise KeyError("No such color scheme: %s"%colors)
General Comments 0
You need to be logged in to leave comments. Login now