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