##// END OF EJS Templates
create menu bar on OSX with some action...
Matthias BUSSONNIER -
Show More
@@ -1,1787 +1,1870 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, Int, 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 = Int(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 = Int(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 141 # When the control key is down, these keys are mapped.
142 142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
148 148 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
149 149 if not sys.platform == 'darwin':
150 150 # On OS X, Ctrl-E already does the right thing, whereas End moves the
151 151 # cursor to the bottom of the buffer.
152 152 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
153 153
154 154 # The shortcuts defined by this widget. We need to keep track of these to
155 155 # support 'override_shortcuts' above.
156 156 _shortcuts = set(_ctrl_down_remap.keys() +
157 157 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
158 158 QtCore.Qt.Key_V ])
159 159
160 160 #---------------------------------------------------------------------------
161 161 # 'QObject' interface
162 162 #---------------------------------------------------------------------------
163 163
164 164 def __init__(self, parent=None, **kw):
165 165 """ Create a ConsoleWidget.
166 166
167 167 Parameters:
168 168 -----------
169 169 parent : QWidget, optional [default None]
170 170 The parent for this widget.
171 171 """
172 172 QtGui.QWidget.__init__(self, parent)
173 173 LoggingConfigurable.__init__(self, **kw)
174 174
175 175 # Create the layout and underlying text widget.
176 176 layout = QtGui.QStackedLayout(self)
177 177 layout.setContentsMargins(0, 0, 0, 0)
178 178 self._control = self._create_control()
179 179 self._page_control = None
180 180 self._splitter = None
181 181 if self.paging in ('hsplit', 'vsplit'):
182 182 self._splitter = QtGui.QSplitter()
183 183 if self.paging == 'hsplit':
184 184 self._splitter.setOrientation(QtCore.Qt.Horizontal)
185 185 else:
186 186 self._splitter.setOrientation(QtCore.Qt.Vertical)
187 187 self._splitter.addWidget(self._control)
188 188 layout.addWidget(self._splitter)
189 189 else:
190 190 layout.addWidget(self._control)
191 191
192 192 # Create the paging widget, if necessary.
193 193 if self.paging in ('inside', 'hsplit', 'vsplit'):
194 194 self._page_control = self._create_page_control()
195 195 if self._splitter:
196 196 self._page_control.hide()
197 197 self._splitter.addWidget(self._page_control)
198 198 else:
199 199 layout.addWidget(self._page_control)
200 200
201 201 # Initialize protected variables. Some variables contain useful state
202 202 # information for subclasses; they should be considered read-only.
203 203 self._append_before_prompt_pos = 0
204 204 self._ansi_processor = QtAnsiCodeProcessor()
205 205 self._completion_widget = CompletionWidget(self._control)
206 206 self._continuation_prompt = '> '
207 207 self._continuation_prompt_html = None
208 208 self._executing = False
209 209 self._filter_drag = False
210 210 self._filter_resize = False
211 211 self._html_exporter = HtmlExporter(self._control)
212 212 self._input_buffer_executing = ''
213 213 self._input_buffer_pending = ''
214 214 self._kill_ring = QtKillRing(self._control)
215 215 self._prompt = ''
216 216 self._prompt_html = None
217 217 self._prompt_pos = 0
218 218 self._prompt_sep = ''
219 219 self._reading = False
220 220 self._reading_callback = None
221 221 self._tab_width = 8
222 222 self._text_completing_pos = 0
223 223
224 224 # Set a monospaced font.
225 225 self.reset_font()
226 226
227 227 # Configure actions.
228 228 action = QtGui.QAction('Print', None)
229 229 action.setEnabled(True)
230 230 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
231 231 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
232 232 # Only override the default if there is a collision.
233 233 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
234 234 printkey = "Ctrl+Shift+P"
235 235 action.setShortcut(printkey)
236 236 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
237 237 action.triggered.connect(self.print_)
238 238 self.addAction(action)
239 self._print_action = action
239 self.print_action = action
240 240
241 241 action = QtGui.QAction('Save as HTML/XML', None)
242 242 action.setShortcut(QtGui.QKeySequence.Save)
243 243 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
244 244 action.triggered.connect(self.export_html)
245 245 self.addAction(action)
246 self._export_action = action
246 self.export_action = action
247 247
248 248 action = QtGui.QAction('Select All', None)
249 249 action.setEnabled(True)
250 250 action.setShortcut(QtGui.QKeySequence.SelectAll)
251 251 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
252 252 action.triggered.connect(self.select_all)
253 253 self.addAction(action)
254 self._select_all_action = action
254 self.select_all_action = action
255
256 self.increase_font_size = QtGui.QAction("Bigger Font",
257 self,
258 shortcut="Ctrl++",
259 statusTip="Increase the font size by one point",
260 triggered=self._increase_font_size)
261 self.addAction(self.increase_font_size)
262
263 self.decrease_font_size = QtGui.QAction("Smaller Font",
264 self,
265 shortcut="Ctrl+-",
266 statusTip="Decrease the font size by one point",
267 triggered=self._decrease_font_size)
268 self.addAction(self.decrease_font_size)
269
270 self.reset_font_size = QtGui.QAction("Normal Font",
271 self,
272 shortcut="Ctrl+0",
273 statusTip="Restore the Normal font size",
274 triggered=self.reset_font)
275 self.addAction(self.reset_font_size)
276
277 self.undo_action = QtGui.QAction("Undo",
278 self,
279 shortcut="Ctrl+Z",
280 statusTip="Undo last action if possible",
281 triggered=self._control.undo)
282 self.addAction(self.undo_action)
283
284 self.redo_action = QtGui.QAction("Redo",
285 self,
286 shortcut="Ctrl+Shift+Z",
287 statusTip="Redo last action if possible",
288 triggered=self._control.redo)
289 self.addAction(self.redo_action)
290
291 self.reset_action = QtGui.QAction("Reset",
292 self,
293 statusTip="Clear all varible from workspace",
294 triggered=self.reset_magic)
295 self.addAction(self.reset_action)
296
297 self.clear_action = QtGui.QAction("Clear",
298 self,
299 statusTip="Clear the console",
300 triggered=self.clear_magic)
301 self.addAction(self.clear_action)
302
303 self.who_action = QtGui.QAction("Who",
304 self,
305 statusTip="List interactive variable",
306 triggered=self.who_magic)
307 self.addAction(self.who_action)
308
309 self.whos_action = QtGui.QAction("Whos",
310 self,
311 statusTip="List interactive variable with detail",
312 triggered=self.whos_magic)
313 self.addAction(self.whos_action)
314
315 self.who_ls_action = QtGui.QAction("Who ls",
316 self,
317 statusTip="Return a list of interactive variable",
318 triggered=self.who_ls_magic)
319 self.addAction(self.who_ls_action)
320
255 321
256 322 def eventFilter(self, obj, event):
257 323 """ Reimplemented to ensure a console-like behavior in the underlying
258 324 text widgets.
259 325 """
260 326 etype = event.type()
261 327 if etype == QtCore.QEvent.KeyPress:
262 328
263 329 # Re-map keys for all filtered widgets.
264 330 key = event.key()
265 331 if self._control_key_down(event.modifiers()) and \
266 332 key in self._ctrl_down_remap:
267 333 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
268 334 self._ctrl_down_remap[key],
269 335 QtCore.Qt.NoModifier)
270 336 QtGui.qApp.sendEvent(obj, new_event)
271 337 return True
272 338
273 339 elif obj == self._control:
274 340 return self._event_filter_console_keypress(event)
275 341
276 342 elif obj == self._page_control:
277 343 return self._event_filter_page_keypress(event)
278 344
279 345 # Make middle-click paste safe.
280 346 elif etype == QtCore.QEvent.MouseButtonRelease and \
281 347 event.button() == QtCore.Qt.MidButton and \
282 348 obj == self._control.viewport():
283 349 cursor = self._control.cursorForPosition(event.pos())
284 350 self._control.setTextCursor(cursor)
285 351 self.paste(QtGui.QClipboard.Selection)
286 352 return True
287 353
288 354 # Manually adjust the scrollbars *after* a resize event is dispatched.
289 355 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
290 356 self._filter_resize = True
291 357 QtGui.qApp.sendEvent(obj, event)
292 358 self._adjust_scrollbars()
293 359 self._filter_resize = False
294 360 return True
295 361
296 362 # Override shortcuts for all filtered widgets.
297 363 elif etype == QtCore.QEvent.ShortcutOverride and \
298 364 self.override_shortcuts and \
299 365 self._control_key_down(event.modifiers()) and \
300 366 event.key() in self._shortcuts:
301 367 event.accept()
302 368
303 369 # Ensure that drags are safe. The problem is that the drag starting
304 370 # logic, which determines whether the drag is a Copy or Move, is locked
305 371 # down in QTextControl. If the widget is editable, which it must be if
306 372 # we're not executing, the drag will be a Move. The following hack
307 373 # prevents QTextControl from deleting the text by clearing the selection
308 374 # when a drag leave event originating from this widget is dispatched.
309 375 # The fact that we have to clear the user's selection is unfortunate,
310 376 # but the alternative--trying to prevent Qt from using its hardwired
311 377 # drag logic and writing our own--is worse.
312 378 elif etype == QtCore.QEvent.DragEnter and \
313 379 obj == self._control.viewport() and \
314 380 event.source() == self._control.viewport():
315 381 self._filter_drag = True
316 382 elif etype == QtCore.QEvent.DragLeave and \
317 383 obj == self._control.viewport() and \
318 384 self._filter_drag:
319 385 cursor = self._control.textCursor()
320 386 cursor.clearSelection()
321 387 self._control.setTextCursor(cursor)
322 388 self._filter_drag = False
323 389
324 390 # Ensure that drops are safe.
325 391 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
326 392 cursor = self._control.cursorForPosition(event.pos())
327 393 if self._in_buffer(cursor.position()):
328 394 text = event.mimeData().text()
329 395 self._insert_plain_text_into_buffer(cursor, text)
330 396
331 397 # Qt is expecting to get something here--drag and drop occurs in its
332 398 # own event loop. Send a DragLeave event to end it.
333 399 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
334 400 return True
335 401
336 402 return super(ConsoleWidget, self).eventFilter(obj, event)
337 403
338 404 #---------------------------------------------------------------------------
339 405 # 'QWidget' interface
340 406 #---------------------------------------------------------------------------
341 407
342 408 def sizeHint(self):
343 409 """ Reimplemented to suggest a size that is 80 characters wide and
344 410 25 lines high.
345 411 """
346 412 font_metrics = QtGui.QFontMetrics(self.font)
347 413 margin = (self._control.frameWidth() +
348 414 self._control.document().documentMargin()) * 2
349 415 style = self.style()
350 416 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
351 417
352 418 # Note 1: Despite my best efforts to take the various margins into
353 419 # account, the width is still coming out a bit too small, so we include
354 420 # a fudge factor of one character here.
355 421 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
356 422 # to a Qt bug on certain Mac OS systems where it returns 0.
357 423 width = font_metrics.width(' ') * 81 + margin
358 424 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
359 425 if self.paging == 'hsplit':
360 426 width = width * 2 + splitwidth
361 427
362 428 height = font_metrics.height() * 25 + margin
363 429 if self.paging == 'vsplit':
364 430 height = height * 2 + splitwidth
365 431
366 432 return QtCore.QSize(width, height)
367 433
368 434 #---------------------------------------------------------------------------
369 435 # 'ConsoleWidget' public interface
370 436 #---------------------------------------------------------------------------
371 437
372 438 def can_copy(self):
373 439 """ Returns whether text can be copied to the clipboard.
374 440 """
375 441 return self._control.textCursor().hasSelection()
376 442
377 443 def can_cut(self):
378 444 """ Returns whether text can be cut to the clipboard.
379 445 """
380 446 cursor = self._control.textCursor()
381 447 return (cursor.hasSelection() and
382 448 self._in_buffer(cursor.anchor()) and
383 449 self._in_buffer(cursor.position()))
384 450
385 451 def can_paste(self):
386 452 """ Returns whether text can be pasted from the clipboard.
387 453 """
388 454 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
389 455 return bool(QtGui.QApplication.clipboard().text())
390 456 return False
391 457
392 458 def clear(self, keep_input=True):
393 459 """ Clear the console.
394 460
395 461 Parameters:
396 462 -----------
397 463 keep_input : bool, optional (default True)
398 464 If set, restores the old input buffer if a new prompt is written.
399 465 """
400 466 if self._executing:
401 467 self._control.clear()
402 468 else:
403 469 if keep_input:
404 470 input_buffer = self.input_buffer
405 471 self._control.clear()
406 472 self._show_prompt()
407 473 if keep_input:
408 474 self.input_buffer = input_buffer
409 475
410 476 def copy(self):
411 477 """ Copy the currently selected text to the clipboard.
412 478 """
413 479 self._control.copy()
414 480
415 481 def cut(self):
416 482 """ Copy the currently selected text to the clipboard and delete it
417 483 if it's inside the input buffer.
418 484 """
419 485 self.copy()
420 486 if self.can_cut():
421 487 self._control.textCursor().removeSelectedText()
422 488
423 489 def execute(self, source=None, hidden=False, interactive=False):
424 490 """ Executes source or the input buffer, possibly prompting for more
425 491 input.
426 492
427 493 Parameters:
428 494 -----------
429 495 source : str, optional
430 496
431 497 The source to execute. If not specified, the input buffer will be
432 498 used. If specified and 'hidden' is False, the input buffer will be
433 499 replaced with the source before execution.
434 500
435 501 hidden : bool, optional (default False)
436 502
437 503 If set, no output will be shown and the prompt will not be modified.
438 504 In other words, it will be completely invisible to the user that
439 505 an execution has occurred.
440 506
441 507 interactive : bool, optional (default False)
442 508
443 509 Whether the console is to treat the source as having been manually
444 510 entered by the user. The effect of this parameter depends on the
445 511 subclass implementation.
446 512
447 513 Raises:
448 514 -------
449 515 RuntimeError
450 516 If incomplete input is given and 'hidden' is True. In this case,
451 517 it is not possible to prompt for more input.
452 518
453 519 Returns:
454 520 --------
455 521 A boolean indicating whether the source was executed.
456 522 """
457 523 # WARNING: The order in which things happen here is very particular, in
458 524 # large part because our syntax highlighting is fragile. If you change
459 525 # something, test carefully!
460 526
461 527 # Decide what to execute.
462 528 if source is None:
463 529 source = self.input_buffer
464 530 if not hidden:
465 531 # A newline is appended later, but it should be considered part
466 532 # of the input buffer.
467 533 source += '\n'
468 534 elif not hidden:
469 535 self.input_buffer = source
470 536
471 537 # Execute the source or show a continuation prompt if it is incomplete.
472 538 complete = self._is_complete(source, interactive)
473 539 if hidden:
474 540 if complete:
475 541 self._execute(source, hidden)
476 542 else:
477 543 error = 'Incomplete noninteractive input: "%s"'
478 544 raise RuntimeError(error % source)
479 545 else:
480 546 if complete:
481 547 self._append_plain_text('\n')
482 548 self._input_buffer_executing = self.input_buffer
483 549 self._executing = True
484 550 self._prompt_finished()
485 551
486 552 # The maximum block count is only in effect during execution.
487 553 # This ensures that _prompt_pos does not become invalid due to
488 554 # text truncation.
489 555 self._control.document().setMaximumBlockCount(self.buffer_size)
490 556
491 557 # Setting a positive maximum block count will automatically
492 558 # disable the undo/redo history, but just to be safe:
493 559 self._control.setUndoRedoEnabled(False)
494 560
495 561 # Perform actual execution.
496 562 self._execute(source, hidden)
497 563
498 564 else:
499 565 # Do this inside an edit block so continuation prompts are
500 566 # removed seamlessly via undo/redo.
501 567 cursor = self._get_end_cursor()
502 568 cursor.beginEditBlock()
503 569 cursor.insertText('\n')
504 570 self._insert_continuation_prompt(cursor)
505 571 cursor.endEditBlock()
506 572
507 573 # Do not do this inside the edit block. It works as expected
508 574 # when using a QPlainTextEdit control, but does not have an
509 575 # effect when using a QTextEdit. I believe this is a Qt bug.
510 576 self._control.moveCursor(QtGui.QTextCursor.End)
511 577
512 578 return complete
513 579
514 580 def export_html(self):
515 581 """ Shows a dialog to export HTML/XML in various formats.
516 582 """
517 583 self._html_exporter.export()
518 584
519 585 def _get_input_buffer(self, force=False):
520 586 """ The text that the user has entered entered at the current prompt.
521 587
522 588 If the console is currently executing, the text that is executing will
523 589 always be returned.
524 590 """
525 591 # If we're executing, the input buffer may not even exist anymore due to
526 592 # the limit imposed by 'buffer_size'. Therefore, we store it.
527 593 if self._executing and not force:
528 594 return self._input_buffer_executing
529 595
530 596 cursor = self._get_end_cursor()
531 597 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
532 598 input_buffer = cursor.selection().toPlainText()
533 599
534 600 # Strip out continuation prompts.
535 601 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
536 602
537 603 def _set_input_buffer(self, string):
538 604 """ Sets the text in the input buffer.
539 605
540 606 If the console is currently executing, this call has no *immediate*
541 607 effect. When the execution is finished, the input buffer will be updated
542 608 appropriately.
543 609 """
544 610 # If we're executing, store the text for later.
545 611 if self._executing:
546 612 self._input_buffer_pending = string
547 613 return
548 614
549 615 # Remove old text.
550 616 cursor = self._get_end_cursor()
551 617 cursor.beginEditBlock()
552 618 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
553 619 cursor.removeSelectedText()
554 620
555 621 # Insert new text with continuation prompts.
556 622 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
557 623 cursor.endEditBlock()
558 624 self._control.moveCursor(QtGui.QTextCursor.End)
559 625
560 626 input_buffer = property(_get_input_buffer, _set_input_buffer)
561 627
562 628 def _get_font(self):
563 629 """ The base font being used by the ConsoleWidget.
564 630 """
565 631 return self._control.document().defaultFont()
566 632
567 633 def _set_font(self, font):
568 634 """ Sets the base font for the ConsoleWidget to the specified QFont.
569 635 """
570 636 font_metrics = QtGui.QFontMetrics(font)
571 637 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
572 638
573 639 self._completion_widget.setFont(font)
574 640 self._control.document().setDefaultFont(font)
575 641 if self._page_control:
576 642 self._page_control.document().setDefaultFont(font)
577 643
578 644 self.font_changed.emit(font)
579 645
580 646 font = property(_get_font, _set_font)
581 647
582 def paste(self, mode=QtGui.QClipboard.Clipboard):
583 """ Paste the contents of the clipboard into the input region.
648 def paste(self, mode=QtGui.QClipboard.Clipboard,text=None):
649 """ Paste the contents of the clipboard, or given text,
650 into the input region.
584 651
585 652 Parameters:
586 653 -----------
587 654 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
655 text : string, optional, overide mode if given.
588 656
589 657 Controls which part of the system clipboard is used. This can be
590 658 used to access the selection clipboard in X11 and the Find buffer
591 659 in Mac OS. By default, the regular clipboard is used.
592 660 """
593 661 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
594 662 # Make sure the paste is safe.
595 663 self._keep_cursor_in_buffer()
596 664 cursor = self._control.textCursor()
597 665
598 666 # Remove any trailing newline, which confuses the GUI and forces the
599 667 # user to backspace.
600 text = QtGui.QApplication.clipboard().text(mode).rstrip()
668 if not text:
669 text = QtGui.QApplication.clipboard().text(mode).rstrip()
601 670 self._insert_plain_text_into_buffer(cursor, dedent(text))
602 671
672 def pasteMagic(self,text):
673 self._keyboard_quit()
674 self.paste(text=text)
675 self.execute()
676
677 def who_magic(self):
678 self.pasteMagic("%who")
679
680 def whos_magic(self):
681 self.pasteMagic("%whos")
682
683 def who_ls_magic(self):
684 self.pasteMagic("%who_ls")
685
686 def clear_magic(self):
687 self.pasteMagic("%clear")
688
689 def reset_magic(self):
690 self.pasteMagic("%reset")
691
603 692 def print_(self, printer = None):
604 693 """ Print the contents of the ConsoleWidget to the specified QPrinter.
605 694 """
606 695 if (not printer):
607 696 printer = QtGui.QPrinter()
608 697 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
609 698 return
610 699 self._control.print_(printer)
611 700
612 701 def prompt_to_top(self):
613 702 """ Moves the prompt to the top of the viewport.
614 703 """
615 704 if not self._executing:
616 705 prompt_cursor = self._get_prompt_cursor()
617 706 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
618 707 self._set_cursor(prompt_cursor)
619 708 self._set_top_cursor(prompt_cursor)
620 709
621 710 def redo(self):
622 711 """ Redo the last operation. If there is no operation to redo, nothing
623 712 happens.
624 713 """
625 714 self._control.redo()
626 715
627 716 def reset_font(self):
628 717 """ Sets the font to the default fixed-width font for this platform.
629 718 """
630 719 if sys.platform == 'win32':
631 720 # Consolas ships with Vista/Win7, fallback to Courier if needed
632 721 fallback = 'Courier'
633 722 elif sys.platform == 'darwin':
634 723 # OSX always has Monaco
635 724 fallback = 'Monaco'
636 725 else:
637 726 # Monospace should always exist
638 727 fallback = 'Monospace'
639 728 font = get_font(self.font_family, fallback)
640 729 if self.font_size:
641 730 font.setPointSize(self.font_size)
642 731 else:
643 732 font.setPointSize(QtGui.qApp.font().pointSize())
644 733 font.setStyleHint(QtGui.QFont.TypeWriter)
645 734 self._set_font(font)
646 735
647 736 def change_font_size(self, delta):
648 737 """Change the font size by the specified amount (in points).
649 738 """
650 739 font = self.font
651 740 size = max(font.pointSize() + delta, 1) # minimum 1 point
652 741 font.setPointSize(size)
653 742 self._set_font(font)
654 743
744 def _increase_font_size(self):
745 self.change_font_size(1)
746
747 def _decrease_font_size(self):
748 self.change_font_size(-1)
749
655 750 def select_all(self):
656 751 """ Selects all the text in the buffer.
657 752 """
658 753 self._control.selectAll()
659 754
660 755 def _get_tab_width(self):
661 756 """ The width (in terms of space characters) for tab characters.
662 757 """
663 758 return self._tab_width
664 759
665 760 def _set_tab_width(self, tab_width):
666 761 """ Sets the width (in terms of space characters) for tab characters.
667 762 """
668 763 font_metrics = QtGui.QFontMetrics(self.font)
669 764 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
670 765
671 766 self._tab_width = tab_width
672 767
673 768 tab_width = property(_get_tab_width, _set_tab_width)
674 769
675 770 def undo(self):
676 771 """ Undo the last operation. If there is no operation to undo, nothing
677 772 happens.
678 773 """
679 774 self._control.undo()
680 775
681 776 #---------------------------------------------------------------------------
682 777 # 'ConsoleWidget' abstract interface
683 778 #---------------------------------------------------------------------------
684 779
685 780 def _is_complete(self, source, interactive):
686 781 """ Returns whether 'source' can be executed. When triggered by an
687 782 Enter/Return key press, 'interactive' is True; otherwise, it is
688 783 False.
689 784 """
690 785 raise NotImplementedError
691 786
692 787 def _execute(self, source, hidden):
693 788 """ Execute 'source'. If 'hidden', do not show any output.
694 789 """
695 790 raise NotImplementedError
696 791
697 792 def _prompt_started_hook(self):
698 793 """ Called immediately after a new prompt is displayed.
699 794 """
700 795 pass
701 796
702 797 def _prompt_finished_hook(self):
703 798 """ Called immediately after a prompt is finished, i.e. when some input
704 799 will be processed and a new prompt displayed.
705 800 """
706 801 pass
707 802
708 803 def _up_pressed(self, shift_modifier):
709 804 """ Called when the up key is pressed. Returns whether to continue
710 805 processing the event.
711 806 """
712 807 return True
713 808
714 809 def _down_pressed(self, shift_modifier):
715 810 """ Called when the down key is pressed. Returns whether to continue
716 811 processing the event.
717 812 """
718 813 return True
719 814
720 815 def _tab_pressed(self):
721 816 """ Called when the tab key is pressed. Returns whether to continue
722 817 processing the event.
723 818 """
724 819 return False
725 820
726 821 #--------------------------------------------------------------------------
727 822 # 'ConsoleWidget' protected interface
728 823 #--------------------------------------------------------------------------
729 824
730 825 def _append_custom(self, insert, input, before_prompt=False):
731 826 """ A low-level method for appending content to the end of the buffer.
732 827
733 828 If 'before_prompt' is enabled, the content will be inserted before the
734 829 current prompt, if there is one.
735 830 """
736 831 # Determine where to insert the content.
737 832 cursor = self._control.textCursor()
738 833 if before_prompt and not self._executing:
739 834 cursor.setPosition(self._append_before_prompt_pos)
740 835 else:
741 836 cursor.movePosition(QtGui.QTextCursor.End)
742 837 start_pos = cursor.position()
743 838
744 839 # Perform the insertion.
745 840 result = insert(cursor, input)
746 841
747 842 # Adjust the prompt position if we have inserted before it. This is safe
748 843 # because buffer truncation is disabled when not executing.
749 844 if before_prompt and not self._executing:
750 845 diff = cursor.position() - start_pos
751 846 self._append_before_prompt_pos += diff
752 847 self._prompt_pos += diff
753 848
754 849 return result
755 850
756 851 def _append_html(self, html, before_prompt=False):
757 852 """ Appends HTML at the end of the console buffer.
758 853 """
759 854 self._append_custom(self._insert_html, html, before_prompt)
760 855
761 856 def _append_html_fetching_plain_text(self, html, before_prompt=False):
762 857 """ Appends HTML, then returns the plain text version of it.
763 858 """
764 859 return self._append_custom(self._insert_html_fetching_plain_text,
765 860 html, before_prompt)
766 861
767 862 def _append_plain_text(self, text, before_prompt=False):
768 863 """ Appends plain text, processing ANSI codes if enabled.
769 864 """
770 865 self._append_custom(self._insert_plain_text, text, before_prompt)
771 866
772 867 def _cancel_text_completion(self):
773 868 """ If text completion is progress, cancel it.
774 869 """
775 870 if self._text_completing_pos:
776 871 self._clear_temporary_buffer()
777 872 self._text_completing_pos = 0
778 873
779 874 def _clear_temporary_buffer(self):
780 875 """ Clears the "temporary text" buffer, i.e. all the text following
781 876 the prompt region.
782 877 """
783 878 # Select and remove all text below the input buffer.
784 879 cursor = self._get_prompt_cursor()
785 880 prompt = self._continuation_prompt.lstrip()
786 881 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
787 882 temp_cursor = QtGui.QTextCursor(cursor)
788 883 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
789 884 text = temp_cursor.selection().toPlainText().lstrip()
790 885 if not text.startswith(prompt):
791 886 break
792 887 else:
793 888 # We've reached the end of the input buffer and no text follows.
794 889 return
795 890 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
796 891 cursor.movePosition(QtGui.QTextCursor.End,
797 892 QtGui.QTextCursor.KeepAnchor)
798 893 cursor.removeSelectedText()
799 894
800 895 # After doing this, we have no choice but to clear the undo/redo
801 896 # history. Otherwise, the text is not "temporary" at all, because it
802 897 # can be recalled with undo/redo. Unfortunately, Qt does not expose
803 898 # fine-grained control to the undo/redo system.
804 899 if self._control.isUndoRedoEnabled():
805 900 self._control.setUndoRedoEnabled(False)
806 901 self._control.setUndoRedoEnabled(True)
807 902
808 903 def _complete_with_items(self, cursor, items):
809 904 """ Performs completion with 'items' at the specified cursor location.
810 905 """
811 906 self._cancel_text_completion()
812 907
813 908 if len(items) == 1:
814 909 cursor.setPosition(self._control.textCursor().position(),
815 910 QtGui.QTextCursor.KeepAnchor)
816 911 cursor.insertText(items[0])
817 912
818 913 elif len(items) > 1:
819 914 current_pos = self._control.textCursor().position()
820 915 prefix = commonprefix(items)
821 916 if prefix:
822 917 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
823 918 cursor.insertText(prefix)
824 919 current_pos = cursor.position()
825 920
826 921 if self.gui_completion:
827 922 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
828 923 self._completion_widget.show_items(cursor, items)
829 924 else:
830 925 cursor.beginEditBlock()
831 926 self._append_plain_text('\n')
832 927 self._page(self._format_as_columns(items))
833 928 cursor.endEditBlock()
834 929
835 930 cursor.setPosition(current_pos)
836 931 self._control.moveCursor(QtGui.QTextCursor.End)
837 932 self._control.setTextCursor(cursor)
838 933 self._text_completing_pos = current_pos
839 934
840 935 def _context_menu_make(self, pos):
841 936 """ Creates a context menu for the given QPoint (in widget coordinates).
842 937 """
843 938 menu = QtGui.QMenu(self)
844 939
845 940 cut_action = menu.addAction('Cut', self.cut)
846 941 cut_action.setEnabled(self.can_cut())
847 942 cut_action.setShortcut(QtGui.QKeySequence.Cut)
848 943
849 944 copy_action = menu.addAction('Copy', self.copy)
850 945 copy_action.setEnabled(self.can_copy())
851 946 copy_action.setShortcut(QtGui.QKeySequence.Copy)
852 947
853 948 paste_action = menu.addAction('Paste', self.paste)
854 949 paste_action.setEnabled(self.can_paste())
855 950 paste_action.setShortcut(QtGui.QKeySequence.Paste)
856 951
857 952 menu.addSeparator()
858 menu.addAction(self._select_all_action)
953 menu.addAction(self.select_all_action)
859 954
860 955 menu.addSeparator()
861 menu.addAction(self._export_action)
862 menu.addAction(self._print_action)
956 menu.addAction(self.export_action)
957 menu.addAction(self.print_action)
863 958
864 959 return menu
865 960
866 961 def _control_key_down(self, modifiers, include_command=False):
867 962 """ Given a KeyboardModifiers flags object, return whether the Control
868 963 key is down.
869 964
870 965 Parameters:
871 966 -----------
872 967 include_command : bool, optional (default True)
873 968 Whether to treat the Command key as a (mutually exclusive) synonym
874 969 for Control when in Mac OS.
875 970 """
876 971 # Note that on Mac OS, ControlModifier corresponds to the Command key
877 972 # while MetaModifier corresponds to the Control key.
878 973 if sys.platform == 'darwin':
879 974 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
880 975 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
881 976 else:
882 977 return bool(modifiers & QtCore.Qt.ControlModifier)
883 978
884 979 def _create_control(self):
885 980 """ Creates and connects the underlying text widget.
886 981 """
887 982 # Create the underlying control.
888 983 if self.kind == 'plain':
889 984 control = QtGui.QPlainTextEdit()
890 985 elif self.kind == 'rich':
891 986 control = QtGui.QTextEdit()
892 987 control.setAcceptRichText(False)
893 988
894 989 # Install event filters. The filter on the viewport is needed for
895 990 # mouse events and drag events.
896 991 control.installEventFilter(self)
897 992 control.viewport().installEventFilter(self)
898 993
899 994 # Connect signals.
900 995 control.cursorPositionChanged.connect(self._cursor_position_changed)
901 996 control.customContextMenuRequested.connect(
902 997 self._custom_context_menu_requested)
903 998 control.copyAvailable.connect(self.copy_available)
904 999 control.redoAvailable.connect(self.redo_available)
905 1000 control.undoAvailable.connect(self.undo_available)
906 1001
907 1002 # Hijack the document size change signal to prevent Qt from adjusting
908 1003 # the viewport's scrollbar. We are relying on an implementation detail
909 1004 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
910 1005 # this functionality we cannot create a nice terminal interface.
911 1006 layout = control.document().documentLayout()
912 1007 layout.documentSizeChanged.disconnect()
913 1008 layout.documentSizeChanged.connect(self._adjust_scrollbars)
914 1009
915 1010 # Configure the control.
916 1011 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
917 1012 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
918 1013 control.setReadOnly(True)
919 1014 control.setUndoRedoEnabled(False)
920 1015 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
921 1016 return control
922 1017
923 1018 def _create_page_control(self):
924 1019 """ Creates and connects the underlying paging widget.
925 1020 """
926 1021 if self.kind == 'plain':
927 1022 control = QtGui.QPlainTextEdit()
928 1023 elif self.kind == 'rich':
929 1024 control = QtGui.QTextEdit()
930 1025 control.installEventFilter(self)
931 1026 control.setReadOnly(True)
932 1027 control.setUndoRedoEnabled(False)
933 1028 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
934 1029 return control
935 1030
936 1031 def _event_filter_console_keypress(self, event):
937 1032 """ Filter key events for the underlying text widget to create a
938 1033 console-like interface.
939 1034 """
940 1035 intercepted = False
941 1036 cursor = self._control.textCursor()
942 1037 position = cursor.position()
943 1038 key = event.key()
944 1039 ctrl_down = self._control_key_down(event.modifiers())
945 1040 alt_down = event.modifiers() & QtCore.Qt.AltModifier
946 1041 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
947 1042
948 1043 #------ Special sequences ----------------------------------------------
949 1044
950 1045 if event.matches(QtGui.QKeySequence.Copy):
951 1046 self.copy()
952 1047 intercepted = True
953 1048
954 1049 elif event.matches(QtGui.QKeySequence.Cut):
955 1050 self.cut()
956 1051 intercepted = True
957 1052
958 1053 elif event.matches(QtGui.QKeySequence.Paste):
959 1054 self.paste()
960 1055 intercepted = True
961 1056
962 1057 #------ Special modifier logic -----------------------------------------
963 1058
964 1059 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
965 1060 intercepted = True
966 1061
967 1062 # Special handling when tab completing in text mode.
968 1063 self._cancel_text_completion()
969 1064
970 1065 if self._in_buffer(position):
971 1066 # Special handling when a reading a line of raw input.
972 1067 if self._reading:
973 1068 self._append_plain_text('\n')
974 1069 self._reading = False
975 1070 if self._reading_callback:
976 1071 self._reading_callback()
977 1072
978 1073 # If the input buffer is a single line or there is only
979 1074 # whitespace after the cursor, execute. Otherwise, split the
980 1075 # line with a continuation prompt.
981 1076 elif not self._executing:
982 1077 cursor.movePosition(QtGui.QTextCursor.End,
983 1078 QtGui.QTextCursor.KeepAnchor)
984 1079 at_end = len(cursor.selectedText().strip()) == 0
985 1080 single_line = (self._get_end_cursor().blockNumber() ==
986 1081 self._get_prompt_cursor().blockNumber())
987 1082 if (at_end or shift_down or single_line) and not ctrl_down:
988 1083 self.execute(interactive = not shift_down)
989 1084 else:
990 1085 # Do this inside an edit block for clean undo/redo.
991 1086 cursor.beginEditBlock()
992 1087 cursor.setPosition(position)
993 1088 cursor.insertText('\n')
994 1089 self._insert_continuation_prompt(cursor)
995 1090 cursor.endEditBlock()
996 1091
997 1092 # Ensure that the whole input buffer is visible.
998 1093 # FIXME: This will not be usable if the input buffer is
999 1094 # taller than the console widget.
1000 1095 self._control.moveCursor(QtGui.QTextCursor.End)
1001 1096 self._control.setTextCursor(cursor)
1002 1097
1003 1098 #------ Control/Cmd modifier -------------------------------------------
1004 1099
1005 1100 elif ctrl_down:
1006 1101 if key == QtCore.Qt.Key_G:
1007 1102 self._keyboard_quit()
1008 1103 intercepted = True
1009 1104
1010 1105 elif key == QtCore.Qt.Key_K:
1011 1106 if self._in_buffer(position):
1012 1107 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1013 1108 QtGui.QTextCursor.KeepAnchor)
1014 1109 if not cursor.hasSelection():
1015 1110 # Line deletion (remove continuation prompt)
1016 1111 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1017 1112 QtGui.QTextCursor.KeepAnchor)
1018 1113 cursor.movePosition(QtGui.QTextCursor.Right,
1019 1114 QtGui.QTextCursor.KeepAnchor,
1020 1115 len(self._continuation_prompt))
1021 1116 self._kill_ring.kill_cursor(cursor)
1022 1117 intercepted = True
1023 1118
1024 1119 elif key == QtCore.Qt.Key_L:
1025 1120 self.prompt_to_top()
1026 1121 intercepted = True
1027 1122
1028 1123 elif key == QtCore.Qt.Key_O:
1029 1124 if self._page_control and self._page_control.isVisible():
1030 1125 self._page_control.setFocus()
1031 1126 intercepted = True
1032 1127
1033 1128 elif key == QtCore.Qt.Key_U:
1034 1129 if self._in_buffer(position):
1035 1130 start_line = cursor.blockNumber()
1036 1131 if start_line == self._get_prompt_cursor().blockNumber():
1037 1132 offset = len(self._prompt)
1038 1133 else:
1039 1134 offset = len(self._continuation_prompt)
1040 1135 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1041 1136 QtGui.QTextCursor.KeepAnchor)
1042 1137 cursor.movePosition(QtGui.QTextCursor.Right,
1043 1138 QtGui.QTextCursor.KeepAnchor, offset)
1044 1139 self._kill_ring.kill_cursor(cursor)
1045 1140 intercepted = True
1046 1141
1047 1142 elif key == QtCore.Qt.Key_Y:
1048 1143 self._keep_cursor_in_buffer()
1049 1144 self._kill_ring.yank()
1050 1145 intercepted = True
1051 1146
1052 1147 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1053 1148 if key == QtCore.Qt.Key_Backspace:
1054 1149 cursor = self._get_word_start_cursor(position)
1055 1150 else: # key == QtCore.Qt.Key_Delete
1056 1151 cursor = self._get_word_end_cursor(position)
1057 1152 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1058 1153 self._kill_ring.kill_cursor(cursor)
1059 1154 intercepted = True
1060 1155
1061 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1062 self.change_font_size(1)
1063 intercepted = True
1064
1065 elif key == QtCore.Qt.Key_Minus:
1066 self.change_font_size(-1)
1067 intercepted = True
1068
1069 elif key == QtCore.Qt.Key_0:
1070 self.reset_font()
1071 intercepted = True
1072
1073 1156 #------ Alt modifier ---------------------------------------------------
1074 1157
1075 1158 elif alt_down:
1076 1159 if key == QtCore.Qt.Key_B:
1077 1160 self._set_cursor(self._get_word_start_cursor(position))
1078 1161 intercepted = True
1079 1162
1080 1163 elif key == QtCore.Qt.Key_F:
1081 1164 self._set_cursor(self._get_word_end_cursor(position))
1082 1165 intercepted = True
1083 1166
1084 1167 elif key == QtCore.Qt.Key_Y:
1085 1168 self._kill_ring.rotate()
1086 1169 intercepted = True
1087 1170
1088 1171 elif key == QtCore.Qt.Key_Backspace:
1089 1172 cursor = self._get_word_start_cursor(position)
1090 1173 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1091 1174 self._kill_ring.kill_cursor(cursor)
1092 1175 intercepted = True
1093 1176
1094 1177 elif key == QtCore.Qt.Key_D:
1095 1178 cursor = self._get_word_end_cursor(position)
1096 1179 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1097 1180 self._kill_ring.kill_cursor(cursor)
1098 1181 intercepted = True
1099 1182
1100 1183 elif key == QtCore.Qt.Key_Delete:
1101 1184 intercepted = True
1102 1185
1103 1186 elif key == QtCore.Qt.Key_Greater:
1104 1187 self._control.moveCursor(QtGui.QTextCursor.End)
1105 1188 intercepted = True
1106 1189
1107 1190 elif key == QtCore.Qt.Key_Less:
1108 1191 self._control.setTextCursor(self._get_prompt_cursor())
1109 1192 intercepted = True
1110 1193
1111 1194 #------ No modifiers ---------------------------------------------------
1112 1195
1113 1196 else:
1114 1197 if shift_down:
1115 1198 anchormode = QtGui.QTextCursor.KeepAnchor
1116 1199 else:
1117 1200 anchormode = QtGui.QTextCursor.MoveAnchor
1118 1201
1119 1202 if key == QtCore.Qt.Key_Escape:
1120 1203 self._keyboard_quit()
1121 1204 intercepted = True
1122 1205
1123 1206 elif key == QtCore.Qt.Key_Up:
1124 1207 if self._reading or not self._up_pressed(shift_down):
1125 1208 intercepted = True
1126 1209 else:
1127 1210 prompt_line = self._get_prompt_cursor().blockNumber()
1128 1211 intercepted = cursor.blockNumber() <= prompt_line
1129 1212
1130 1213 elif key == QtCore.Qt.Key_Down:
1131 1214 if self._reading or not self._down_pressed(shift_down):
1132 1215 intercepted = True
1133 1216 else:
1134 1217 end_line = self._get_end_cursor().blockNumber()
1135 1218 intercepted = cursor.blockNumber() == end_line
1136 1219
1137 1220 elif key == QtCore.Qt.Key_Tab:
1138 1221 if not self._reading:
1139 1222 intercepted = not self._tab_pressed()
1140 1223
1141 1224 elif key == QtCore.Qt.Key_Left:
1142 1225
1143 1226 # Move to the previous line
1144 1227 line, col = cursor.blockNumber(), cursor.columnNumber()
1145 1228 if line > self._get_prompt_cursor().blockNumber() and \
1146 1229 col == len(self._continuation_prompt):
1147 1230 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1148 1231 mode=anchormode)
1149 1232 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1150 1233 mode=anchormode)
1151 1234 intercepted = True
1152 1235
1153 1236 # Regular left movement
1154 1237 else:
1155 1238 intercepted = not self._in_buffer(position - 1)
1156 1239
1157 1240 elif key == QtCore.Qt.Key_Right:
1158 1241 original_block_number = cursor.blockNumber()
1159 1242 cursor.movePosition(QtGui.QTextCursor.Right,
1160 1243 mode=anchormode)
1161 1244 if cursor.blockNumber() != original_block_number:
1162 1245 cursor.movePosition(QtGui.QTextCursor.Right,
1163 1246 n=len(self._continuation_prompt),
1164 1247 mode=anchormode)
1165 1248 self._set_cursor(cursor)
1166 1249 intercepted = True
1167 1250
1168 1251 elif key == QtCore.Qt.Key_Home:
1169 1252 start_line = cursor.blockNumber()
1170 1253 if start_line == self._get_prompt_cursor().blockNumber():
1171 1254 start_pos = self._prompt_pos
1172 1255 else:
1173 1256 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1174 1257 QtGui.QTextCursor.KeepAnchor)
1175 1258 start_pos = cursor.position()
1176 1259 start_pos += len(self._continuation_prompt)
1177 1260 cursor.setPosition(position)
1178 1261 if shift_down and self._in_buffer(position):
1179 1262 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1180 1263 else:
1181 1264 cursor.setPosition(start_pos)
1182 1265 self._set_cursor(cursor)
1183 1266 intercepted = True
1184 1267
1185 1268 elif key == QtCore.Qt.Key_Backspace:
1186 1269
1187 1270 # Line deletion (remove continuation prompt)
1188 1271 line, col = cursor.blockNumber(), cursor.columnNumber()
1189 1272 if not self._reading and \
1190 1273 col == len(self._continuation_prompt) and \
1191 1274 line > self._get_prompt_cursor().blockNumber():
1192 1275 cursor.beginEditBlock()
1193 1276 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1194 1277 QtGui.QTextCursor.KeepAnchor)
1195 1278 cursor.removeSelectedText()
1196 1279 cursor.deletePreviousChar()
1197 1280 cursor.endEditBlock()
1198 1281 intercepted = True
1199 1282
1200 1283 # Regular backwards deletion
1201 1284 else:
1202 1285 anchor = cursor.anchor()
1203 1286 if anchor == position:
1204 1287 intercepted = not self._in_buffer(position - 1)
1205 1288 else:
1206 1289 intercepted = not self._in_buffer(min(anchor, position))
1207 1290
1208 1291 elif key == QtCore.Qt.Key_Delete:
1209 1292
1210 1293 # Line deletion (remove continuation prompt)
1211 1294 if not self._reading and self._in_buffer(position) and \
1212 1295 cursor.atBlockEnd() and not cursor.hasSelection():
1213 1296 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1214 1297 QtGui.QTextCursor.KeepAnchor)
1215 1298 cursor.movePosition(QtGui.QTextCursor.Right,
1216 1299 QtGui.QTextCursor.KeepAnchor,
1217 1300 len(self._continuation_prompt))
1218 1301 cursor.removeSelectedText()
1219 1302 intercepted = True
1220 1303
1221 1304 # Regular forwards deletion:
1222 1305 else:
1223 1306 anchor = cursor.anchor()
1224 1307 intercepted = (not self._in_buffer(anchor) or
1225 1308 not self._in_buffer(position))
1226 1309
1227 1310 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1228 1311 # using the keyboard in any part of the buffer. Also, permit scrolling
1229 1312 # with Page Up/Down keys. Finally, if we're executing, don't move the
1230 1313 # cursor (if even this made sense, we can't guarantee that the prompt
1231 1314 # position is still valid due to text truncation).
1232 1315 if not (self._control_key_down(event.modifiers(), include_command=True)
1233 1316 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1234 1317 or (self._executing and not self._reading)):
1235 1318 self._keep_cursor_in_buffer()
1236 1319
1237 1320 return intercepted
1238 1321
1239 1322 def _event_filter_page_keypress(self, event):
1240 1323 """ Filter key events for the paging widget to create console-like
1241 1324 interface.
1242 1325 """
1243 1326 key = event.key()
1244 1327 ctrl_down = self._control_key_down(event.modifiers())
1245 1328 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1246 1329
1247 1330 if ctrl_down:
1248 1331 if key == QtCore.Qt.Key_O:
1249 1332 self._control.setFocus()
1250 1333 intercept = True
1251 1334
1252 1335 elif alt_down:
1253 1336 if key == QtCore.Qt.Key_Greater:
1254 1337 self._page_control.moveCursor(QtGui.QTextCursor.End)
1255 1338 intercepted = True
1256 1339
1257 1340 elif key == QtCore.Qt.Key_Less:
1258 1341 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1259 1342 intercepted = True
1260 1343
1261 1344 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1262 1345 if self._splitter:
1263 1346 self._page_control.hide()
1264 1347 self._control.setFocus()
1265 1348 else:
1266 1349 self.layout().setCurrentWidget(self._control)
1267 1350 return True
1268 1351
1269 1352 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1270 1353 QtCore.Qt.Key_Tab):
1271 1354 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1272 1355 QtCore.Qt.Key_PageDown,
1273 1356 QtCore.Qt.NoModifier)
1274 1357 QtGui.qApp.sendEvent(self._page_control, new_event)
1275 1358 return True
1276 1359
1277 1360 elif key == QtCore.Qt.Key_Backspace:
1278 1361 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1279 1362 QtCore.Qt.Key_PageUp,
1280 1363 QtCore.Qt.NoModifier)
1281 1364 QtGui.qApp.sendEvent(self._page_control, new_event)
1282 1365 return True
1283 1366
1284 1367 return False
1285 1368
1286 1369 def _format_as_columns(self, items, separator=' '):
1287 1370 """ Transform a list of strings into a single string with columns.
1288 1371
1289 1372 Parameters
1290 1373 ----------
1291 1374 items : sequence of strings
1292 1375 The strings to process.
1293 1376
1294 1377 separator : str, optional [default is two spaces]
1295 1378 The string that separates columns.
1296 1379
1297 1380 Returns
1298 1381 -------
1299 1382 The formatted string.
1300 1383 """
1301 1384 # Calculate the number of characters available.
1302 1385 width = self._control.viewport().width()
1303 1386 char_width = QtGui.QFontMetrics(self.font).width(' ')
1304 1387 displaywidth = max(10, (width / char_width) - 1)
1305 1388
1306 1389 return columnize(items, separator, displaywidth)
1307 1390
1308 1391 def _get_block_plain_text(self, block):
1309 1392 """ Given a QTextBlock, return its unformatted text.
1310 1393 """
1311 1394 cursor = QtGui.QTextCursor(block)
1312 1395 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1313 1396 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1314 1397 QtGui.QTextCursor.KeepAnchor)
1315 1398 return cursor.selection().toPlainText()
1316 1399
1317 1400 def _get_cursor(self):
1318 1401 """ Convenience method that returns a cursor for the current position.
1319 1402 """
1320 1403 return self._control.textCursor()
1321 1404
1322 1405 def _get_end_cursor(self):
1323 1406 """ Convenience method that returns a cursor for the last character.
1324 1407 """
1325 1408 cursor = self._control.textCursor()
1326 1409 cursor.movePosition(QtGui.QTextCursor.End)
1327 1410 return cursor
1328 1411
1329 1412 def _get_input_buffer_cursor_column(self):
1330 1413 """ Returns the column of the cursor in the input buffer, excluding the
1331 1414 contribution by the prompt, or -1 if there is no such column.
1332 1415 """
1333 1416 prompt = self._get_input_buffer_cursor_prompt()
1334 1417 if prompt is None:
1335 1418 return -1
1336 1419 else:
1337 1420 cursor = self._control.textCursor()
1338 1421 return cursor.columnNumber() - len(prompt)
1339 1422
1340 1423 def _get_input_buffer_cursor_line(self):
1341 1424 """ Returns the text of the line of the input buffer that contains the
1342 1425 cursor, or None if there is no such line.
1343 1426 """
1344 1427 prompt = self._get_input_buffer_cursor_prompt()
1345 1428 if prompt is None:
1346 1429 return None
1347 1430 else:
1348 1431 cursor = self._control.textCursor()
1349 1432 text = self._get_block_plain_text(cursor.block())
1350 1433 return text[len(prompt):]
1351 1434
1352 1435 def _get_input_buffer_cursor_prompt(self):
1353 1436 """ Returns the (plain text) prompt for line of the input buffer that
1354 1437 contains the cursor, or None if there is no such line.
1355 1438 """
1356 1439 if self._executing:
1357 1440 return None
1358 1441 cursor = self._control.textCursor()
1359 1442 if cursor.position() >= self._prompt_pos:
1360 1443 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1361 1444 return self._prompt
1362 1445 else:
1363 1446 return self._continuation_prompt
1364 1447 else:
1365 1448 return None
1366 1449
1367 1450 def _get_prompt_cursor(self):
1368 1451 """ Convenience method that returns a cursor for the prompt position.
1369 1452 """
1370 1453 cursor = self._control.textCursor()
1371 1454 cursor.setPosition(self._prompt_pos)
1372 1455 return cursor
1373 1456
1374 1457 def _get_selection_cursor(self, start, end):
1375 1458 """ Convenience method that returns a cursor with text selected between
1376 1459 the positions 'start' and 'end'.
1377 1460 """
1378 1461 cursor = self._control.textCursor()
1379 1462 cursor.setPosition(start)
1380 1463 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1381 1464 return cursor
1382 1465
1383 1466 def _get_word_start_cursor(self, position):
1384 1467 """ Find the start of the word to the left the given position. If a
1385 1468 sequence of non-word characters precedes the first word, skip over
1386 1469 them. (This emulates the behavior of bash, emacs, etc.)
1387 1470 """
1388 1471 document = self._control.document()
1389 1472 position -= 1
1390 1473 while position >= self._prompt_pos and \
1391 1474 not is_letter_or_number(document.characterAt(position)):
1392 1475 position -= 1
1393 1476 while position >= self._prompt_pos and \
1394 1477 is_letter_or_number(document.characterAt(position)):
1395 1478 position -= 1
1396 1479 cursor = self._control.textCursor()
1397 1480 cursor.setPosition(position + 1)
1398 1481 return cursor
1399 1482
1400 1483 def _get_word_end_cursor(self, position):
1401 1484 """ Find the end of the word to the right the given position. If a
1402 1485 sequence of non-word characters precedes the first word, skip over
1403 1486 them. (This emulates the behavior of bash, emacs, etc.)
1404 1487 """
1405 1488 document = self._control.document()
1406 1489 end = self._get_end_cursor().position()
1407 1490 while position < end and \
1408 1491 not is_letter_or_number(document.characterAt(position)):
1409 1492 position += 1
1410 1493 while position < end and \
1411 1494 is_letter_or_number(document.characterAt(position)):
1412 1495 position += 1
1413 1496 cursor = self._control.textCursor()
1414 1497 cursor.setPosition(position)
1415 1498 return cursor
1416 1499
1417 1500 def _insert_continuation_prompt(self, cursor):
1418 1501 """ Inserts new continuation prompt using the specified cursor.
1419 1502 """
1420 1503 if self._continuation_prompt_html is None:
1421 1504 self._insert_plain_text(cursor, self._continuation_prompt)
1422 1505 else:
1423 1506 self._continuation_prompt = self._insert_html_fetching_plain_text(
1424 1507 cursor, self._continuation_prompt_html)
1425 1508
1426 1509 def _insert_html(self, cursor, html):
1427 1510 """ Inserts HTML using the specified cursor in such a way that future
1428 1511 formatting is unaffected.
1429 1512 """
1430 1513 cursor.beginEditBlock()
1431 1514 cursor.insertHtml(html)
1432 1515
1433 1516 # After inserting HTML, the text document "remembers" it's in "html
1434 1517 # mode", which means that subsequent calls adding plain text will result
1435 1518 # in unwanted formatting, lost tab characters, etc. The following code
1436 1519 # hacks around this behavior, which I consider to be a bug in Qt, by
1437 1520 # (crudely) resetting the document's style state.
1438 1521 cursor.movePosition(QtGui.QTextCursor.Left,
1439 1522 QtGui.QTextCursor.KeepAnchor)
1440 1523 if cursor.selection().toPlainText() == ' ':
1441 1524 cursor.removeSelectedText()
1442 1525 else:
1443 1526 cursor.movePosition(QtGui.QTextCursor.Right)
1444 1527 cursor.insertText(' ', QtGui.QTextCharFormat())
1445 1528 cursor.endEditBlock()
1446 1529
1447 1530 def _insert_html_fetching_plain_text(self, cursor, html):
1448 1531 """ Inserts HTML using the specified cursor, then returns its plain text
1449 1532 version.
1450 1533 """
1451 1534 cursor.beginEditBlock()
1452 1535 cursor.removeSelectedText()
1453 1536
1454 1537 start = cursor.position()
1455 1538 self._insert_html(cursor, html)
1456 1539 end = cursor.position()
1457 1540 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1458 1541 text = cursor.selection().toPlainText()
1459 1542
1460 1543 cursor.setPosition(end)
1461 1544 cursor.endEditBlock()
1462 1545 return text
1463 1546
1464 1547 def _insert_plain_text(self, cursor, text):
1465 1548 """ Inserts plain text using the specified cursor, processing ANSI codes
1466 1549 if enabled.
1467 1550 """
1468 1551 cursor.beginEditBlock()
1469 1552 if self.ansi_codes:
1470 1553 for substring in self._ansi_processor.split_string(text):
1471 1554 for act in self._ansi_processor.actions:
1472 1555
1473 1556 # Unlike real terminal emulators, we don't distinguish
1474 1557 # between the screen and the scrollback buffer. A screen
1475 1558 # erase request clears everything.
1476 1559 if act.action == 'erase' and act.area == 'screen':
1477 1560 cursor.select(QtGui.QTextCursor.Document)
1478 1561 cursor.removeSelectedText()
1479 1562
1480 1563 # Simulate a form feed by scrolling just past the last line.
1481 1564 elif act.action == 'scroll' and act.unit == 'page':
1482 1565 cursor.insertText('\n')
1483 1566 cursor.endEditBlock()
1484 1567 self._set_top_cursor(cursor)
1485 1568 cursor.joinPreviousEditBlock()
1486 1569 cursor.deletePreviousChar()
1487 1570
1488 1571 format = self._ansi_processor.get_format()
1489 1572 cursor.insertText(substring, format)
1490 1573 else:
1491 1574 cursor.insertText(text)
1492 1575 cursor.endEditBlock()
1493 1576
1494 1577 def _insert_plain_text_into_buffer(self, cursor, text):
1495 1578 """ Inserts text into the input buffer using the specified cursor (which
1496 1579 must be in the input buffer), ensuring that continuation prompts are
1497 1580 inserted as necessary.
1498 1581 """
1499 1582 lines = text.splitlines(True)
1500 1583 if lines:
1501 1584 cursor.beginEditBlock()
1502 1585 cursor.insertText(lines[0])
1503 1586 for line in lines[1:]:
1504 1587 if self._continuation_prompt_html is None:
1505 1588 cursor.insertText(self._continuation_prompt)
1506 1589 else:
1507 1590 self._continuation_prompt = \
1508 1591 self._insert_html_fetching_plain_text(
1509 1592 cursor, self._continuation_prompt_html)
1510 1593 cursor.insertText(line)
1511 1594 cursor.endEditBlock()
1512 1595
1513 1596 def _in_buffer(self, position=None):
1514 1597 """ Returns whether the current cursor (or, if specified, a position) is
1515 1598 inside the editing region.
1516 1599 """
1517 1600 cursor = self._control.textCursor()
1518 1601 if position is None:
1519 1602 position = cursor.position()
1520 1603 else:
1521 1604 cursor.setPosition(position)
1522 1605 line = cursor.blockNumber()
1523 1606 prompt_line = self._get_prompt_cursor().blockNumber()
1524 1607 if line == prompt_line:
1525 1608 return position >= self._prompt_pos
1526 1609 elif line > prompt_line:
1527 1610 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1528 1611 prompt_pos = cursor.position() + len(self._continuation_prompt)
1529 1612 return position >= prompt_pos
1530 1613 return False
1531 1614
1532 1615 def _keep_cursor_in_buffer(self):
1533 1616 """ Ensures that the cursor is inside the editing region. Returns
1534 1617 whether the cursor was moved.
1535 1618 """
1536 1619 moved = not self._in_buffer()
1537 1620 if moved:
1538 1621 cursor = self._control.textCursor()
1539 1622 cursor.movePosition(QtGui.QTextCursor.End)
1540 1623 self._control.setTextCursor(cursor)
1541 1624 return moved
1542 1625
1543 1626 def _keyboard_quit(self):
1544 1627 """ Cancels the current editing task ala Ctrl-G in Emacs.
1545 1628 """
1546 1629 if self._text_completing_pos:
1547 1630 self._cancel_text_completion()
1548 1631 else:
1549 1632 self.input_buffer = ''
1550 1633
1551 1634 def _page(self, text, html=False):
1552 1635 """ Displays text using the pager if it exceeds the height of the
1553 1636 viewport.
1554 1637
1555 1638 Parameters:
1556 1639 -----------
1557 1640 html : bool, optional (default False)
1558 1641 If set, the text will be interpreted as HTML instead of plain text.
1559 1642 """
1560 1643 line_height = QtGui.QFontMetrics(self.font).height()
1561 1644 minlines = self._control.viewport().height() / line_height
1562 1645 if self.paging != 'none' and \
1563 1646 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1564 1647 if self.paging == 'custom':
1565 1648 self.custom_page_requested.emit(text)
1566 1649 else:
1567 1650 self._page_control.clear()
1568 1651 cursor = self._page_control.textCursor()
1569 1652 if html:
1570 1653 self._insert_html(cursor, text)
1571 1654 else:
1572 1655 self._insert_plain_text(cursor, text)
1573 1656 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1574 1657
1575 1658 self._page_control.viewport().resize(self._control.size())
1576 1659 if self._splitter:
1577 1660 self._page_control.show()
1578 1661 self._page_control.setFocus()
1579 1662 else:
1580 1663 self.layout().setCurrentWidget(self._page_control)
1581 1664 elif html:
1582 1665 self._append_plain_html(text)
1583 1666 else:
1584 1667 self._append_plain_text(text)
1585 1668
1586 1669 def _prompt_finished(self):
1587 1670 """ Called immediately after a prompt is finished, i.e. when some input
1588 1671 will be processed and a new prompt displayed.
1589 1672 """
1590 1673 self._control.setReadOnly(True)
1591 1674 self._prompt_finished_hook()
1592 1675
1593 1676 def _prompt_started(self):
1594 1677 """ Called immediately after a new prompt is displayed.
1595 1678 """
1596 1679 # Temporarily disable the maximum block count to permit undo/redo and
1597 1680 # to ensure that the prompt position does not change due to truncation.
1598 1681 self._control.document().setMaximumBlockCount(0)
1599 1682 self._control.setUndoRedoEnabled(True)
1600 1683
1601 1684 # Work around bug in QPlainTextEdit: input method is not re-enabled
1602 1685 # when read-only is disabled.
1603 1686 self._control.setReadOnly(False)
1604 1687 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1605 1688
1606 1689 if not self._reading:
1607 1690 self._executing = False
1608 1691 self._prompt_started_hook()
1609 1692
1610 1693 # If the input buffer has changed while executing, load it.
1611 1694 if self._input_buffer_pending:
1612 1695 self.input_buffer = self._input_buffer_pending
1613 1696 self._input_buffer_pending = ''
1614 1697
1615 1698 self._control.moveCursor(QtGui.QTextCursor.End)
1616 1699
1617 1700 def _readline(self, prompt='', callback=None):
1618 1701 """ Reads one line of input from the user.
1619 1702
1620 1703 Parameters
1621 1704 ----------
1622 1705 prompt : str, optional
1623 1706 The prompt to print before reading the line.
1624 1707
1625 1708 callback : callable, optional
1626 1709 A callback to execute with the read line. If not specified, input is
1627 1710 read *synchronously* and this method does not return until it has
1628 1711 been read.
1629 1712
1630 1713 Returns
1631 1714 -------
1632 1715 If a callback is specified, returns nothing. Otherwise, returns the
1633 1716 input string with the trailing newline stripped.
1634 1717 """
1635 1718 if self._reading:
1636 1719 raise RuntimeError('Cannot read a line. Widget is already reading.')
1637 1720
1638 1721 if not callback and not self.isVisible():
1639 1722 # If the user cannot see the widget, this function cannot return.
1640 1723 raise RuntimeError('Cannot synchronously read a line if the widget '
1641 1724 'is not visible!')
1642 1725
1643 1726 self._reading = True
1644 1727 self._show_prompt(prompt, newline=False)
1645 1728
1646 1729 if callback is None:
1647 1730 self._reading_callback = None
1648 1731 while self._reading:
1649 1732 QtCore.QCoreApplication.processEvents()
1650 1733 return self._get_input_buffer(force=True).rstrip('\n')
1651 1734
1652 1735 else:
1653 1736 self._reading_callback = lambda: \
1654 1737 callback(self._get_input_buffer(force=True).rstrip('\n'))
1655 1738
1656 1739 def _set_continuation_prompt(self, prompt, html=False):
1657 1740 """ Sets the continuation prompt.
1658 1741
1659 1742 Parameters
1660 1743 ----------
1661 1744 prompt : str
1662 1745 The prompt to show when more input is needed.
1663 1746
1664 1747 html : bool, optional (default False)
1665 1748 If set, the prompt will be inserted as formatted HTML. Otherwise,
1666 1749 the prompt will be treated as plain text, though ANSI color codes
1667 1750 will be handled.
1668 1751 """
1669 1752 if html:
1670 1753 self._continuation_prompt_html = prompt
1671 1754 else:
1672 1755 self._continuation_prompt = prompt
1673 1756 self._continuation_prompt_html = None
1674 1757
1675 1758 def _set_cursor(self, cursor):
1676 1759 """ Convenience method to set the current cursor.
1677 1760 """
1678 1761 self._control.setTextCursor(cursor)
1679 1762
1680 1763 def _set_top_cursor(self, cursor):
1681 1764 """ Scrolls the viewport so that the specified cursor is at the top.
1682 1765 """
1683 1766 scrollbar = self._control.verticalScrollBar()
1684 1767 scrollbar.setValue(scrollbar.maximum())
1685 1768 original_cursor = self._control.textCursor()
1686 1769 self._control.setTextCursor(cursor)
1687 1770 self._control.ensureCursorVisible()
1688 1771 self._control.setTextCursor(original_cursor)
1689 1772
1690 1773 def _show_prompt(self, prompt=None, html=False, newline=True):
1691 1774 """ Writes a new prompt at the end of the buffer.
1692 1775
1693 1776 Parameters
1694 1777 ----------
1695 1778 prompt : str, optional
1696 1779 The prompt to show. If not specified, the previous prompt is used.
1697 1780
1698 1781 html : bool, optional (default False)
1699 1782 Only relevant when a prompt is specified. If set, the prompt will
1700 1783 be inserted as formatted HTML. Otherwise, the prompt will be treated
1701 1784 as plain text, though ANSI color codes will be handled.
1702 1785
1703 1786 newline : bool, optional (default True)
1704 1787 If set, a new line will be written before showing the prompt if
1705 1788 there is not already a newline at the end of the buffer.
1706 1789 """
1707 1790 # Save the current end position to support _append*(before_prompt=True).
1708 1791 cursor = self._get_end_cursor()
1709 1792 self._append_before_prompt_pos = cursor.position()
1710 1793
1711 1794 # Insert a preliminary newline, if necessary.
1712 1795 if newline and cursor.position() > 0:
1713 1796 cursor.movePosition(QtGui.QTextCursor.Left,
1714 1797 QtGui.QTextCursor.KeepAnchor)
1715 1798 if cursor.selection().toPlainText() != '\n':
1716 1799 self._append_plain_text('\n')
1717 1800
1718 1801 # Write the prompt.
1719 1802 self._append_plain_text(self._prompt_sep)
1720 1803 if prompt is None:
1721 1804 if self._prompt_html is None:
1722 1805 self._append_plain_text(self._prompt)
1723 1806 else:
1724 1807 self._append_html(self._prompt_html)
1725 1808 else:
1726 1809 if html:
1727 1810 self._prompt = self._append_html_fetching_plain_text(prompt)
1728 1811 self._prompt_html = prompt
1729 1812 else:
1730 1813 self._append_plain_text(prompt)
1731 1814 self._prompt = prompt
1732 1815 self._prompt_html = None
1733 1816
1734 1817 self._prompt_pos = self._get_end_cursor().position()
1735 1818 self._prompt_started()
1736 1819
1737 1820 #------ Signal handlers ----------------------------------------------------
1738 1821
1739 1822 def _adjust_scrollbars(self):
1740 1823 """ Expands the vertical scrollbar beyond the range set by Qt.
1741 1824 """
1742 1825 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1743 1826 # and qtextedit.cpp.
1744 1827 document = self._control.document()
1745 1828 scrollbar = self._control.verticalScrollBar()
1746 1829 viewport_height = self._control.viewport().height()
1747 1830 if isinstance(self._control, QtGui.QPlainTextEdit):
1748 1831 maximum = max(0, document.lineCount() - 1)
1749 1832 step = viewport_height / self._control.fontMetrics().lineSpacing()
1750 1833 else:
1751 1834 # QTextEdit does not do line-based layout and blocks will not in
1752 1835 # general have the same height. Therefore it does not make sense to
1753 1836 # attempt to scroll in line height increments.
1754 1837 maximum = document.size().height()
1755 1838 step = viewport_height
1756 1839 diff = maximum - scrollbar.maximum()
1757 1840 scrollbar.setRange(0, maximum)
1758 1841 scrollbar.setPageStep(step)
1759 1842
1760 1843 # Compensate for undesirable scrolling that occurs automatically due to
1761 1844 # maximumBlockCount() text truncation.
1762 1845 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1763 1846 scrollbar.setValue(scrollbar.value() + diff)
1764 1847
1765 1848 def _cursor_position_changed(self):
1766 1849 """ Clears the temporary buffer based on the cursor position.
1767 1850 """
1768 1851 if self._text_completing_pos:
1769 1852 document = self._control.document()
1770 1853 if self._text_completing_pos < document.characterCount():
1771 1854 cursor = self._control.textCursor()
1772 1855 pos = cursor.position()
1773 1856 text_cursor = self._control.textCursor()
1774 1857 text_cursor.setPosition(self._text_completing_pos)
1775 1858 if pos < self._text_completing_pos or \
1776 1859 cursor.blockNumber() > text_cursor.blockNumber():
1777 1860 self._clear_temporary_buffer()
1778 1861 self._text_completing_pos = 0
1779 1862 else:
1780 1863 self._clear_temporary_buffer()
1781 1864 self._text_completing_pos = 0
1782 1865
1783 1866 def _custom_context_menu_requested(self, pos):
1784 1867 """ Shows a context menu at the given QPoint (in widget coordinates).
1785 1868 """
1786 1869 menu = self._context_menu_make(pos)
1787 1870 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,242 +1,270 b''
1 1 # System library imports
2 2 from IPython.external.qt import QtGui
3 3
4 4 # Local imports
5 5 from IPython.utils.traitlets import Bool
6 6 from console_widget import ConsoleWidget
7 7
8 8
9 9 class HistoryConsoleWidget(ConsoleWidget):
10 10 """ A ConsoleWidget that keeps a history of the commands that have been
11 11 executed and provides a readline-esque interface to this history.
12 12 """
13 13
14 14 #------ Configuration ------------------------------------------------------
15 15
16 16 # If enabled, the input buffer will become "locked" to history movement when
17 17 # an edit is made to a multi-line input buffer. To override the lock, use
18 18 # Shift in conjunction with the standard history cycling keys.
19 19 history_lock = Bool(False, config=True)
20 20
21 21 #---------------------------------------------------------------------------
22 22 # 'object' interface
23 23 #---------------------------------------------------------------------------
24 24
25 25 def __init__(self, *args, **kw):
26 26 super(HistoryConsoleWidget, self).__init__(*args, **kw)
27 27
28 28 # HistoryConsoleWidget protected variables.
29 29 self._history = []
30 30 self._history_edits = {}
31 31 self._history_index = 0
32 32 self._history_prefix = ''
33 33
34 self.history_action = QtGui.QAction("History",
35 self,
36 statusTip="show command history",
37 triggered=self.history_magic)
38 self.addAction(self.history_action)
39
40 self.save_action = QtGui.QAction("Export History ",
41 self,
42 statusTip="Export History as Python File",
43 triggered=self.save_magic)
44 self.addAction(self.save_action)
45
34 46 #---------------------------------------------------------------------------
35 47 # 'ConsoleWidget' public interface
36 48 #---------------------------------------------------------------------------
37 49
38 50 def execute(self, source=None, hidden=False, interactive=False):
39 51 """ Reimplemented to the store history.
40 52 """
41 53 if not hidden:
42 54 history = self.input_buffer if source is None else source
43 55
44 56 executed = super(HistoryConsoleWidget, self).execute(
45 57 source, hidden, interactive)
46 58
47 59 if executed and not hidden:
48 60 # Save the command unless it was an empty string or was identical
49 61 # to the previous command.
50 62 history = history.rstrip()
51 63 if history and (not self._history or self._history[-1] != history):
52 64 self._history.append(history)
53 65
54 66 # Emulate readline: reset all history edits.
55 67 self._history_edits = {}
56 68
57 69 # Move the history index to the most recent item.
58 70 self._history_index = len(self._history)
59 71
60 72 return executed
61 73
62 74 #---------------------------------------------------------------------------
63 75 # 'ConsoleWidget' abstract interface
64 76 #---------------------------------------------------------------------------
65 77
66 78 def _up_pressed(self, shift_modifier):
67 79 """ Called when the up key is pressed. Returns whether to continue
68 80 processing the event.
69 81 """
70 82 prompt_cursor = self._get_prompt_cursor()
71 83 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
72 84 # Bail out if we're locked.
73 85 if self._history_locked() and not shift_modifier:
74 86 return False
75 87
76 88 # Set a search prefix based on the cursor position.
77 89 col = self._get_input_buffer_cursor_column()
78 90 input_buffer = self.input_buffer
79 91 if self._history_index == len(self._history) or \
80 92 (self._history_prefix and col != len(self._history_prefix)):
81 93 self._history_index = len(self._history)
82 94 self._history_prefix = input_buffer[:col]
83 95
84 96 # Perform the search.
85 97 self.history_previous(self._history_prefix,
86 98 as_prefix=not shift_modifier)
87 99
88 100 # Go to the first line of the prompt for seemless history scrolling.
89 101 # Emulate readline: keep the cursor position fixed for a prefix
90 102 # search.
91 103 cursor = self._get_prompt_cursor()
92 104 if self._history_prefix:
93 105 cursor.movePosition(QtGui.QTextCursor.Right,
94 106 n=len(self._history_prefix))
95 107 else:
96 108 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
97 109 self._set_cursor(cursor)
98 110
99 111 return False
100 112
101 113 return True
102 114
103 115 def _down_pressed(self, shift_modifier):
104 116 """ Called when the down key is pressed. Returns whether to continue
105 117 processing the event.
106 118 """
107 119 end_cursor = self._get_end_cursor()
108 120 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
109 121 # Bail out if we're locked.
110 122 if self._history_locked() and not shift_modifier:
111 123 return False
112 124
113 125 # Perform the search.
114 126 replaced = self.history_next(self._history_prefix,
115 127 as_prefix=not shift_modifier)
116 128
117 129 # Emulate readline: keep the cursor position fixed for a prefix
118 130 # search. (We don't need to move the cursor to the end of the buffer
119 131 # in the other case because this happens automatically when the
120 132 # input buffer is set.)
121 133 if self._history_prefix and replaced:
122 134 cursor = self._get_prompt_cursor()
123 135 cursor.movePosition(QtGui.QTextCursor.Right,
124 136 n=len(self._history_prefix))
125 137 self._set_cursor(cursor)
126 138
127 139 return False
128 140
129 141 return True
130 142
131 143 #---------------------------------------------------------------------------
132 144 # 'HistoryConsoleWidget' public interface
133 145 #---------------------------------------------------------------------------
134 146
135 147 def history_previous(self, substring='', as_prefix=True):
136 148 """ If possible, set the input buffer to a previous history item.
137 149
138 150 Parameters:
139 151 -----------
140 152 substring : str, optional
141 153 If specified, search for an item with this substring.
142 154 as_prefix : bool, optional
143 155 If True, the substring must match at the beginning (default).
144 156
145 157 Returns:
146 158 --------
147 159 Whether the input buffer was changed.
148 160 """
149 161 index = self._history_index
150 162 replace = False
151 163 while index > 0:
152 164 index -= 1
153 165 history = self._get_edited_history(index)
154 166 if (as_prefix and history.startswith(substring)) \
155 167 or (not as_prefix and substring in history):
156 168 replace = True
157 169 break
158 170
159 171 if replace:
160 172 self._store_edits()
161 173 self._history_index = index
162 174 self.input_buffer = history
163 175
164 176 return replace
165 177
166 178 def history_next(self, substring='', as_prefix=True):
167 179 """ If possible, set the input buffer to a subsequent history item.
168 180
169 181 Parameters:
170 182 -----------
171 183 substring : str, optional
172 184 If specified, search for an item with this substring.
173 185 as_prefix : bool, optional
174 186 If True, the substring must match at the beginning (default).
175 187
176 188 Returns:
177 189 --------
178 190 Whether the input buffer was changed.
179 191 """
180 192 index = self._history_index
181 193 replace = False
182 194 while self._history_index < len(self._history):
183 195 index += 1
184 196 history = self._get_edited_history(index)
185 197 if (as_prefix and history.startswith(substring)) \
186 198 or (not as_prefix and substring in history):
187 199 replace = True
188 200 break
189 201
190 202 if replace:
191 203 self._store_edits()
192 204 self._history_index = index
193 205 self.input_buffer = history
194 206
195 207 return replace
196 208
197 209 def history_tail(self, n=10):
198 210 """ Get the local history list.
199 211
200 212 Parameters:
201 213 -----------
202 214 n : int
203 215 The (maximum) number of history items to get.
204 216 """
205 217 return self._history[-n:]
206 218
219 def history_magic(self):
220 self.pasteMagic("%history")
221
222 def save_magic(self):
223 file_name, ok = QtGui.QInputDialog.getText(self,
224 'Enter A file Name',
225 'Please enter a filename to wich export the history as python file:',
226 text='untilted.py')
227 if ok:
228 hist_range, ok = QtGui.QInputDialog.getText(self,
229 'Please enter an interval of command to save',
230 'Saving commands:',
231 text='1-500')
232 if ok:
233 self.pasteMagic("%save"+" "+file_name+" "+str(hist_range))
234
207 235 #---------------------------------------------------------------------------
208 236 # 'HistoryConsoleWidget' protected interface
209 237 #---------------------------------------------------------------------------
210 238
211 239 def _history_locked(self):
212 240 """ Returns whether history movement is locked.
213 241 """
214 242 return (self.history_lock and
215 243 (self._get_edited_history(self._history_index) !=
216 244 self.input_buffer) and
217 245 (self._get_prompt_cursor().blockNumber() !=
218 246 self._get_end_cursor().blockNumber()))
219 247
220 248 def _get_edited_history(self, index):
221 249 """ Retrieves a history item, possibly with temporary edits.
222 250 """
223 251 if index in self._history_edits:
224 252 return self._history_edits[index]
225 253 elif index == len(self._history):
226 254 return unicode()
227 255 return self._history[index]
228 256
229 257 def _set_history(self, history):
230 258 """ Replace the current history with a sequence of history items.
231 259 """
232 260 self._history = list(history)
233 261 self._history_edits = {}
234 262 self._history_index = len(self._history)
235 263
236 264 def _store_edits(self):
237 265 """ If there are edits to the current input buffer, store them.
238 266 """
239 267 current = self.input_buffer
240 268 if self._history_index == len(self._history) or \
241 269 self._history[self._history_index] != current:
242 270 self._history_edits[self._history_index] = current
@@ -1,595 +1,698 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
13 13 """
14 14
15 15 #-----------------------------------------------------------------------------
16 16 # Imports
17 17 #-----------------------------------------------------------------------------
18 18
19 19 # stdlib imports
20 20 import json
21 21 import os
22 22 import signal
23 23 import sys
24 24
25 25 # System library imports
26 from IPython.external.qt import QtGui
26 from IPython.external.qt import QtGui,QtCore
27 27 from pygments.styles import get_all_styles
28 28
29 29 # Local imports
30 30 from IPython.config.application import boolean_flag
31 31 from IPython.core.application import BaseIPythonApplication
32 32 from IPython.core.profiledir import ProfileDir
33 33 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
34 34 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
35 35 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
36 36 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
37 37 from IPython.frontend.qt.console import styles
38 38 from IPython.frontend.qt.kernelmanager import QtKernelManager
39 39 from IPython.utils.path import filefind
40 40 from IPython.utils.py3compat import str_to_bytes
41 41 from IPython.utils.traitlets import (
42 42 Dict, List, Unicode, Int, CaselessStrEnum, CBool, Any
43 43 )
44 44 from IPython.zmq.ipkernel import (
45 45 flags as ipkernel_flags,
46 46 aliases as ipkernel_aliases,
47 47 IPKernelApp
48 48 )
49 49 from IPython.zmq.session import Session, default_secure
50 50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51 51
52 52
53 53 #-----------------------------------------------------------------------------
54 54 # Network Constants
55 55 #-----------------------------------------------------------------------------
56 56
57 57 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
58 58
59 59 #-----------------------------------------------------------------------------
60 60 # Globals
61 61 #-----------------------------------------------------------------------------
62 62
63 63 _examples = """
64 64 ipython qtconsole # start the qtconsole
65 65 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
66 66 """
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Classes
70 70 #-----------------------------------------------------------------------------
71 71
72 72 class MainWindow(QtGui.QMainWindow):
73 73
74 74 #---------------------------------------------------------------------------
75 75 # 'object' interface
76 76 #---------------------------------------------------------------------------
77
77
78 78 def __init__(self, app, frontend, existing=False, may_close=True,
79 79 confirm_exit=True):
80 80 """ Create a MainWindow for the specified FrontendWidget.
81 81
82 82 The app is passed as an argument to allow for different
83 83 closing behavior depending on whether we are the Kernel's parent.
84 84
85 85 If existing is True, then this Console does not own the Kernel.
86 86
87 87 If may_close is True, then this Console is permitted to close the kernel
88 88 """
89 89 super(MainWindow, self).__init__()
90 90 self._app = app
91 91 self._frontend = frontend
92 92 self._existing = existing
93 93 if existing:
94 94 self._may_close = may_close
95 95 else:
96 96 self._may_close = True
97 97 self._frontend.exit_requested.connect(self.close)
98 98 self._confirm_exit = confirm_exit
99 99 self.setCentralWidget(frontend)
100 100
101 # MenuBar is always present on Mac Os, so let's populate
102 # it with possible action, don't do it on other platform
103 # as some user might not want the menu bar, or give them
104 # an option to remove it
105 if sys.platform == 'darwin':
106 #create menu in the order they should appear in the menu bar
107 self.fileMenu = self.menuBar().addMenu("File")
108 self.editMenu = self.menuBar().addMenu("Edit")
109 self.fontMenu = self.menuBar().addMenu("Font")
110 self.windowMenu = self.menuBar().addMenu("Window")
111 self.magicMenu = self.menuBar().addMenu("Magic")
112
113 # please keep the Help menu in Mac Os even if empty. It will
114 # automatically contain a search field to search inside menus and
115 # please keep it spelled in English, as long as Qt Doesn't support
116 # a QAction.MenuRole like HelpMenuRole otherwise it will loose
117 # this search field fonctionnality
118
119 self.helpMenu = self.menuBar().addMenu("Help")
120
121 # sould wrap every line of the following block into a try/except,
122 # as we are not sure of instanciating a _frontend which support all
123 # theses actions, but there might be a better way
124
125 self.fileMenu.addAction(self._frontend.print_action)
126 self.fileMenu.addAction(self._frontend.export_action)
127 self.fileMenu.addAction(self._frontend.select_all_action)
128
129 self.editMenu.addAction(self._frontend.undo_action)
130 self.editMenu.addAction(self._frontend.redo_action)
131
132 self.fontMenu.addAction(self._frontend.increase_font_size)
133 self.fontMenu.addAction(self._frontend.decrease_font_size)
134 self.fontMenu.addAction(self._frontend.reset_font_size)
135
136 self.magicMenu.addAction(self._frontend.reset_action)
137 self.magicMenu.addAction(self._frontend.history_action)
138 self.magicMenu.addAction(self._frontend.save_action)
139 self.magicMenu.addAction(self._frontend.clear_action)
140 self.magicMenu.addAction(self._frontend.who_action)
141 self.magicMenu.addAction(self._frontend.who_ls_action)
142 self.magicMenu.addAction(self._frontend.whos_action)
143
101 144 #---------------------------------------------------------------------------
102 145 # QWidget interface
103 146 #---------------------------------------------------------------------------
104 147
105 148 def closeEvent(self, event):
106 149 """ Close the window and the kernel (if necessary).
107 150
108 151 This will prompt the user if they are finished with the kernel, and if
109 152 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
110 153 it closes without prompt.
111 154 """
112 155 keepkernel = None #Use the prompt by default
113 156 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
114 157 keepkernel = self._frontend._keep_kernel_on_exit
115 158
116 159 kernel_manager = self._frontend.kernel_manager
117 160
118 161 if keepkernel is None and not self._confirm_exit:
119 162 # don't prompt, just terminate the kernel if we own it
120 163 # or leave it alone if we don't
121 164 keepkernel = not self._existing
122 165
123 166 if keepkernel is None: #show prompt
124 167 if kernel_manager and kernel_manager.channels_running:
125 168 title = self.window().windowTitle()
126 169 cancel = QtGui.QMessageBox.Cancel
127 170 okay = QtGui.QMessageBox.Ok
128 171 if self._may_close:
129 172 msg = "You are closing this Console window."
130 173 info = "Would you like to quit the Kernel and all attached Consoles as well?"
131 174 justthis = QtGui.QPushButton("&No, just this Console", self)
132 175 justthis.setShortcut('N')
133 176 closeall = QtGui.QPushButton("&Yes, quit everything", self)
134 177 closeall.setShortcut('Y')
135 178 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
136 179 title, msg)
137 180 box.setInformativeText(info)
138 181 box.addButton(cancel)
139 182 box.addButton(justthis, QtGui.QMessageBox.NoRole)
140 183 box.addButton(closeall, QtGui.QMessageBox.YesRole)
141 184 box.setDefaultButton(closeall)
142 185 box.setEscapeButton(cancel)
143 186 reply = box.exec_()
144 187 if reply == 1: # close All
145 188 kernel_manager.shutdown_kernel()
146 189 #kernel_manager.stop_channels()
147 190 event.accept()
148 191 elif reply == 0: # close Console
149 192 if not self._existing:
150 193 # Have kernel: don't quit, just close the window
151 194 self._app.setQuitOnLastWindowClosed(False)
152 195 self.deleteLater()
153 196 event.accept()
154 197 else:
155 198 event.ignore()
156 199 else:
157 200 reply = QtGui.QMessageBox.question(self, title,
158 201 "Are you sure you want to close this Console?"+
159 202 "\nThe Kernel and other Consoles will remain active.",
160 203 okay|cancel,
161 204 defaultButton=okay
162 205 )
163 206 if reply == okay:
164 207 event.accept()
165 208 else:
166 209 event.ignore()
167 210 elif keepkernel: #close console but leave kernel running (no prompt)
168 211 if kernel_manager and kernel_manager.channels_running:
169 212 if not self._existing:
170 213 # I have the kernel: don't quit, just close the window
171 214 self._app.setQuitOnLastWindowClosed(False)
172 215 event.accept()
173 216 else: #close console and kernel (no prompt)
174 217 if kernel_manager and kernel_manager.channels_running:
175 218 kernel_manager.shutdown_kernel()
176 219 event.accept()
177 220
178 221 #-----------------------------------------------------------------------------
179 222 # Aliases and Flags
180 223 #-----------------------------------------------------------------------------
181 224
182 225 flags = dict(ipkernel_flags)
183 226 qt_flags = {
184 227 'existing' : ({'IPythonQtConsoleApp' : {'existing' : 'kernel*.json'}},
185 228 "Connect to an existing kernel. If no argument specified, guess most recent"),
186 229 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
187 230 "Use a pure Python kernel instead of an IPython kernel."),
188 231 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
189 232 "Disable rich text support."),
190 233 }
191 234 qt_flags.update(boolean_flag(
192 235 'gui-completion', 'ConsoleWidget.gui_completion',
193 236 "use a GUI widget for tab completion",
194 237 "use plaintext output for completion"
195 238 ))
196 239 qt_flags.update(boolean_flag(
197 240 'confirm-exit', 'IPythonQtConsoleApp.confirm_exit',
198 241 """Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
199 242 to force a direct exit without any confirmation.
200 243 """,
201 244 """Don't prompt the user when exiting. This will terminate the kernel
202 245 if it is owned by the frontend, and leave it alive if it is external.
203 246 """
204 247 ))
205 248 flags.update(qt_flags)
206 249
207 250 aliases = dict(ipkernel_aliases)
208 251
209 252 qt_aliases = dict(
210 253 hb = 'IPythonQtConsoleApp.hb_port',
211 254 shell = 'IPythonQtConsoleApp.shell_port',
212 255 iopub = 'IPythonQtConsoleApp.iopub_port',
213 256 stdin = 'IPythonQtConsoleApp.stdin_port',
214 257 ip = 'IPythonQtConsoleApp.ip',
215 258 existing = 'IPythonQtConsoleApp.existing',
216 259 f = 'IPythonQtConsoleApp.connection_file',
217 260
218 261 style = 'IPythonWidget.syntax_style',
219 262 stylesheet = 'IPythonQtConsoleApp.stylesheet',
220 263 colors = 'ZMQInteractiveShell.colors',
221 264
222 265 editor = 'IPythonWidget.editor',
223 266 paging = 'ConsoleWidget.paging',
224 267 ssh = 'IPythonQtConsoleApp.sshserver',
225 268 )
226 269 aliases.update(qt_aliases)
227 270
228 271
229 272 #-----------------------------------------------------------------------------
230 273 # IPythonQtConsole
231 274 #-----------------------------------------------------------------------------
232 275
233 276
234 277 class IPythonQtConsoleApp(BaseIPythonApplication):
235 278 name = 'ipython-qtconsole'
236 279 default_config_file_name='ipython_config.py'
237 280
238 281 description = """
239 282 The IPython QtConsole.
240 283
241 284 This launches a Console-style application using Qt. It is not a full
242 285 console, in that launched terminal subprocesses will not be able to accept
243 286 input.
244 287
245 288 The QtConsole supports various extra features beyond the Terminal IPython
246 289 shell, such as inline plotting with matplotlib, via:
247 290
248 291 ipython qtconsole --pylab=inline
249 292
250 293 as well as saving your session as HTML, and printing the output.
251 294
252 295 """
253 296 examples = _examples
254 297
255 298 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
256 299 flags = Dict(flags)
257 300 aliases = Dict(aliases)
258 301
259 302 kernel_argv = List(Unicode)
260 303
261 304 # create requested profiles by default, if they don't exist:
262 305 auto_create = CBool(True)
263 306 # connection info:
264 307 ip = Unicode(LOCALHOST, config=True,
265 308 help="""Set the kernel\'s IP address [default localhost].
266 309 If the IP address is something other than localhost, then
267 310 Consoles on other machines will be able to connect
268 311 to the Kernel, so be careful!"""
269 312 )
270 313
271 314 sshserver = Unicode('', config=True,
272 315 help="""The SSH server to use to connect to the kernel.""")
273 316 sshkey = Unicode('', config=True,
274 317 help="""Path to the ssh key to use for logging in to the ssh server.""")
275 318
276 319 hb_port = Int(0, config=True,
277 320 help="set the heartbeat port [default: random]")
278 321 shell_port = Int(0, config=True,
279 322 help="set the shell (XREP) port [default: random]")
280 323 iopub_port = Int(0, config=True,
281 324 help="set the iopub (PUB) port [default: random]")
282 325 stdin_port = Int(0, config=True,
283 326 help="set the stdin (XREQ) port [default: random]")
284 327 connection_file = Unicode('', config=True,
285 328 help="""JSON file in which to store connection info [default: kernel-<pid>.json]
286 329
287 330 This file will contain the IP, ports, and authentication key needed to connect
288 331 clients to this kernel. By default, this file will be created in the security-dir
289 332 of the current profile, but can be specified by absolute path.
290 333 """)
291 334 def _connection_file_default(self):
292 335 return 'kernel-%i.json' % os.getpid()
293 336
294 337 existing = Unicode('', config=True,
295 338 help="""Connect to an already running kernel""")
296 339
297 340 stylesheet = Unicode('', config=True,
298 341 help="path to a custom CSS stylesheet")
299 342
300 343 pure = CBool(False, config=True,
301 344 help="Use a pure Python kernel instead of an IPython kernel.")
302 345 plain = CBool(False, config=True,
303 346 help="Use a plaintext widget instead of rich text (plain can't print/save).")
304 347
305 348 def _pure_changed(self, name, old, new):
306 349 kind = 'plain' if self.plain else 'rich'
307 350 self.config.ConsoleWidget.kind = kind
308 351 if self.pure:
309 352 self.widget_factory = FrontendWidget
310 353 elif self.plain:
311 354 self.widget_factory = IPythonWidget
312 355 else:
313 356 self.widget_factory = RichIPythonWidget
314 357
315 358 _plain_changed = _pure_changed
316 359
317 360 confirm_exit = CBool(True, config=True,
318 361 help="""
319 362 Set to display confirmation dialog on exit. You can always use 'exit' or 'quit',
320 363 to force a direct exit without any confirmation.""",
321 364 )
322 365
323 366 # the factory for creating a widget
324 367 widget_factory = Any(RichIPythonWidget)
325 368
326 369 def parse_command_line(self, argv=None):
327 370 super(IPythonQtConsoleApp, self).parse_command_line(argv)
328 371 if argv is None:
329 372 argv = sys.argv[1:]
330 373
331 374 self.kernel_argv = list(argv) # copy
332 375 # kernel should inherit default config file from frontend
333 376 self.kernel_argv.append("--KernelApp.parent_appname='%s'"%self.name)
334 377 # Scrub frontend-specific flags
335 378 for a in argv:
336 379 if a.startswith('-') and a.lstrip('-') in qt_flags:
337 380 self.kernel_argv.remove(a)
338 381 swallow_next = False
339 382 for a in argv:
340 383 if swallow_next:
341 384 self.kernel_argv.remove(a)
342 385 swallow_next = False
343 386 continue
344 387 if a.startswith('-'):
345 388 split = a.lstrip('-').split('=')
346 389 alias = split[0]
347 390 if alias in qt_aliases:
348 391 self.kernel_argv.remove(a)
349 392 if len(split) == 1:
350 393 # alias passed with arg via space
351 394 swallow_next = True
352 395
353 396 def init_connection_file(self):
354 397 """find the connection file, and load the info if found.
355 398
356 399 The current working directory and the current profile's security
357 400 directory will be searched for the file if it is not given by
358 401 absolute path.
359 402
360 403 When attempting to connect to an existing kernel and the `--existing`
361 404 argument does not match an existing file, it will be interpreted as a
362 405 fileglob, and the matching file in the current profile's security dir
363 406 with the latest access time will be used.
364 407 """
365 408 if self.existing:
366 409 try:
367 410 cf = find_connection_file(self.existing)
368 411 except Exception:
369 412 self.log.critical("Could not find existing kernel connection file %s", self.existing)
370 413 self.exit(1)
371 414 self.log.info("Connecting to existing kernel: %s" % cf)
372 415 self.connection_file = cf
373 416 # should load_connection_file only be used for existing?
374 417 # as it is now, this allows reusing ports if an existing
375 418 # file is requested
376 419 try:
377 420 self.load_connection_file()
378 421 except Exception:
379 422 self.log.error("Failed to load connection file: %r", self.connection_file, exc_info=True)
380 423 self.exit(1)
381 424
382 425 def load_connection_file(self):
383 426 """load ip/port/hmac config from JSON connection file"""
384 427 # this is identical to KernelApp.load_connection_file
385 428 # perhaps it can be centralized somewhere?
386 429 try:
387 430 fname = filefind(self.connection_file, ['.', self.profile_dir.security_dir])
388 431 except IOError:
389 432 self.log.debug("Connection File not found: %s", self.connection_file)
390 433 return
391 434 self.log.debug(u"Loading connection file %s", fname)
392 435 with open(fname) as f:
393 436 s = f.read()
394 437 cfg = json.loads(s)
395 438 if self.ip == LOCALHOST and 'ip' in cfg:
396 439 # not overridden by config or cl_args
397 440 self.ip = cfg['ip']
398 441 for channel in ('hb', 'shell', 'iopub', 'stdin'):
399 442 name = channel + '_port'
400 443 if getattr(self, name) == 0 and name in cfg:
401 444 # not overridden by config or cl_args
402 445 setattr(self, name, cfg[name])
403 446 if 'key' in cfg:
404 447 self.config.Session.key = str_to_bytes(cfg['key'])
405 448
406 449 def init_ssh(self):
407 450 """set up ssh tunnels, if needed."""
408 451 if not self.sshserver and not self.sshkey:
409 452 return
410 453
411 454 if self.sshkey and not self.sshserver:
412 455 # specifying just the key implies that we are connecting directly
413 456 self.sshserver = self.ip
414 457 self.ip = LOCALHOST
415 458
416 459 # build connection dict for tunnels:
417 460 info = dict(ip=self.ip,
418 461 shell_port=self.shell_port,
419 462 iopub_port=self.iopub_port,
420 463 stdin_port=self.stdin_port,
421 464 hb_port=self.hb_port
422 465 )
423 466
424 467 self.log.info("Forwarding connections to %s via %s"%(self.ip, self.sshserver))
425 468
426 469 # tunnels return a new set of ports, which will be on localhost:
427 470 self.ip = LOCALHOST
428 471 try:
429 472 newports = tunnel_to_kernel(info, self.sshserver, self.sshkey)
430 473 except:
431 474 # even catch KeyboardInterrupt
432 475 self.log.error("Could not setup tunnels", exc_info=True)
433 476 self.exit(1)
434 477
435 478 self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports
436 479
437 480 cf = self.connection_file
438 481 base,ext = os.path.splitext(cf)
439 482 base = os.path.basename(base)
440 483 self.connection_file = os.path.basename(base)+'-ssh'+ext
441 484 self.log.critical("To connect another client via this tunnel, use:")
442 485 self.log.critical("--existing %s" % self.connection_file)
443 486
444 487 def init_kernel_manager(self):
445 488 # Don't let Qt or ZMQ swallow KeyboardInterupts.
446 489 signal.signal(signal.SIGINT, signal.SIG_DFL)
447 490 sec = self.profile_dir.security_dir
448 491 try:
449 492 cf = filefind(self.connection_file, ['.', sec])
450 493 except IOError:
451 494 # file might not exist
452 495 if self.connection_file == os.path.basename(self.connection_file):
453 496 # just shortname, put it in security dir
454 497 cf = os.path.join(sec, self.connection_file)
455 498 else:
456 499 cf = self.connection_file
457 500
458 501 # Create a KernelManager and start a kernel.
459 502 self.kernel_manager = QtKernelManager(
460 503 ip=self.ip,
461 504 shell_port=self.shell_port,
462 505 iopub_port=self.iopub_port,
463 506 stdin_port=self.stdin_port,
464 507 hb_port=self.hb_port,
465 508 connection_file=cf,
466 509 config=self.config,
467 510 )
468 511 # start the kernel
469 512 if not self.existing:
470 513 kwargs = dict(ipython=not self.pure)
471 514 kwargs['extra_arguments'] = self.kernel_argv
472 515 self.kernel_manager.start_kernel(**kwargs)
473 516 elif self.sshserver:
474 517 # ssh, write new connection file
475 518 self.kernel_manager.write_connection_file()
476 519 self.kernel_manager.start_channels()
477 520
478 521
479 522 def init_qt_elements(self):
480 523 # Create the widget.
481 524 self.app = QtGui.QApplication([])
482 525 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
483 526 self.widget = self.widget_factory(config=self.config,
484 527 local_kernel=local_kernel)
485 528 self.widget.kernel_manager = self.kernel_manager
486 529 self.window = MainWindow(self.app, self.widget, self.existing,
487 530 may_close=local_kernel,
488 531 confirm_exit=self.confirm_exit)
489 532 self.window.setWindowTitle('Python' if self.pure else 'IPython')
490 533
491 534 def init_colors(self):
492 535 """Configure the coloring of the widget"""
493 536 # Note: This will be dramatically simplified when colors
494 537 # are removed from the backend.
495 538
496 539 if self.pure:
497 540 # only IPythonWidget supports styling
498 541 return
499 542
500 543 # parse the colors arg down to current known labels
501 544 try:
502 545 colors = self.config.ZMQInteractiveShell.colors
503 546 except AttributeError:
504 547 colors = None
505 548 try:
506 549 style = self.config.IPythonWidget.colors
507 550 except AttributeError:
508 551 style = None
509 552
510 553 # find the value for colors:
511 554 if colors:
512 555 colors=colors.lower()
513 556 if colors in ('lightbg', 'light'):
514 557 colors='lightbg'
515 558 elif colors in ('dark', 'linux'):
516 559 colors='linux'
517 560 else:
518 561 colors='nocolor'
519 562 elif style:
520 563 if style=='bw':
521 564 colors='nocolor'
522 565 elif styles.dark_style(style):
523 566 colors='linux'
524 567 else:
525 568 colors='lightbg'
526 569 else:
527 570 colors=None
528 571
529 572 # Configure the style.
530 573 widget = self.widget
531 574 if style:
532 575 widget.style_sheet = styles.sheet_from_template(style, colors)
533 576 widget.syntax_style = style
534 577 widget._syntax_style_changed()
535 578 widget._style_sheet_changed()
536 579 elif colors:
537 580 # use a default style
538 581 widget.set_default_style(colors=colors)
539 582 else:
540 583 # this is redundant for now, but allows the widget's
541 584 # defaults to change
542 585 widget.set_default_style()
543 586
544 587 if self.stylesheet:
545 588 # we got an expicit stylesheet
546 589 if os.path.isfile(self.stylesheet):
547 590 with open(self.stylesheet) as f:
548 591 sheet = f.read()
549 592 widget.style_sheet = sheet
550 593 widget._style_sheet_changed()
551 594 else:
552 595 raise IOError("Stylesheet %r not found."%self.stylesheet)
553 596
554 597 def initialize(self, argv=None):
555 598 super(IPythonQtConsoleApp, self).initialize(argv)
556 599 self.init_connection_file()
557 600 default_secure(self.config)
558 601 self.init_ssh()
559 602 self.init_kernel_manager()
560 603 self.init_qt_elements()
561 604 self.init_colors()
562 605 self.init_window_shortcut()
563 606
564 607 def init_window_shortcut(self):
565 fullScreenAction = QtGui.QAction('Toggle Full Screen', self.window)
566 fullScreenAction.setShortcut('Ctrl+Meta+Space')
567 fullScreenAction.triggered.connect(self.toggleFullScreen)
568 self.window.addAction(fullScreenAction)
569 608
609 self.fullScreenAct = QtGui.QAction("Full Screen",
610 self.window,
611 shortcut="Ctrl+Meta+Space",
612 statusTip="Toggle between Fullscreen and Normal Size",
613 triggered=self.toggleFullScreen)
614
615
616 # creating shortcut in menubar only for Mac OS as I don't
617 # know the shortcut or if the windows manager assign it in
618 # other platform.
619 if sys.platform == 'darwin':
620 self.minimizeAct = QtGui.QAction("Minimize",
621 self.window,
622 shortcut="Ctrl+m",
623 statusTip="Minimize the window/Restore Normal Size",
624 triggered=self.toggleMinimized)
625 self.maximizeAct = QtGui.QAction("Maximize",
626 self.window,
627 shortcut="Ctrl+Shift+M",
628 statusTip="Maximize the window/Restore Normal Size",
629 triggered=self.toggleMaximized)
630
631 self.onlineHelpAct = QtGui.QAction("Open Online Help",
632 self.window,
633 triggered=self._open_online_help)
634
635 self.windowMenu = self.window.windowMenu
636 self.windowMenu.addAction(self.minimizeAct)
637 self.windowMenu.addAction(self.maximizeAct)
638 self.windowMenu.addSeparator()
639 self.windowMenu.addAction(self.fullScreenAct)
640
641 self.window.helpMenu.addAction(self.onlineHelpAct)
642 else:
643 # if we don't put it in a menu, we add it to the window so
644 # that it can still be triggerd by shortcut
645 self.window.addAction(self.fullScreenAct)
646
647 def toggleMinimized(self):
648 if not self.window.isMinimized():
649 self.window.showMinimized()
650 else:
651 self.window.showNormal()
652
653 def _open_online_help(self):
654 QtGui.QDesktopServices.openUrl(
655 QtCore.QUrl("http://ipython.org/documentation.html",
656 QtCore.QUrl.TolerantMode)
657 )
658
659 def toggleMaximized(self):
660 if not self.window.isMaximized():
661 self.window.showMaximized()
662 else:
663 self.window.showNormal()
664
665 # Min/Max imizing while in full screen give a bug
666 # when going out of full screen, at least on OSX
570 667 def toggleFullScreen(self):
571 668 if not self.window.isFullScreen():
572 669 self.window.showFullScreen()
670 if sys.platform == 'darwin':
671 self.maximizeAct.setEnabled(False)
672 self.minimizeAct.setEnabled(False)
573 673 else:
574 674 self.window.showNormal()
675 if sys.platform == 'darwin':
676 self.maximizeAct.setEnabled(True)
677 self.minimizeAct.setEnabled(True)
575 678
576 679 def start(self):
577 680
578 681 # draw the window
579 682 self.window.show()
580 683
581 684 # Start the application main loop.
582 685 self.app.exec_()
583 686
584 687 #-----------------------------------------------------------------------------
585 688 # Main entry point
586 689 #-----------------------------------------------------------------------------
587 690
588 691 def main():
589 692 app = IPythonQtConsoleApp()
590 693 app.initialize()
591 694 app.start()
592 695
593 696
594 697 if __name__ == '__main__':
595 698 main()
General Comments 0
You need to be logged in to leave comments. Login now