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