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