##// END OF EJS Templates
Merge pull request #6123 from minrk/zmq-console-echo-other...
Thomas Kluyver -
r18432:5ef19765 merge
parent child Browse files
Show More
@@ -1,150 +1,158 b''
1 1 """ Defines a convenient mix-in class for implementing Qt frontends.
2 2 """
3 3
4 4 class BaseFrontendMixin(object):
5 5 """ A mix-in class for implementing Qt frontends.
6 6
7 7 To handle messages of a particular type, frontends need only define an
8 8 appropriate handler method. For example, to handle 'stream' messaged, define
9 9 a '_handle_stream(msg)' method.
10 10 """
11 11
12 12 #---------------------------------------------------------------------------
13 13 # 'BaseFrontendMixin' concrete interface
14 14 #---------------------------------------------------------------------------
15 15 _kernel_client = None
16 16 _kernel_manager = None
17 17
18 18 @property
19 19 def kernel_client(self):
20 20 """Returns the current kernel client."""
21 21 return self._kernel_client
22 22
23 23 @kernel_client.setter
24 24 def kernel_client(self, kernel_client):
25 25 """Disconnect from the current kernel client (if any) and set a new
26 26 kernel client.
27 27 """
28 28 # Disconnect the old kernel client, if necessary.
29 29 old_client = self._kernel_client
30 30 if old_client is not None:
31 31 old_client.started_channels.disconnect(self._started_channels)
32 32 old_client.stopped_channels.disconnect(self._stopped_channels)
33 33
34 34 # Disconnect the old kernel client's channels.
35 35 old_client.iopub_channel.message_received.disconnect(self._dispatch)
36 36 old_client.shell_channel.message_received.disconnect(self._dispatch)
37 37 old_client.stdin_channel.message_received.disconnect(self._dispatch)
38 38 old_client.hb_channel.kernel_died.disconnect(
39 39 self._handle_kernel_died)
40 40
41 41 # Handle the case where the old kernel client is still listening.
42 42 if old_client.channels_running:
43 43 self._stopped_channels()
44 44
45 45 # Set the new kernel client.
46 46 self._kernel_client = kernel_client
47 47 if kernel_client is None:
48 48 return
49 49
50 50 # Connect the new kernel client.
51 51 kernel_client.started_channels.connect(self._started_channels)
52 52 kernel_client.stopped_channels.connect(self._stopped_channels)
53 53
54 54 # Connect the new kernel client's channels.
55 55 kernel_client.iopub_channel.message_received.connect(self._dispatch)
56 56 kernel_client.shell_channel.message_received.connect(self._dispatch)
57 57 kernel_client.stdin_channel.message_received.connect(self._dispatch)
58 58 # hb_channel
59 59 kernel_client.hb_channel.kernel_died.connect(self._handle_kernel_died)
60 60
61 61 # Handle the case where the kernel client started channels before
62 62 # we connected.
63 63 if kernel_client.channels_running:
64 64 self._started_channels()
65 65
66 66 @property
67 67 def kernel_manager(self):
68 68 """The kernel manager, if any"""
69 69 return self._kernel_manager
70 70
71 71 @kernel_manager.setter
72 72 def kernel_manager(self, kernel_manager):
73 73 old_man = self._kernel_manager
74 74 if old_man is not None:
75 75 old_man.kernel_restarted.disconnect(self._handle_kernel_restarted)
76 76
77 77 self._kernel_manager = kernel_manager
78 78 if kernel_manager is None:
79 79 return
80 80
81 81 kernel_manager.kernel_restarted.connect(self._handle_kernel_restarted)
82 82
83 83 #---------------------------------------------------------------------------
84 84 # 'BaseFrontendMixin' abstract interface
85 85 #---------------------------------------------------------------------------
86 86
87 87 def _handle_kernel_died(self, since_last_heartbeat):
88 88 """ This is called when the ``kernel_died`` signal is emitted.
89 89
90 90 This method is called when the kernel heartbeat has not been
91 91 active for a certain amount of time.
92 92 This is a strictly passive notification -
93 93 the kernel is likely being restarted by its KernelManager.
94 94
95 95 Parameters
96 96 ----------
97 97 since_last_heartbeat : float
98 98 The time since the heartbeat was last received.
99 99 """
100 100
101 101 def _handle_kernel_restarted(self):
102 102 """ This is called when the ``kernel_restarted`` signal is emitted.
103 103
104 104 This method is called when the kernel has been restarted by the
105 105 autorestart mechanism.
106 106
107 107 Parameters
108 108 ----------
109 109 since_last_heartbeat : float
110 110 The time since the heartbeat was last received.
111 111 """
112 112 def _started_kernel(self):
113 113 """Called when the KernelManager starts (or restarts) the kernel subprocess.
114 114 Channels may or may not be running at this point.
115 115 """
116 116
117 117 def _started_channels(self):
118 118 """ Called when the KernelManager channels have started listening or
119 119 when the frontend is assigned an already listening KernelManager.
120 120 """
121 121
122 122 def _stopped_channels(self):
123 123 """ Called when the KernelManager channels have stopped listening or
124 124 when a listening KernelManager is removed from the frontend.
125 125 """
126 126
127 127 #---------------------------------------------------------------------------
128 128 # 'BaseFrontendMixin' protected interface
129 129 #---------------------------------------------------------------------------
130 130
131 131 def _dispatch(self, msg):
132 132 """ Calls the frontend handler associated with the message type of the
133 133 given message.
134 134 """
135 135 msg_type = msg['header']['msg_type']
136 136 handler = getattr(self, '_handle_' + msg_type, None)
137 137 if handler:
138 138 handler(msg)
139
140 def _is_from_this_session(self, msg):
141 """ Returns whether a reply from the kernel originated from a request
142 from this frontend.
143 """
144 session = self._kernel_client.session.session
145 parent = msg['parent_header']
146 if not parent:
147 # if the message has no parent, assume it is meant for all frontends
139
140 def from_here(self, msg):
141 """Return whether a message is from this session"""
142 session_id = self._kernel_client.session.session
143 return msg['parent_header'].get("session", session_id) == session_id
144
145 def include_output(self, msg):
146 """Return whether we should include a given output message"""
147 if self._hidden:
148 return False
149 from_here = self.from_here(msg)
150 if msg['msg_type'] == 'execute_input':
151 # only echo inputs not from here
152 return self.include_other_output and not from_here
153
154 if self.include_other_output:
148 155 return True
149 156 else:
150 return parent.get('session') == session
157 return from_here
158
@@ -1,2155 +1,2163 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.path
9 9 import re
10 10 import sys
11 11 from textwrap import dedent
12 12 import time
13 13 from unicodedata import category
14 14 import webbrowser
15 15
16 16 # System library imports
17 17 from IPython.external.qt import QtCore, QtGui
18 18
19 19 # Local imports
20 20 from IPython.config.configurable import LoggingConfigurable
21 21 from IPython.core.inputsplitter import ESC_SEQUENCES
22 22 from IPython.qt.rich_text import HtmlExporter
23 23 from IPython.qt.util import MetaQObjectHasTraits, get_font
24 24 from IPython.utils.text import columnize
25 25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
26 26 from .ansi_code_processor import QtAnsiCodeProcessor
27 27 from .completion_widget import CompletionWidget
28 28 from .completion_html import CompletionHtml
29 29 from .completion_plain import CompletionPlain
30 30 from .kill_ring import QtKillRing
31 31
32 32
33 33 #-----------------------------------------------------------------------------
34 34 # Functions
35 35 #-----------------------------------------------------------------------------
36 36
37 37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
38 38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
39 39
40 40 def commonprefix(items):
41 41 """Get common prefix for completions
42 42
43 43 Return the longest common prefix of a list of strings, but with special
44 44 treatment of escape characters that might precede commands in IPython,
45 45 such as %magic functions. Used in tab completion.
46 46
47 47 For a more general function, see os.path.commonprefix
48 48 """
49 49 # the last item will always have the least leading % symbol
50 50 # min / max are first/last in alphabetical order
51 51 first_match = ESCAPE_RE.match(min(items))
52 52 last_match = ESCAPE_RE.match(max(items))
53 53 # common suffix is (common prefix of reversed items) reversed
54 54 if first_match and last_match:
55 55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
56 56 else:
57 57 prefix = ''
58 58
59 59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
60 60 return prefix+os.path.commonprefix(items)
61 61
62 62 def is_letter_or_number(char):
63 63 """ Returns whether the specified unicode character is a letter or a number.
64 64 """
65 65 cat = category(char)
66 66 return cat.startswith('L') or cat.startswith('N')
67 67
68 68 #-----------------------------------------------------------------------------
69 69 # Classes
70 70 #-----------------------------------------------------------------------------
71 71
72 72 class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui.QWidget), {})):
73 73 """ An abstract base class for console-type widgets. This class has
74 74 functionality for:
75 75
76 76 * Maintaining a prompt and editing region
77 77 * Providing the traditional Unix-style console keyboard shortcuts
78 78 * Performing tab completion
79 79 * Paging text
80 80 * Handling ANSI escape codes
81 81
82 82 ConsoleWidget also provides a number of utility methods that will be
83 83 convenient to implementors of a console-style widget.
84 84 """
85 85
86 86 #------ Configuration ------------------------------------------------------
87 87
88 88 ansi_codes = Bool(True, config=True,
89 89 help="Whether to process ANSI escape codes."
90 90 )
91 91 buffer_size = Integer(500, config=True,
92 92 help="""
93 93 The maximum number of lines of text before truncation. Specifying a
94 94 non-positive number disables text truncation (not recommended).
95 95 """
96 96 )
97 97 execute_on_complete_input = Bool(True, config=True,
98 98 help="""Whether to automatically execute on syntactically complete input.
99 99
100 100 If False, Shift-Enter is required to submit each execution.
101 101 Disabling this is mainly useful for non-Python kernels,
102 102 where the completion check would be wrong.
103 103 """
104 104 )
105 105 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
106 106 default_value = 'ncurses',
107 107 help="""
108 108 The type of completer to use. Valid values are:
109 109
110 110 'plain' : Show the available completion as a text list
111 111 Below the editing area.
112 112 'droplist': Show the completion in a drop down list navigable
113 113 by the arrow keys, and from which you can select
114 114 completion by pressing Return.
115 115 'ncurses' : Show the completion as a text list which is navigable by
116 116 `tab` and arrow keys.
117 117 """
118 118 )
119 119 # NOTE: this value can only be specified during initialization.
120 120 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
121 121 help="""
122 122 The type of underlying text widget to use. Valid values are 'plain',
123 123 which specifies a QPlainTextEdit, and 'rich', which specifies a
124 124 QTextEdit.
125 125 """
126 126 )
127 127 # NOTE: this value can only be specified during initialization.
128 128 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
129 129 default_value='inside', config=True,
130 130 help="""
131 131 The type of paging to use. Valid values are:
132 132
133 133 'inside'
134 134 The widget pages like a traditional terminal.
135 135 'hsplit'
136 136 When paging is requested, the widget is split horizontally. The top
137 137 pane contains the console, and the bottom pane contains the paged text.
138 138 'vsplit'
139 139 Similar to 'hsplit', except that a vertical splitter is used.
140 140 'custom'
141 141 No action is taken by the widget beyond emitting a
142 142 'custom_page_requested(str)' signal.
143 143 'none'
144 144 The text is written directly to the console.
145 145 """)
146 146
147 147 font_family = Unicode(config=True,
148 148 help="""The font family to use for the console.
149 149 On OSX this defaults to Monaco, on Windows the default is
150 150 Consolas with fallback of Courier, and on other platforms
151 151 the default is Monospace.
152 152 """)
153 153 def _font_family_default(self):
154 154 if sys.platform == 'win32':
155 155 # Consolas ships with Vista/Win7, fallback to Courier if needed
156 156 return 'Consolas'
157 157 elif sys.platform == 'darwin':
158 158 # OSX always has Monaco, no need for a fallback
159 159 return 'Monaco'
160 160 else:
161 161 # Monospace should always exist, no need for a fallback
162 162 return 'Monospace'
163 163
164 164 font_size = Integer(config=True,
165 165 help="""The font size. If unconfigured, Qt will be entrusted
166 166 with the size of the font.
167 167 """)
168 168
169 169 width = Integer(81, config=True,
170 170 help="""The width of the console at start time in number
171 171 of characters (will double with `hsplit` paging)
172 172 """)
173 173
174 174 height = Integer(25, config=True,
175 175 help="""The height of the console at start time in number
176 176 of characters (will double with `vsplit` paging)
177 177 """)
178 178
179 179 # Whether to override ShortcutEvents for the keybindings defined by this
180 180 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
181 181 # priority (when it has focus) over, e.g., window-level menu shortcuts.
182 182 override_shortcuts = Bool(False)
183 183
184 184 # ------ Custom Qt Widgets -------------------------------------------------
185 185
186 186 # For other projects to easily override the Qt widgets used by the console
187 187 # (e.g. Spyder)
188 188 custom_control = None
189 189 custom_page_control = None
190 190
191 191 #------ Signals ------------------------------------------------------------
192 192
193 193 # Signals that indicate ConsoleWidget state.
194 194 copy_available = QtCore.Signal(bool)
195 195 redo_available = QtCore.Signal(bool)
196 196 undo_available = QtCore.Signal(bool)
197 197
198 198 # Signal emitted when paging is needed and the paging style has been
199 199 # specified as 'custom'.
200 200 custom_page_requested = QtCore.Signal(object)
201 201
202 202 # Signal emitted when the font is changed.
203 203 font_changed = QtCore.Signal(QtGui.QFont)
204 204
205 205 #------ Protected class variables ------------------------------------------
206 206
207 207 # control handles
208 208 _control = None
209 209 _page_control = None
210 210 _splitter = None
211 211
212 212 # When the control key is down, these keys are mapped.
213 213 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
214 214 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
215 215 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
216 216 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
217 217 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
218 218 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
219 219 if not sys.platform == 'darwin':
220 220 # On OS X, Ctrl-E already does the right thing, whereas End moves the
221 221 # cursor to the bottom of the buffer.
222 222 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
223 223
224 224 # The shortcuts defined by this widget. We need to keep track of these to
225 225 # support 'override_shortcuts' above.
226 226 _shortcuts = set(_ctrl_down_remap.keys()) | \
227 227 { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
228 228 QtCore.Qt.Key_V }
229 229
230 230 _temp_buffer_filled = False
231 231
232 232 #---------------------------------------------------------------------------
233 233 # 'QObject' interface
234 234 #---------------------------------------------------------------------------
235 235
236 236 def __init__(self, parent=None, **kw):
237 237 """ Create a ConsoleWidget.
238 238
239 239 Parameters
240 240 ----------
241 241 parent : QWidget, optional [default None]
242 242 The parent for this widget.
243 243 """
244 244 QtGui.QWidget.__init__(self, parent)
245 245 LoggingConfigurable.__init__(self, **kw)
246 246
247 247 # While scrolling the pager on Mac OS X, it tears badly. The
248 248 # NativeGesture is platform and perhaps build-specific hence
249 249 # we take adequate precautions here.
250 250 self._pager_scroll_events = [QtCore.QEvent.Wheel]
251 251 if hasattr(QtCore.QEvent, 'NativeGesture'):
252 252 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
253 253
254 254 # Create the layout and underlying text widget.
255 255 layout = QtGui.QStackedLayout(self)
256 256 layout.setContentsMargins(0, 0, 0, 0)
257 257 self._control = self._create_control()
258 258 if self.paging in ('hsplit', 'vsplit'):
259 259 self._splitter = QtGui.QSplitter()
260 260 if self.paging == 'hsplit':
261 261 self._splitter.setOrientation(QtCore.Qt.Horizontal)
262 262 else:
263 263 self._splitter.setOrientation(QtCore.Qt.Vertical)
264 264 self._splitter.addWidget(self._control)
265 265 layout.addWidget(self._splitter)
266 266 else:
267 267 layout.addWidget(self._control)
268 268
269 269 # Create the paging widget, if necessary.
270 270 if self.paging in ('inside', 'hsplit', 'vsplit'):
271 271 self._page_control = self._create_page_control()
272 272 if self._splitter:
273 273 self._page_control.hide()
274 274 self._splitter.addWidget(self._page_control)
275 275 else:
276 276 layout.addWidget(self._page_control)
277 277
278 278 # Initialize protected variables. Some variables contain useful state
279 279 # information for subclasses; they should be considered read-only.
280 280 self._append_before_prompt_pos = 0
281 281 self._ansi_processor = QtAnsiCodeProcessor()
282 282 if self.gui_completion == 'ncurses':
283 283 self._completion_widget = CompletionHtml(self)
284 284 elif self.gui_completion == 'droplist':
285 285 self._completion_widget = CompletionWidget(self)
286 286 elif self.gui_completion == 'plain':
287 287 self._completion_widget = CompletionPlain(self)
288 288
289 289 self._continuation_prompt = '> '
290 290 self._continuation_prompt_html = None
291 291 self._executing = False
292 292 self._filter_resize = False
293 293 self._html_exporter = HtmlExporter(self._control)
294 294 self._input_buffer_executing = ''
295 295 self._input_buffer_pending = ''
296 296 self._kill_ring = QtKillRing(self._control)
297 297 self._prompt = ''
298 298 self._prompt_html = None
299 299 self._prompt_pos = 0
300 300 self._prompt_sep = ''
301 301 self._reading = False
302 302 self._reading_callback = None
303 303 self._tab_width = 8
304 304
305 305 # List of strings pending to be appended as plain text in the widget.
306 306 # The text is not immediately inserted when available to not
307 307 # choke the Qt event loop with paint events for the widget in
308 308 # case of lots of output from kernel.
309 309 self._pending_insert_text = []
310 310
311 311 # Timer to flush the pending stream messages. The interval is adjusted
312 312 # later based on actual time taken for flushing a screen (buffer_size)
313 313 # of output text.
314 314 self._pending_text_flush_interval = QtCore.QTimer(self._control)
315 315 self._pending_text_flush_interval.setInterval(100)
316 316 self._pending_text_flush_interval.setSingleShot(True)
317 317 self._pending_text_flush_interval.timeout.connect(
318 318 self._on_flush_pending_stream_timer)
319 319
320 320 # Set a monospaced font.
321 321 self.reset_font()
322 322
323 323 # Configure actions.
324 324 action = QtGui.QAction('Print', None)
325 325 action.setEnabled(True)
326 326 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
327 327 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
328 328 # Only override the default if there is a collision.
329 329 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
330 330 printkey = "Ctrl+Shift+P"
331 331 action.setShortcut(printkey)
332 332 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
333 333 action.triggered.connect(self.print_)
334 334 self.addAction(action)
335 335 self.print_action = action
336 336
337 337 action = QtGui.QAction('Save as HTML/XML', None)
338 338 action.setShortcut(QtGui.QKeySequence.Save)
339 339 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
340 340 action.triggered.connect(self.export_html)
341 341 self.addAction(action)
342 342 self.export_action = action
343 343
344 344 action = QtGui.QAction('Select All', None)
345 345 action.setEnabled(True)
346 346 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
347 347 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
348 348 # Only override the default if there is a collision.
349 349 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
350 350 selectall = "Ctrl+Shift+A"
351 351 action.setShortcut(selectall)
352 352 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
353 353 action.triggered.connect(self.select_all)
354 354 self.addAction(action)
355 355 self.select_all_action = action
356 356
357 357 self.increase_font_size = QtGui.QAction("Bigger Font",
358 358 self,
359 359 shortcut=QtGui.QKeySequence.ZoomIn,
360 360 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
361 361 statusTip="Increase the font size by one point",
362 362 triggered=self._increase_font_size)
363 363 self.addAction(self.increase_font_size)
364 364
365 365 self.decrease_font_size = QtGui.QAction("Smaller Font",
366 366 self,
367 367 shortcut=QtGui.QKeySequence.ZoomOut,
368 368 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
369 369 statusTip="Decrease the font size by one point",
370 370 triggered=self._decrease_font_size)
371 371 self.addAction(self.decrease_font_size)
372 372
373 373 self.reset_font_size = QtGui.QAction("Normal Font",
374 374 self,
375 375 shortcut="Ctrl+0",
376 376 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
377 377 statusTip="Restore the Normal font size",
378 378 triggered=self.reset_font)
379 379 self.addAction(self.reset_font_size)
380 380
381 381 # Accept drag and drop events here. Drops were already turned off
382 382 # in self._control when that widget was created.
383 383 self.setAcceptDrops(True)
384 384
385 385 #---------------------------------------------------------------------------
386 386 # Drag and drop support
387 387 #---------------------------------------------------------------------------
388 388
389 389 def dragEnterEvent(self, e):
390 390 if e.mimeData().hasUrls():
391 391 # The link action should indicate to that the drop will insert
392 392 # the file anme.
393 393 e.setDropAction(QtCore.Qt.LinkAction)
394 394 e.accept()
395 395 elif e.mimeData().hasText():
396 396 # By changing the action to copy we don't need to worry about
397 397 # the user accidentally moving text around in the widget.
398 398 e.setDropAction(QtCore.Qt.CopyAction)
399 399 e.accept()
400 400
401 401 def dragMoveEvent(self, e):
402 402 if e.mimeData().hasUrls():
403 403 pass
404 404 elif e.mimeData().hasText():
405 405 cursor = self._control.cursorForPosition(e.pos())
406 406 if self._in_buffer(cursor.position()):
407 407 e.setDropAction(QtCore.Qt.CopyAction)
408 408 self._control.setTextCursor(cursor)
409 409 else:
410 410 e.setDropAction(QtCore.Qt.IgnoreAction)
411 411 e.accept()
412 412
413 413 def dropEvent(self, e):
414 414 if e.mimeData().hasUrls():
415 415 self._keep_cursor_in_buffer()
416 416 cursor = self._control.textCursor()
417 417 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
418 418 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
419 419 for f in filenames)
420 420 self._insert_plain_text_into_buffer(cursor, text)
421 421 elif e.mimeData().hasText():
422 422 cursor = self._control.cursorForPosition(e.pos())
423 423 if self._in_buffer(cursor.position()):
424 424 text = e.mimeData().text()
425 425 self._insert_plain_text_into_buffer(cursor, text)
426 426
427 427 def eventFilter(self, obj, event):
428 428 """ Reimplemented to ensure a console-like behavior in the underlying
429 429 text widgets.
430 430 """
431 431 etype = event.type()
432 432 if etype == QtCore.QEvent.KeyPress:
433 433
434 434 # Re-map keys for all filtered widgets.
435 435 key = event.key()
436 436 if self._control_key_down(event.modifiers()) and \
437 437 key in self._ctrl_down_remap:
438 438 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
439 439 self._ctrl_down_remap[key],
440 440 QtCore.Qt.NoModifier)
441 441 QtGui.qApp.sendEvent(obj, new_event)
442 442 return True
443 443
444 444 elif obj == self._control:
445 445 return self._event_filter_console_keypress(event)
446 446
447 447 elif obj == self._page_control:
448 448 return self._event_filter_page_keypress(event)
449 449
450 450 # Make middle-click paste safe.
451 451 elif etype == QtCore.QEvent.MouseButtonRelease and \
452 452 event.button() == QtCore.Qt.MidButton and \
453 453 obj == self._control.viewport():
454 454 cursor = self._control.cursorForPosition(event.pos())
455 455 self._control.setTextCursor(cursor)
456 456 self.paste(QtGui.QClipboard.Selection)
457 457 return True
458 458
459 459 # Manually adjust the scrollbars *after* a resize event is dispatched.
460 460 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
461 461 self._filter_resize = True
462 462 QtGui.qApp.sendEvent(obj, event)
463 463 self._adjust_scrollbars()
464 464 self._filter_resize = False
465 465 return True
466 466
467 467 # Override shortcuts for all filtered widgets.
468 468 elif etype == QtCore.QEvent.ShortcutOverride and \
469 469 self.override_shortcuts and \
470 470 self._control_key_down(event.modifiers()) and \
471 471 event.key() in self._shortcuts:
472 472 event.accept()
473 473
474 474 # Handle scrolling of the vsplit pager. This hack attempts to solve
475 475 # problems with tearing of the help text inside the pager window. This
476 476 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
477 477 # perfect but makes the pager more usable.
478 478 elif etype in self._pager_scroll_events and \
479 479 obj == self._page_control:
480 480 self._page_control.repaint()
481 481 return True
482 482
483 483 elif etype == QtCore.QEvent.MouseMove:
484 484 anchor = self._control.anchorAt(event.pos())
485 485 QtGui.QToolTip.showText(event.globalPos(), anchor)
486 486
487 487 return super(ConsoleWidget, self).eventFilter(obj, event)
488 488
489 489 #---------------------------------------------------------------------------
490 490 # 'QWidget' interface
491 491 #---------------------------------------------------------------------------
492 492
493 493 def sizeHint(self):
494 494 """ Reimplemented to suggest a size that is 80 characters wide and
495 495 25 lines high.
496 496 """
497 497 font_metrics = QtGui.QFontMetrics(self.font)
498 498 margin = (self._control.frameWidth() +
499 499 self._control.document().documentMargin()) * 2
500 500 style = self.style()
501 501 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
502 502
503 503 # Note 1: Despite my best efforts to take the various margins into
504 504 # account, the width is still coming out a bit too small, so we include
505 505 # a fudge factor of one character here.
506 506 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
507 507 # to a Qt bug on certain Mac OS systems where it returns 0.
508 508 width = font_metrics.width(' ') * self.width + margin
509 509 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
510 510 if self.paging == 'hsplit':
511 511 width = width * 2 + splitwidth
512 512
513 513 height = font_metrics.height() * self.height + margin
514 514 if self.paging == 'vsplit':
515 515 height = height * 2 + splitwidth
516 516
517 517 return QtCore.QSize(width, height)
518 518
519 519 #---------------------------------------------------------------------------
520 520 # 'ConsoleWidget' public interface
521 521 #---------------------------------------------------------------------------
522
522
523 include_other_output = Bool(False, config=True,
524 help="""Whether to include output from clients
525 other than this one sharing the same kernel.
526
527 Outputs are not displayed until enter is pressed.
528 """
529 )
530
523 531 def can_copy(self):
524 532 """ Returns whether text can be copied to the clipboard.
525 533 """
526 534 return self._control.textCursor().hasSelection()
527 535
528 536 def can_cut(self):
529 537 """ Returns whether text can be cut to the clipboard.
530 538 """
531 539 cursor = self._control.textCursor()
532 540 return (cursor.hasSelection() and
533 541 self._in_buffer(cursor.anchor()) and
534 542 self._in_buffer(cursor.position()))
535 543
536 544 def can_paste(self):
537 545 """ Returns whether text can be pasted from the clipboard.
538 546 """
539 547 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
540 548 return bool(QtGui.QApplication.clipboard().text())
541 549 return False
542 550
543 551 def clear(self, keep_input=True):
544 552 """ Clear the console.
545 553
546 554 Parameters
547 555 ----------
548 556 keep_input : bool, optional (default True)
549 557 If set, restores the old input buffer if a new prompt is written.
550 558 """
551 559 if self._executing:
552 560 self._control.clear()
553 561 else:
554 562 if keep_input:
555 563 input_buffer = self.input_buffer
556 564 self._control.clear()
557 565 self._show_prompt()
558 566 if keep_input:
559 567 self.input_buffer = input_buffer
560 568
561 569 def copy(self):
562 570 """ Copy the currently selected text to the clipboard.
563 571 """
564 572 self.layout().currentWidget().copy()
565 573
566 574 def copy_anchor(self, anchor):
567 575 """ Copy anchor text to the clipboard
568 576 """
569 577 QtGui.QApplication.clipboard().setText(anchor)
570 578
571 579 def cut(self):
572 580 """ Copy the currently selected text to the clipboard and delete it
573 581 if it's inside the input buffer.
574 582 """
575 583 self.copy()
576 584 if self.can_cut():
577 585 self._control.textCursor().removeSelectedText()
578 586
579 587 def execute(self, source=None, hidden=False, interactive=False):
580 588 """ Executes source or the input buffer, possibly prompting for more
581 589 input.
582 590
583 591 Parameters
584 592 ----------
585 593 source : str, optional
586 594
587 595 The source to execute. If not specified, the input buffer will be
588 596 used. If specified and 'hidden' is False, the input buffer will be
589 597 replaced with the source before execution.
590 598
591 599 hidden : bool, optional (default False)
592 600
593 601 If set, no output will be shown and the prompt will not be modified.
594 602 In other words, it will be completely invisible to the user that
595 603 an execution has occurred.
596 604
597 605 interactive : bool, optional (default False)
598 606
599 607 Whether the console is to treat the source as having been manually
600 608 entered by the user. The effect of this parameter depends on the
601 609 subclass implementation.
602 610
603 611 Raises
604 612 ------
605 613 RuntimeError
606 614 If incomplete input is given and 'hidden' is True. In this case,
607 615 it is not possible to prompt for more input.
608 616
609 617 Returns
610 618 -------
611 619 A boolean indicating whether the source was executed.
612 620 """
613 621 # WARNING: The order in which things happen here is very particular, in
614 622 # large part because our syntax highlighting is fragile. If you change
615 623 # something, test carefully!
616 624
617 625 # Decide what to execute.
618 626 if source is None:
619 627 source = self.input_buffer
620 628 if not hidden:
621 629 # A newline is appended later, but it should be considered part
622 630 # of the input buffer.
623 631 source += '\n'
624 632 elif not hidden:
625 633 self.input_buffer = source
626 634
627 635 # Execute the source or show a continuation prompt if it is incomplete.
628 636 if self.execute_on_complete_input:
629 637 complete = self._is_complete(source, interactive)
630 638 else:
631 639 complete = not interactive
632 640 if hidden:
633 641 if complete or not self.execute_on_complete_input:
634 642 self._execute(source, hidden)
635 643 else:
636 644 error = 'Incomplete noninteractive input: "%s"'
637 645 raise RuntimeError(error % source)
638 646 else:
639 647 if complete:
640 648 self._append_plain_text('\n')
641 649 self._input_buffer_executing = self.input_buffer
642 650 self._executing = True
643 651 self._prompt_finished()
644 652
645 653 # The maximum block count is only in effect during execution.
646 654 # This ensures that _prompt_pos does not become invalid due to
647 655 # text truncation.
648 656 self._control.document().setMaximumBlockCount(self.buffer_size)
649 657
650 658 # Setting a positive maximum block count will automatically
651 659 # disable the undo/redo history, but just to be safe:
652 660 self._control.setUndoRedoEnabled(False)
653 661
654 662 # Perform actual execution.
655 663 self._execute(source, hidden)
656 664
657 665 else:
658 666 # Do this inside an edit block so continuation prompts are
659 667 # removed seamlessly via undo/redo.
660 668 cursor = self._get_end_cursor()
661 669 cursor.beginEditBlock()
662 670 cursor.insertText('\n')
663 671 self._insert_continuation_prompt(cursor)
664 672 cursor.endEditBlock()
665 673
666 674 # Do not do this inside the edit block. It works as expected
667 675 # when using a QPlainTextEdit control, but does not have an
668 676 # effect when using a QTextEdit. I believe this is a Qt bug.
669 677 self._control.moveCursor(QtGui.QTextCursor.End)
670 678
671 679 return complete
672 680
673 681 def export_html(self):
674 682 """ Shows a dialog to export HTML/XML in various formats.
675 683 """
676 684 self._html_exporter.export()
677 685
678 686 def _get_input_buffer(self, force=False):
679 687 """ The text that the user has entered entered at the current prompt.
680 688
681 689 If the console is currently executing, the text that is executing will
682 690 always be returned.
683 691 """
684 692 # If we're executing, the input buffer may not even exist anymore due to
685 693 # the limit imposed by 'buffer_size'. Therefore, we store it.
686 694 if self._executing and not force:
687 695 return self._input_buffer_executing
688 696
689 697 cursor = self._get_end_cursor()
690 698 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
691 699 input_buffer = cursor.selection().toPlainText()
692 700
693 701 # Strip out continuation prompts.
694 702 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
695 703
696 704 def _set_input_buffer(self, string):
697 705 """ Sets the text in the input buffer.
698 706
699 707 If the console is currently executing, this call has no *immediate*
700 708 effect. When the execution is finished, the input buffer will be updated
701 709 appropriately.
702 710 """
703 711 # If we're executing, store the text for later.
704 712 if self._executing:
705 713 self._input_buffer_pending = string
706 714 return
707 715
708 716 # Remove old text.
709 717 cursor = self._get_end_cursor()
710 718 cursor.beginEditBlock()
711 719 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
712 720 cursor.removeSelectedText()
713 721
714 722 # Insert new text with continuation prompts.
715 723 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
716 724 cursor.endEditBlock()
717 725 self._control.moveCursor(QtGui.QTextCursor.End)
718 726
719 727 input_buffer = property(_get_input_buffer, _set_input_buffer)
720 728
721 729 def _get_font(self):
722 730 """ The base font being used by the ConsoleWidget.
723 731 """
724 732 return self._control.document().defaultFont()
725 733
726 734 def _set_font(self, font):
727 735 """ Sets the base font for the ConsoleWidget to the specified QFont.
728 736 """
729 737 font_metrics = QtGui.QFontMetrics(font)
730 738 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
731 739
732 740 self._completion_widget.setFont(font)
733 741 self._control.document().setDefaultFont(font)
734 742 if self._page_control:
735 743 self._page_control.document().setDefaultFont(font)
736 744
737 745 self.font_changed.emit(font)
738 746
739 747 font = property(_get_font, _set_font)
740 748
741 749 def open_anchor(self, anchor):
742 750 """ Open selected anchor in the default webbrowser
743 751 """
744 752 webbrowser.open( anchor )
745 753
746 754 def paste(self, mode=QtGui.QClipboard.Clipboard):
747 755 """ Paste the contents of the clipboard into the input region.
748 756
749 757 Parameters
750 758 ----------
751 759 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
752 760
753 761 Controls which part of the system clipboard is used. This can be
754 762 used to access the selection clipboard in X11 and the Find buffer
755 763 in Mac OS. By default, the regular clipboard is used.
756 764 """
757 765 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
758 766 # Make sure the paste is safe.
759 767 self._keep_cursor_in_buffer()
760 768 cursor = self._control.textCursor()
761 769
762 770 # Remove any trailing newline, which confuses the GUI and forces the
763 771 # user to backspace.
764 772 text = QtGui.QApplication.clipboard().text(mode).rstrip()
765 773 self._insert_plain_text_into_buffer(cursor, dedent(text))
766 774
767 775 def print_(self, printer = None):
768 776 """ Print the contents of the ConsoleWidget to the specified QPrinter.
769 777 """
770 778 if (not printer):
771 779 printer = QtGui.QPrinter()
772 780 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
773 781 return
774 782 self._control.print_(printer)
775 783
776 784 def prompt_to_top(self):
777 785 """ Moves the prompt to the top of the viewport.
778 786 """
779 787 if not self._executing:
780 788 prompt_cursor = self._get_prompt_cursor()
781 789 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
782 790 self._set_cursor(prompt_cursor)
783 791 self._set_top_cursor(prompt_cursor)
784 792
785 793 def redo(self):
786 794 """ Redo the last operation. If there is no operation to redo, nothing
787 795 happens.
788 796 """
789 797 self._control.redo()
790 798
791 799 def reset_font(self):
792 800 """ Sets the font to the default fixed-width font for this platform.
793 801 """
794 802 if sys.platform == 'win32':
795 803 # Consolas ships with Vista/Win7, fallback to Courier if needed
796 804 fallback = 'Courier'
797 805 elif sys.platform == 'darwin':
798 806 # OSX always has Monaco
799 807 fallback = 'Monaco'
800 808 else:
801 809 # Monospace should always exist
802 810 fallback = 'Monospace'
803 811 font = get_font(self.font_family, fallback)
804 812 if self.font_size:
805 813 font.setPointSize(self.font_size)
806 814 else:
807 815 font.setPointSize(QtGui.qApp.font().pointSize())
808 816 font.setStyleHint(QtGui.QFont.TypeWriter)
809 817 self._set_font(font)
810 818
811 819 def change_font_size(self, delta):
812 820 """Change the font size by the specified amount (in points).
813 821 """
814 822 font = self.font
815 823 size = max(font.pointSize() + delta, 1) # minimum 1 point
816 824 font.setPointSize(size)
817 825 self._set_font(font)
818 826
819 827 def _increase_font_size(self):
820 828 self.change_font_size(1)
821 829
822 830 def _decrease_font_size(self):
823 831 self.change_font_size(-1)
824 832
825 833 def select_all(self):
826 834 """ Selects all the text in the buffer.
827 835 """
828 836 self._control.selectAll()
829 837
830 838 def _get_tab_width(self):
831 839 """ The width (in terms of space characters) for tab characters.
832 840 """
833 841 return self._tab_width
834 842
835 843 def _set_tab_width(self, tab_width):
836 844 """ Sets the width (in terms of space characters) for tab characters.
837 845 """
838 846 font_metrics = QtGui.QFontMetrics(self.font)
839 847 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
840 848
841 849 self._tab_width = tab_width
842 850
843 851 tab_width = property(_get_tab_width, _set_tab_width)
844 852
845 853 def undo(self):
846 854 """ Undo the last operation. If there is no operation to undo, nothing
847 855 happens.
848 856 """
849 857 self._control.undo()
850 858
851 859 #---------------------------------------------------------------------------
852 860 # 'ConsoleWidget' abstract interface
853 861 #---------------------------------------------------------------------------
854 862
855 863 def _is_complete(self, source, interactive):
856 864 """ Returns whether 'source' can be executed. When triggered by an
857 865 Enter/Return key press, 'interactive' is True; otherwise, it is
858 866 False.
859 867 """
860 868 raise NotImplementedError
861 869
862 870 def _execute(self, source, hidden):
863 871 """ Execute 'source'. If 'hidden', do not show any output.
864 872 """
865 873 raise NotImplementedError
866 874
867 875 def _prompt_started_hook(self):
868 876 """ Called immediately after a new prompt is displayed.
869 877 """
870 878 pass
871 879
872 880 def _prompt_finished_hook(self):
873 881 """ Called immediately after a prompt is finished, i.e. when some input
874 882 will be processed and a new prompt displayed.
875 883 """
876 884 pass
877 885
878 886 def _up_pressed(self, shift_modifier):
879 887 """ Called when the up key is pressed. Returns whether to continue
880 888 processing the event.
881 889 """
882 890 return True
883 891
884 892 def _down_pressed(self, shift_modifier):
885 893 """ Called when the down key is pressed. Returns whether to continue
886 894 processing the event.
887 895 """
888 896 return True
889 897
890 898 def _tab_pressed(self):
891 899 """ Called when the tab key is pressed. Returns whether to continue
892 900 processing the event.
893 901 """
894 902 return False
895 903
896 904 #--------------------------------------------------------------------------
897 905 # 'ConsoleWidget' protected interface
898 906 #--------------------------------------------------------------------------
899 907
900 908 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
901 909 """ A low-level method for appending content to the end of the buffer.
902 910
903 911 If 'before_prompt' is enabled, the content will be inserted before the
904 912 current prompt, if there is one.
905 913 """
906 914 # Determine where to insert the content.
907 915 cursor = self._control.textCursor()
908 916 if before_prompt and (self._reading or not self._executing):
909 917 self._flush_pending_stream()
910 918 cursor.setPosition(self._append_before_prompt_pos)
911 919 else:
912 920 if insert != self._insert_plain_text:
913 921 self._flush_pending_stream()
914 922 cursor.movePosition(QtGui.QTextCursor.End)
915 923 start_pos = cursor.position()
916 924
917 925 # Perform the insertion.
918 926 result = insert(cursor, input, *args, **kwargs)
919 927
920 928 # Adjust the prompt position if we have inserted before it. This is safe
921 929 # because buffer truncation is disabled when not executing.
922 930 if before_prompt and (self._reading or not self._executing):
923 931 diff = cursor.position() - start_pos
924 932 self._append_before_prompt_pos += diff
925 933 self._prompt_pos += diff
926 934
927 935 return result
928 936
929 937 def _append_block(self, block_format=None, before_prompt=False):
930 938 """ Appends an new QTextBlock to the end of the console buffer.
931 939 """
932 940 self._append_custom(self._insert_block, block_format, before_prompt)
933 941
934 942 def _append_html(self, html, before_prompt=False):
935 943 """ Appends HTML at the end of the console buffer.
936 944 """
937 945 self._append_custom(self._insert_html, html, before_prompt)
938 946
939 947 def _append_html_fetching_plain_text(self, html, before_prompt=False):
940 948 """ Appends HTML, then returns the plain text version of it.
941 949 """
942 950 return self._append_custom(self._insert_html_fetching_plain_text,
943 951 html, before_prompt)
944 952
945 953 def _append_plain_text(self, text, before_prompt=False):
946 954 """ Appends plain text, processing ANSI codes if enabled.
947 955 """
948 956 self._append_custom(self._insert_plain_text, text, before_prompt)
949 957
950 958 def _cancel_completion(self):
951 959 """ If text completion is progress, cancel it.
952 960 """
953 961 self._completion_widget.cancel_completion()
954 962
955 963 def _clear_temporary_buffer(self):
956 964 """ Clears the "temporary text" buffer, i.e. all the text following
957 965 the prompt region.
958 966 """
959 967 # Select and remove all text below the input buffer.
960 968 cursor = self._get_prompt_cursor()
961 969 prompt = self._continuation_prompt.lstrip()
962 970 if(self._temp_buffer_filled):
963 971 self._temp_buffer_filled = False
964 972 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
965 973 temp_cursor = QtGui.QTextCursor(cursor)
966 974 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
967 975 text = temp_cursor.selection().toPlainText().lstrip()
968 976 if not text.startswith(prompt):
969 977 break
970 978 else:
971 979 # We've reached the end of the input buffer and no text follows.
972 980 return
973 981 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
974 982 cursor.movePosition(QtGui.QTextCursor.End,
975 983 QtGui.QTextCursor.KeepAnchor)
976 984 cursor.removeSelectedText()
977 985
978 986 # After doing this, we have no choice but to clear the undo/redo
979 987 # history. Otherwise, the text is not "temporary" at all, because it
980 988 # can be recalled with undo/redo. Unfortunately, Qt does not expose
981 989 # fine-grained control to the undo/redo system.
982 990 if self._control.isUndoRedoEnabled():
983 991 self._control.setUndoRedoEnabled(False)
984 992 self._control.setUndoRedoEnabled(True)
985 993
986 994 def _complete_with_items(self, cursor, items):
987 995 """ Performs completion with 'items' at the specified cursor location.
988 996 """
989 997 self._cancel_completion()
990 998
991 999 if len(items) == 1:
992 1000 cursor.setPosition(self._control.textCursor().position(),
993 1001 QtGui.QTextCursor.KeepAnchor)
994 1002 cursor.insertText(items[0])
995 1003
996 1004 elif len(items) > 1:
997 1005 current_pos = self._control.textCursor().position()
998 1006 prefix = commonprefix(items)
999 1007 if prefix:
1000 1008 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1001 1009 cursor.insertText(prefix)
1002 1010 current_pos = cursor.position()
1003 1011
1004 1012 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1005 1013 self._completion_widget.show_items(cursor, items)
1006 1014
1007 1015
1008 1016 def _fill_temporary_buffer(self, cursor, text, html=False):
1009 1017 """fill the area below the active editting zone with text"""
1010 1018
1011 1019 current_pos = self._control.textCursor().position()
1012 1020
1013 1021 cursor.beginEditBlock()
1014 1022 self._append_plain_text('\n')
1015 1023 self._page(text, html=html)
1016 1024 cursor.endEditBlock()
1017 1025
1018 1026 cursor.setPosition(current_pos)
1019 1027 self._control.moveCursor(QtGui.QTextCursor.End)
1020 1028 self._control.setTextCursor(cursor)
1021 1029
1022 1030 self._temp_buffer_filled = True
1023 1031
1024 1032
1025 1033 def _context_menu_make(self, pos):
1026 1034 """ Creates a context menu for the given QPoint (in widget coordinates).
1027 1035 """
1028 1036 menu = QtGui.QMenu(self)
1029 1037
1030 1038 self.cut_action = menu.addAction('Cut', self.cut)
1031 1039 self.cut_action.setEnabled(self.can_cut())
1032 1040 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1033 1041
1034 1042 self.copy_action = menu.addAction('Copy', self.copy)
1035 1043 self.copy_action.setEnabled(self.can_copy())
1036 1044 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1037 1045
1038 1046 self.paste_action = menu.addAction('Paste', self.paste)
1039 1047 self.paste_action.setEnabled(self.can_paste())
1040 1048 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1041 1049
1042 1050 anchor = self._control.anchorAt(pos)
1043 1051 if anchor:
1044 1052 menu.addSeparator()
1045 1053 self.copy_link_action = menu.addAction(
1046 1054 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1047 1055 self.open_link_action = menu.addAction(
1048 1056 'Open Link', lambda: self.open_anchor(anchor=anchor))
1049 1057
1050 1058 menu.addSeparator()
1051 1059 menu.addAction(self.select_all_action)
1052 1060
1053 1061 menu.addSeparator()
1054 1062 menu.addAction(self.export_action)
1055 1063 menu.addAction(self.print_action)
1056 1064
1057 1065 return menu
1058 1066
1059 1067 def _control_key_down(self, modifiers, include_command=False):
1060 1068 """ Given a KeyboardModifiers flags object, return whether the Control
1061 1069 key is down.
1062 1070
1063 1071 Parameters
1064 1072 ----------
1065 1073 include_command : bool, optional (default True)
1066 1074 Whether to treat the Command key as a (mutually exclusive) synonym
1067 1075 for Control when in Mac OS.
1068 1076 """
1069 1077 # Note that on Mac OS, ControlModifier corresponds to the Command key
1070 1078 # while MetaModifier corresponds to the Control key.
1071 1079 if sys.platform == 'darwin':
1072 1080 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1073 1081 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1074 1082 else:
1075 1083 return bool(modifiers & QtCore.Qt.ControlModifier)
1076 1084
1077 1085 def _create_control(self):
1078 1086 """ Creates and connects the underlying text widget.
1079 1087 """
1080 1088 # Create the underlying control.
1081 1089 if self.custom_control:
1082 1090 control = self.custom_control()
1083 1091 elif self.kind == 'plain':
1084 1092 control = QtGui.QPlainTextEdit()
1085 1093 elif self.kind == 'rich':
1086 1094 control = QtGui.QTextEdit()
1087 1095 control.setAcceptRichText(False)
1088 1096 control.setMouseTracking(True)
1089 1097
1090 1098 # Prevent the widget from handling drops, as we already provide
1091 1099 # the logic in this class.
1092 1100 control.setAcceptDrops(False)
1093 1101
1094 1102 # Install event filters. The filter on the viewport is needed for
1095 1103 # mouse events.
1096 1104 control.installEventFilter(self)
1097 1105 control.viewport().installEventFilter(self)
1098 1106
1099 1107 # Connect signals.
1100 1108 control.customContextMenuRequested.connect(
1101 1109 self._custom_context_menu_requested)
1102 1110 control.copyAvailable.connect(self.copy_available)
1103 1111 control.redoAvailable.connect(self.redo_available)
1104 1112 control.undoAvailable.connect(self.undo_available)
1105 1113
1106 1114 # Hijack the document size change signal to prevent Qt from adjusting
1107 1115 # the viewport's scrollbar. We are relying on an implementation detail
1108 1116 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1109 1117 # this functionality we cannot create a nice terminal interface.
1110 1118 layout = control.document().documentLayout()
1111 1119 layout.documentSizeChanged.disconnect()
1112 1120 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1113 1121
1114 1122 # Configure the control.
1115 1123 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1116 1124 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1117 1125 control.setReadOnly(True)
1118 1126 control.setUndoRedoEnabled(False)
1119 1127 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1120 1128 return control
1121 1129
1122 1130 def _create_page_control(self):
1123 1131 """ Creates and connects the underlying paging widget.
1124 1132 """
1125 1133 if self.custom_page_control:
1126 1134 control = self.custom_page_control()
1127 1135 elif self.kind == 'plain':
1128 1136 control = QtGui.QPlainTextEdit()
1129 1137 elif self.kind == 'rich':
1130 1138 control = QtGui.QTextEdit()
1131 1139 control.installEventFilter(self)
1132 1140 viewport = control.viewport()
1133 1141 viewport.installEventFilter(self)
1134 1142 control.setReadOnly(True)
1135 1143 control.setUndoRedoEnabled(False)
1136 1144 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1137 1145 return control
1138 1146
1139 1147 def _event_filter_console_keypress(self, event):
1140 1148 """ Filter key events for the underlying text widget to create a
1141 1149 console-like interface.
1142 1150 """
1143 1151 intercepted = False
1144 1152 cursor = self._control.textCursor()
1145 1153 position = cursor.position()
1146 1154 key = event.key()
1147 1155 ctrl_down = self._control_key_down(event.modifiers())
1148 1156 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1149 1157 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1150 1158
1151 1159 #------ Special sequences ----------------------------------------------
1152 1160
1153 1161 if event.matches(QtGui.QKeySequence.Copy):
1154 1162 self.copy()
1155 1163 intercepted = True
1156 1164
1157 1165 elif event.matches(QtGui.QKeySequence.Cut):
1158 1166 self.cut()
1159 1167 intercepted = True
1160 1168
1161 1169 elif event.matches(QtGui.QKeySequence.Paste):
1162 1170 self.paste()
1163 1171 intercepted = True
1164 1172
1165 1173 #------ Special modifier logic -----------------------------------------
1166 1174
1167 1175 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1168 1176 intercepted = True
1169 1177
1170 1178 # Special handling when tab completing in text mode.
1171 1179 self._cancel_completion()
1172 1180
1173 1181 if self._in_buffer(position):
1174 1182 # Special handling when a reading a line of raw input.
1175 1183 if self._reading:
1176 1184 self._append_plain_text('\n')
1177 1185 self._reading = False
1178 1186 if self._reading_callback:
1179 1187 self._reading_callback()
1180 1188
1181 1189 # If the input buffer is a single line or there is only
1182 1190 # whitespace after the cursor, execute. Otherwise, split the
1183 1191 # line with a continuation prompt.
1184 1192 elif not self._executing:
1185 1193 cursor.movePosition(QtGui.QTextCursor.End,
1186 1194 QtGui.QTextCursor.KeepAnchor)
1187 1195 at_end = len(cursor.selectedText().strip()) == 0
1188 1196 single_line = (self._get_end_cursor().blockNumber() ==
1189 1197 self._get_prompt_cursor().blockNumber())
1190 1198 if (at_end or shift_down or single_line) and not ctrl_down:
1191 1199 self.execute(interactive = not shift_down)
1192 1200 else:
1193 1201 # Do this inside an edit block for clean undo/redo.
1194 1202 cursor.beginEditBlock()
1195 1203 cursor.setPosition(position)
1196 1204 cursor.insertText('\n')
1197 1205 self._insert_continuation_prompt(cursor)
1198 1206 cursor.endEditBlock()
1199 1207
1200 1208 # Ensure that the whole input buffer is visible.
1201 1209 # FIXME: This will not be usable if the input buffer is
1202 1210 # taller than the console widget.
1203 1211 self._control.moveCursor(QtGui.QTextCursor.End)
1204 1212 self._control.setTextCursor(cursor)
1205 1213
1206 1214 #------ Control/Cmd modifier -------------------------------------------
1207 1215
1208 1216 elif ctrl_down:
1209 1217 if key == QtCore.Qt.Key_G:
1210 1218 self._keyboard_quit()
1211 1219 intercepted = True
1212 1220
1213 1221 elif key == QtCore.Qt.Key_K:
1214 1222 if self._in_buffer(position):
1215 1223 cursor.clearSelection()
1216 1224 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1217 1225 QtGui.QTextCursor.KeepAnchor)
1218 1226 if not cursor.hasSelection():
1219 1227 # Line deletion (remove continuation prompt)
1220 1228 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1221 1229 QtGui.QTextCursor.KeepAnchor)
1222 1230 cursor.movePosition(QtGui.QTextCursor.Right,
1223 1231 QtGui.QTextCursor.KeepAnchor,
1224 1232 len(self._continuation_prompt))
1225 1233 self._kill_ring.kill_cursor(cursor)
1226 1234 self._set_cursor(cursor)
1227 1235 intercepted = True
1228 1236
1229 1237 elif key == QtCore.Qt.Key_L:
1230 1238 self.prompt_to_top()
1231 1239 intercepted = True
1232 1240
1233 1241 elif key == QtCore.Qt.Key_O:
1234 1242 if self._page_control and self._page_control.isVisible():
1235 1243 self._page_control.setFocus()
1236 1244 intercepted = True
1237 1245
1238 1246 elif key == QtCore.Qt.Key_U:
1239 1247 if self._in_buffer(position):
1240 1248 cursor.clearSelection()
1241 1249 start_line = cursor.blockNumber()
1242 1250 if start_line == self._get_prompt_cursor().blockNumber():
1243 1251 offset = len(self._prompt)
1244 1252 else:
1245 1253 offset = len(self._continuation_prompt)
1246 1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1247 1255 QtGui.QTextCursor.KeepAnchor)
1248 1256 cursor.movePosition(QtGui.QTextCursor.Right,
1249 1257 QtGui.QTextCursor.KeepAnchor, offset)
1250 1258 self._kill_ring.kill_cursor(cursor)
1251 1259 self._set_cursor(cursor)
1252 1260 intercepted = True
1253 1261
1254 1262 elif key == QtCore.Qt.Key_Y:
1255 1263 self._keep_cursor_in_buffer()
1256 1264 self._kill_ring.yank()
1257 1265 intercepted = True
1258 1266
1259 1267 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1260 1268 if key == QtCore.Qt.Key_Backspace:
1261 1269 cursor = self._get_word_start_cursor(position)
1262 1270 else: # key == QtCore.Qt.Key_Delete
1263 1271 cursor = self._get_word_end_cursor(position)
1264 1272 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1265 1273 self._kill_ring.kill_cursor(cursor)
1266 1274 intercepted = True
1267 1275
1268 1276 elif key == QtCore.Qt.Key_D:
1269 1277 if len(self.input_buffer) == 0:
1270 1278 self.exit_requested.emit(self)
1271 1279 else:
1272 1280 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1273 1281 QtCore.Qt.Key_Delete,
1274 1282 QtCore.Qt.NoModifier)
1275 1283 QtGui.qApp.sendEvent(self._control, new_event)
1276 1284 intercepted = True
1277 1285
1278 1286 #------ Alt modifier ---------------------------------------------------
1279 1287
1280 1288 elif alt_down:
1281 1289 if key == QtCore.Qt.Key_B:
1282 1290 self._set_cursor(self._get_word_start_cursor(position))
1283 1291 intercepted = True
1284 1292
1285 1293 elif key == QtCore.Qt.Key_F:
1286 1294 self._set_cursor(self._get_word_end_cursor(position))
1287 1295 intercepted = True
1288 1296
1289 1297 elif key == QtCore.Qt.Key_Y:
1290 1298 self._kill_ring.rotate()
1291 1299 intercepted = True
1292 1300
1293 1301 elif key == QtCore.Qt.Key_Backspace:
1294 1302 cursor = self._get_word_start_cursor(position)
1295 1303 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1296 1304 self._kill_ring.kill_cursor(cursor)
1297 1305 intercepted = True
1298 1306
1299 1307 elif key == QtCore.Qt.Key_D:
1300 1308 cursor = self._get_word_end_cursor(position)
1301 1309 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1302 1310 self._kill_ring.kill_cursor(cursor)
1303 1311 intercepted = True
1304 1312
1305 1313 elif key == QtCore.Qt.Key_Delete:
1306 1314 intercepted = True
1307 1315
1308 1316 elif key == QtCore.Qt.Key_Greater:
1309 1317 self._control.moveCursor(QtGui.QTextCursor.End)
1310 1318 intercepted = True
1311 1319
1312 1320 elif key == QtCore.Qt.Key_Less:
1313 1321 self._control.setTextCursor(self._get_prompt_cursor())
1314 1322 intercepted = True
1315 1323
1316 1324 #------ No modifiers ---------------------------------------------------
1317 1325
1318 1326 else:
1319 1327 if shift_down:
1320 1328 anchormode = QtGui.QTextCursor.KeepAnchor
1321 1329 else:
1322 1330 anchormode = QtGui.QTextCursor.MoveAnchor
1323 1331
1324 1332 if key == QtCore.Qt.Key_Escape:
1325 1333 self._keyboard_quit()
1326 1334 intercepted = True
1327 1335
1328 1336 elif key == QtCore.Qt.Key_Up:
1329 1337 if self._reading or not self._up_pressed(shift_down):
1330 1338 intercepted = True
1331 1339 else:
1332 1340 prompt_line = self._get_prompt_cursor().blockNumber()
1333 1341 intercepted = cursor.blockNumber() <= prompt_line
1334 1342
1335 1343 elif key == QtCore.Qt.Key_Down:
1336 1344 if self._reading or not self._down_pressed(shift_down):
1337 1345 intercepted = True
1338 1346 else:
1339 1347 end_line = self._get_end_cursor().blockNumber()
1340 1348 intercepted = cursor.blockNumber() == end_line
1341 1349
1342 1350 elif key == QtCore.Qt.Key_Tab:
1343 1351 if not self._reading:
1344 1352 if self._tab_pressed():
1345 1353 # real tab-key, insert four spaces
1346 1354 cursor.insertText(' '*4)
1347 1355 intercepted = True
1348 1356
1349 1357 elif key == QtCore.Qt.Key_Left:
1350 1358
1351 1359 # Move to the previous line
1352 1360 line, col = cursor.blockNumber(), cursor.columnNumber()
1353 1361 if line > self._get_prompt_cursor().blockNumber() and \
1354 1362 col == len(self._continuation_prompt):
1355 1363 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1356 1364 mode=anchormode)
1357 1365 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1358 1366 mode=anchormode)
1359 1367 intercepted = True
1360 1368
1361 1369 # Regular left movement
1362 1370 else:
1363 1371 intercepted = not self._in_buffer(position - 1)
1364 1372
1365 1373 elif key == QtCore.Qt.Key_Right:
1366 1374 original_block_number = cursor.blockNumber()
1367 1375 self._control.moveCursor(QtGui.QTextCursor.Right,
1368 1376 mode=anchormode)
1369 1377 if cursor.blockNumber() != original_block_number:
1370 1378 self._control.moveCursor(QtGui.QTextCursor.Right,
1371 1379 n=len(self._continuation_prompt),
1372 1380 mode=anchormode)
1373 1381 intercepted = True
1374 1382
1375 1383 elif key == QtCore.Qt.Key_Home:
1376 1384 start_line = cursor.blockNumber()
1377 1385 if start_line == self._get_prompt_cursor().blockNumber():
1378 1386 start_pos = self._prompt_pos
1379 1387 else:
1380 1388 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1381 1389 QtGui.QTextCursor.KeepAnchor)
1382 1390 start_pos = cursor.position()
1383 1391 start_pos += len(self._continuation_prompt)
1384 1392 cursor.setPosition(position)
1385 1393 if shift_down and self._in_buffer(position):
1386 1394 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1387 1395 else:
1388 1396 cursor.setPosition(start_pos)
1389 1397 self._set_cursor(cursor)
1390 1398 intercepted = True
1391 1399
1392 1400 elif key == QtCore.Qt.Key_Backspace:
1393 1401
1394 1402 # Line deletion (remove continuation prompt)
1395 1403 line, col = cursor.blockNumber(), cursor.columnNumber()
1396 1404 if not self._reading and \
1397 1405 col == len(self._continuation_prompt) and \
1398 1406 line > self._get_prompt_cursor().blockNumber():
1399 1407 cursor.beginEditBlock()
1400 1408 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1401 1409 QtGui.QTextCursor.KeepAnchor)
1402 1410 cursor.removeSelectedText()
1403 1411 cursor.deletePreviousChar()
1404 1412 cursor.endEditBlock()
1405 1413 intercepted = True
1406 1414
1407 1415 # Regular backwards deletion
1408 1416 else:
1409 1417 anchor = cursor.anchor()
1410 1418 if anchor == position:
1411 1419 intercepted = not self._in_buffer(position - 1)
1412 1420 else:
1413 1421 intercepted = not self._in_buffer(min(anchor, position))
1414 1422
1415 1423 elif key == QtCore.Qt.Key_Delete:
1416 1424
1417 1425 # Line deletion (remove continuation prompt)
1418 1426 if not self._reading and self._in_buffer(position) and \
1419 1427 cursor.atBlockEnd() and not cursor.hasSelection():
1420 1428 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1421 1429 QtGui.QTextCursor.KeepAnchor)
1422 1430 cursor.movePosition(QtGui.QTextCursor.Right,
1423 1431 QtGui.QTextCursor.KeepAnchor,
1424 1432 len(self._continuation_prompt))
1425 1433 cursor.removeSelectedText()
1426 1434 intercepted = True
1427 1435
1428 1436 # Regular forwards deletion:
1429 1437 else:
1430 1438 anchor = cursor.anchor()
1431 1439 intercepted = (not self._in_buffer(anchor) or
1432 1440 not self._in_buffer(position))
1433 1441
1434 1442 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1435 1443 # using the keyboard in any part of the buffer. Also, permit scrolling
1436 1444 # with Page Up/Down keys. Finally, if we're executing, don't move the
1437 1445 # cursor (if even this made sense, we can't guarantee that the prompt
1438 1446 # position is still valid due to text truncation).
1439 1447 if not (self._control_key_down(event.modifiers(), include_command=True)
1440 1448 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1441 1449 or (self._executing and not self._reading)):
1442 1450 self._keep_cursor_in_buffer()
1443 1451
1444 1452 return intercepted
1445 1453
1446 1454 def _event_filter_page_keypress(self, event):
1447 1455 """ Filter key events for the paging widget to create console-like
1448 1456 interface.
1449 1457 """
1450 1458 key = event.key()
1451 1459 ctrl_down = self._control_key_down(event.modifiers())
1452 1460 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1453 1461
1454 1462 if ctrl_down:
1455 1463 if key == QtCore.Qt.Key_O:
1456 1464 self._control.setFocus()
1457 1465 intercept = True
1458 1466
1459 1467 elif alt_down:
1460 1468 if key == QtCore.Qt.Key_Greater:
1461 1469 self._page_control.moveCursor(QtGui.QTextCursor.End)
1462 1470 intercepted = True
1463 1471
1464 1472 elif key == QtCore.Qt.Key_Less:
1465 1473 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1466 1474 intercepted = True
1467 1475
1468 1476 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1469 1477 if self._splitter:
1470 1478 self._page_control.hide()
1471 1479 self._control.setFocus()
1472 1480 else:
1473 1481 self.layout().setCurrentWidget(self._control)
1474 1482 # re-enable buffer truncation after paging
1475 1483 self._control.document().setMaximumBlockCount(self.buffer_size)
1476 1484 return True
1477 1485
1478 1486 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1479 1487 QtCore.Qt.Key_Tab):
1480 1488 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1481 1489 QtCore.Qt.Key_PageDown,
1482 1490 QtCore.Qt.NoModifier)
1483 1491 QtGui.qApp.sendEvent(self._page_control, new_event)
1484 1492 return True
1485 1493
1486 1494 elif key == QtCore.Qt.Key_Backspace:
1487 1495 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1488 1496 QtCore.Qt.Key_PageUp,
1489 1497 QtCore.Qt.NoModifier)
1490 1498 QtGui.qApp.sendEvent(self._page_control, new_event)
1491 1499 return True
1492 1500
1493 1501 # vi/less -like key bindings
1494 1502 elif key == QtCore.Qt.Key_J:
1495 1503 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1496 1504 QtCore.Qt.Key_Down,
1497 1505 QtCore.Qt.NoModifier)
1498 1506 QtGui.qApp.sendEvent(self._page_control, new_event)
1499 1507 return True
1500 1508
1501 1509 # vi/less -like key bindings
1502 1510 elif key == QtCore.Qt.Key_K:
1503 1511 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1504 1512 QtCore.Qt.Key_Up,
1505 1513 QtCore.Qt.NoModifier)
1506 1514 QtGui.qApp.sendEvent(self._page_control, new_event)
1507 1515 return True
1508 1516
1509 1517 return False
1510 1518
1511 1519 def _on_flush_pending_stream_timer(self):
1512 1520 """ Flush the pending stream output and change the
1513 1521 prompt position appropriately.
1514 1522 """
1515 1523 cursor = self._control.textCursor()
1516 1524 cursor.movePosition(QtGui.QTextCursor.End)
1517 1525 pos = cursor.position()
1518 1526 self._flush_pending_stream()
1519 1527 cursor.movePosition(QtGui.QTextCursor.End)
1520 1528 diff = cursor.position() - pos
1521 1529 if diff > 0:
1522 1530 self._prompt_pos += diff
1523 1531 self._append_before_prompt_pos += diff
1524 1532
1525 1533 def _flush_pending_stream(self):
1526 1534 """ Flush out pending text into the widget. """
1527 1535 text = self._pending_insert_text
1528 1536 self._pending_insert_text = []
1529 1537 buffer_size = self._control.document().maximumBlockCount()
1530 1538 if buffer_size > 0:
1531 1539 text = self._get_last_lines_from_list(text, buffer_size)
1532 1540 text = ''.join(text)
1533 1541 t = time.time()
1534 1542 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1535 1543 # Set the flush interval to equal the maximum time to update text.
1536 1544 self._pending_text_flush_interval.setInterval(max(100,
1537 1545 (time.time()-t)*1000))
1538 1546
1539 1547 def _format_as_columns(self, items, separator=' '):
1540 1548 """ Transform a list of strings into a single string with columns.
1541 1549
1542 1550 Parameters
1543 1551 ----------
1544 1552 items : sequence of strings
1545 1553 The strings to process.
1546 1554
1547 1555 separator : str, optional [default is two spaces]
1548 1556 The string that separates columns.
1549 1557
1550 1558 Returns
1551 1559 -------
1552 1560 The formatted string.
1553 1561 """
1554 1562 # Calculate the number of characters available.
1555 1563 width = self._control.viewport().width()
1556 1564 char_width = QtGui.QFontMetrics(self.font).width(' ')
1557 1565 displaywidth = max(10, (width / char_width) - 1)
1558 1566
1559 1567 return columnize(items, separator, displaywidth)
1560 1568
1561 1569 def _get_block_plain_text(self, block):
1562 1570 """ Given a QTextBlock, return its unformatted text.
1563 1571 """
1564 1572 cursor = QtGui.QTextCursor(block)
1565 1573 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1566 1574 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1567 1575 QtGui.QTextCursor.KeepAnchor)
1568 1576 return cursor.selection().toPlainText()
1569 1577
1570 1578 def _get_cursor(self):
1571 1579 """ Convenience method that returns a cursor for the current position.
1572 1580 """
1573 1581 return self._control.textCursor()
1574 1582
1575 1583 def _get_end_cursor(self):
1576 1584 """ Convenience method that returns a cursor for the last character.
1577 1585 """
1578 1586 cursor = self._control.textCursor()
1579 1587 cursor.movePosition(QtGui.QTextCursor.End)
1580 1588 return cursor
1581 1589
1582 1590 def _get_input_buffer_cursor_column(self):
1583 1591 """ Returns the column of the cursor in the input buffer, excluding the
1584 1592 contribution by the prompt, or -1 if there is no such column.
1585 1593 """
1586 1594 prompt = self._get_input_buffer_cursor_prompt()
1587 1595 if prompt is None:
1588 1596 return -1
1589 1597 else:
1590 1598 cursor = self._control.textCursor()
1591 1599 return cursor.columnNumber() - len(prompt)
1592 1600
1593 1601 def _get_input_buffer_cursor_line(self):
1594 1602 """ Returns the text of the line of the input buffer that contains the
1595 1603 cursor, or None if there is no such line.
1596 1604 """
1597 1605 prompt = self._get_input_buffer_cursor_prompt()
1598 1606 if prompt is None:
1599 1607 return None
1600 1608 else:
1601 1609 cursor = self._control.textCursor()
1602 1610 text = self._get_block_plain_text(cursor.block())
1603 1611 return text[len(prompt):]
1604 1612
1605 1613 def _get_input_buffer_cursor_pos(self):
1606 1614 """Return the cursor position within the input buffer."""
1607 1615 cursor = self._control.textCursor()
1608 1616 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1609 1617 input_buffer = cursor.selection().toPlainText()
1610 1618
1611 1619 # Don't count continuation prompts
1612 1620 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1613 1621
1614 1622 def _get_input_buffer_cursor_prompt(self):
1615 1623 """ Returns the (plain text) prompt for line of the input buffer that
1616 1624 contains the cursor, or None if there is no such line.
1617 1625 """
1618 1626 if self._executing:
1619 1627 return None
1620 1628 cursor = self._control.textCursor()
1621 1629 if cursor.position() >= self._prompt_pos:
1622 1630 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1623 1631 return self._prompt
1624 1632 else:
1625 1633 return self._continuation_prompt
1626 1634 else:
1627 1635 return None
1628 1636
1629 1637 def _get_last_lines(self, text, num_lines, return_count=False):
1630 1638 """ Return last specified number of lines of text (like `tail -n`).
1631 1639 If return_count is True, returns a tuple of clipped text and the
1632 1640 number of lines in the clipped text.
1633 1641 """
1634 1642 pos = len(text)
1635 1643 if pos < num_lines:
1636 1644 if return_count:
1637 1645 return text, text.count('\n') if return_count else text
1638 1646 else:
1639 1647 return text
1640 1648 i = 0
1641 1649 while i < num_lines:
1642 1650 pos = text.rfind('\n', None, pos)
1643 1651 if pos == -1:
1644 1652 pos = None
1645 1653 break
1646 1654 i += 1
1647 1655 if return_count:
1648 1656 return text[pos:], i
1649 1657 else:
1650 1658 return text[pos:]
1651 1659
1652 1660 def _get_last_lines_from_list(self, text_list, num_lines):
1653 1661 """ Return the list of text clipped to last specified lines.
1654 1662 """
1655 1663 ret = []
1656 1664 lines_pending = num_lines
1657 1665 for text in reversed(text_list):
1658 1666 text, lines_added = self._get_last_lines(text, lines_pending,
1659 1667 return_count=True)
1660 1668 ret.append(text)
1661 1669 lines_pending -= lines_added
1662 1670 if lines_pending <= 0:
1663 1671 break
1664 1672 return ret[::-1]
1665 1673
1666 1674 def _get_prompt_cursor(self):
1667 1675 """ Convenience method that returns a cursor for the prompt position.
1668 1676 """
1669 1677 cursor = self._control.textCursor()
1670 1678 cursor.setPosition(self._prompt_pos)
1671 1679 return cursor
1672 1680
1673 1681 def _get_selection_cursor(self, start, end):
1674 1682 """ Convenience method that returns a cursor with text selected between
1675 1683 the positions 'start' and 'end'.
1676 1684 """
1677 1685 cursor = self._control.textCursor()
1678 1686 cursor.setPosition(start)
1679 1687 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1680 1688 return cursor
1681 1689
1682 1690 def _get_word_start_cursor(self, position):
1683 1691 """ Find the start of the word to the left the given position. If a
1684 1692 sequence of non-word characters precedes the first word, skip over
1685 1693 them. (This emulates the behavior of bash, emacs, etc.)
1686 1694 """
1687 1695 document = self._control.document()
1688 1696 position -= 1
1689 1697 while position >= self._prompt_pos and \
1690 1698 not is_letter_or_number(document.characterAt(position)):
1691 1699 position -= 1
1692 1700 while position >= self._prompt_pos and \
1693 1701 is_letter_or_number(document.characterAt(position)):
1694 1702 position -= 1
1695 1703 cursor = self._control.textCursor()
1696 1704 cursor.setPosition(position + 1)
1697 1705 return cursor
1698 1706
1699 1707 def _get_word_end_cursor(self, position):
1700 1708 """ Find the end of the word to the right the given position. If a
1701 1709 sequence of non-word characters precedes the first word, skip over
1702 1710 them. (This emulates the behavior of bash, emacs, etc.)
1703 1711 """
1704 1712 document = self._control.document()
1705 1713 end = self._get_end_cursor().position()
1706 1714 while position < end and \
1707 1715 not is_letter_or_number(document.characterAt(position)):
1708 1716 position += 1
1709 1717 while position < end and \
1710 1718 is_letter_or_number(document.characterAt(position)):
1711 1719 position += 1
1712 1720 cursor = self._control.textCursor()
1713 1721 cursor.setPosition(position)
1714 1722 return cursor
1715 1723
1716 1724 def _insert_continuation_prompt(self, cursor):
1717 1725 """ Inserts new continuation prompt using the specified cursor.
1718 1726 """
1719 1727 if self._continuation_prompt_html is None:
1720 1728 self._insert_plain_text(cursor, self._continuation_prompt)
1721 1729 else:
1722 1730 self._continuation_prompt = self._insert_html_fetching_plain_text(
1723 1731 cursor, self._continuation_prompt_html)
1724 1732
1725 1733 def _insert_block(self, cursor, block_format=None):
1726 1734 """ Inserts an empty QTextBlock using the specified cursor.
1727 1735 """
1728 1736 if block_format is None:
1729 1737 block_format = QtGui.QTextBlockFormat()
1730 1738 cursor.insertBlock(block_format)
1731 1739
1732 1740 def _insert_html(self, cursor, html):
1733 1741 """ Inserts HTML using the specified cursor in such a way that future
1734 1742 formatting is unaffected.
1735 1743 """
1736 1744 cursor.beginEditBlock()
1737 1745 cursor.insertHtml(html)
1738 1746
1739 1747 # After inserting HTML, the text document "remembers" it's in "html
1740 1748 # mode", which means that subsequent calls adding plain text will result
1741 1749 # in unwanted formatting, lost tab characters, etc. The following code
1742 1750 # hacks around this behavior, which I consider to be a bug in Qt, by
1743 1751 # (crudely) resetting the document's style state.
1744 1752 cursor.movePosition(QtGui.QTextCursor.Left,
1745 1753 QtGui.QTextCursor.KeepAnchor)
1746 1754 if cursor.selection().toPlainText() == ' ':
1747 1755 cursor.removeSelectedText()
1748 1756 else:
1749 1757 cursor.movePosition(QtGui.QTextCursor.Right)
1750 1758 cursor.insertText(' ', QtGui.QTextCharFormat())
1751 1759 cursor.endEditBlock()
1752 1760
1753 1761 def _insert_html_fetching_plain_text(self, cursor, html):
1754 1762 """ Inserts HTML using the specified cursor, then returns its plain text
1755 1763 version.
1756 1764 """
1757 1765 cursor.beginEditBlock()
1758 1766 cursor.removeSelectedText()
1759 1767
1760 1768 start = cursor.position()
1761 1769 self._insert_html(cursor, html)
1762 1770 end = cursor.position()
1763 1771 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1764 1772 text = cursor.selection().toPlainText()
1765 1773
1766 1774 cursor.setPosition(end)
1767 1775 cursor.endEditBlock()
1768 1776 return text
1769 1777
1770 1778 def _insert_plain_text(self, cursor, text, flush=False):
1771 1779 """ Inserts plain text using the specified cursor, processing ANSI codes
1772 1780 if enabled.
1773 1781 """
1774 1782 # maximumBlockCount() can be different from self.buffer_size in
1775 1783 # case input prompt is active.
1776 1784 buffer_size = self._control.document().maximumBlockCount()
1777 1785
1778 1786 if (self._executing and not flush and
1779 1787 self._pending_text_flush_interval.isActive() and
1780 1788 cursor.position() == self._get_end_cursor().position()):
1781 1789 # Queue the text to insert in case it is being inserted at end
1782 1790 self._pending_insert_text.append(text)
1783 1791 if buffer_size > 0:
1784 1792 self._pending_insert_text = self._get_last_lines_from_list(
1785 1793 self._pending_insert_text, buffer_size)
1786 1794 return
1787 1795
1788 1796 if self._executing and not self._pending_text_flush_interval.isActive():
1789 1797 self._pending_text_flush_interval.start()
1790 1798
1791 1799 # Clip the text to last `buffer_size` lines.
1792 1800 if buffer_size > 0:
1793 1801 text = self._get_last_lines(text, buffer_size)
1794 1802
1795 1803 cursor.beginEditBlock()
1796 1804 if self.ansi_codes:
1797 1805 for substring in self._ansi_processor.split_string(text):
1798 1806 for act in self._ansi_processor.actions:
1799 1807
1800 1808 # Unlike real terminal emulators, we don't distinguish
1801 1809 # between the screen and the scrollback buffer. A screen
1802 1810 # erase request clears everything.
1803 1811 if act.action == 'erase' and act.area == 'screen':
1804 1812 cursor.select(QtGui.QTextCursor.Document)
1805 1813 cursor.removeSelectedText()
1806 1814
1807 1815 # Simulate a form feed by scrolling just past the last line.
1808 1816 elif act.action == 'scroll' and act.unit == 'page':
1809 1817 cursor.insertText('\n')
1810 1818 cursor.endEditBlock()
1811 1819 self._set_top_cursor(cursor)
1812 1820 cursor.joinPreviousEditBlock()
1813 1821 cursor.deletePreviousChar()
1814 1822
1815 1823 elif act.action == 'carriage-return':
1816 1824 cursor.movePosition(
1817 1825 cursor.StartOfLine, cursor.KeepAnchor)
1818 1826
1819 1827 elif act.action == 'beep':
1820 1828 QtGui.qApp.beep()
1821 1829
1822 1830 elif act.action == 'backspace':
1823 1831 if not cursor.atBlockStart():
1824 1832 cursor.movePosition(
1825 1833 cursor.PreviousCharacter, cursor.KeepAnchor)
1826 1834
1827 1835 elif act.action == 'newline':
1828 1836 cursor.movePosition(cursor.EndOfLine)
1829 1837
1830 1838 format = self._ansi_processor.get_format()
1831 1839
1832 1840 selection = cursor.selectedText()
1833 1841 if len(selection) == 0:
1834 1842 cursor.insertText(substring, format)
1835 1843 elif substring is not None:
1836 1844 # BS and CR are treated as a change in print
1837 1845 # position, rather than a backwards character
1838 1846 # deletion for output equivalence with (I)Python
1839 1847 # terminal.
1840 1848 if len(substring) >= len(selection):
1841 1849 cursor.insertText(substring, format)
1842 1850 else:
1843 1851 old_text = selection[len(substring):]
1844 1852 cursor.insertText(substring + old_text, format)
1845 1853 cursor.movePosition(cursor.PreviousCharacter,
1846 1854 cursor.KeepAnchor, len(old_text))
1847 1855 else:
1848 1856 cursor.insertText(text)
1849 1857 cursor.endEditBlock()
1850 1858
1851 1859 def _insert_plain_text_into_buffer(self, cursor, text):
1852 1860 """ Inserts text into the input buffer using the specified cursor (which
1853 1861 must be in the input buffer), ensuring that continuation prompts are
1854 1862 inserted as necessary.
1855 1863 """
1856 1864 lines = text.splitlines(True)
1857 1865 if lines:
1858 1866 cursor.beginEditBlock()
1859 1867 cursor.insertText(lines[0])
1860 1868 for line in lines[1:]:
1861 1869 if self._continuation_prompt_html is None:
1862 1870 cursor.insertText(self._continuation_prompt)
1863 1871 else:
1864 1872 self._continuation_prompt = \
1865 1873 self._insert_html_fetching_plain_text(
1866 1874 cursor, self._continuation_prompt_html)
1867 1875 cursor.insertText(line)
1868 1876 cursor.endEditBlock()
1869 1877
1870 1878 def _in_buffer(self, position=None):
1871 1879 """ Returns whether the current cursor (or, if specified, a position) is
1872 1880 inside the editing region.
1873 1881 """
1874 1882 cursor = self._control.textCursor()
1875 1883 if position is None:
1876 1884 position = cursor.position()
1877 1885 else:
1878 1886 cursor.setPosition(position)
1879 1887 line = cursor.blockNumber()
1880 1888 prompt_line = self._get_prompt_cursor().blockNumber()
1881 1889 if line == prompt_line:
1882 1890 return position >= self._prompt_pos
1883 1891 elif line > prompt_line:
1884 1892 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1885 1893 prompt_pos = cursor.position() + len(self._continuation_prompt)
1886 1894 return position >= prompt_pos
1887 1895 return False
1888 1896
1889 1897 def _keep_cursor_in_buffer(self):
1890 1898 """ Ensures that the cursor is inside the editing region. Returns
1891 1899 whether the cursor was moved.
1892 1900 """
1893 1901 moved = not self._in_buffer()
1894 1902 if moved:
1895 1903 cursor = self._control.textCursor()
1896 1904 cursor.movePosition(QtGui.QTextCursor.End)
1897 1905 self._control.setTextCursor(cursor)
1898 1906 return moved
1899 1907
1900 1908 def _keyboard_quit(self):
1901 1909 """ Cancels the current editing task ala Ctrl-G in Emacs.
1902 1910 """
1903 1911 if self._temp_buffer_filled :
1904 1912 self._cancel_completion()
1905 1913 self._clear_temporary_buffer()
1906 1914 else:
1907 1915 self.input_buffer = ''
1908 1916
1909 1917 def _page(self, text, html=False):
1910 1918 """ Displays text using the pager if it exceeds the height of the
1911 1919 viewport.
1912 1920
1913 1921 Parameters
1914 1922 ----------
1915 1923 html : bool, optional (default False)
1916 1924 If set, the text will be interpreted as HTML instead of plain text.
1917 1925 """
1918 1926 line_height = QtGui.QFontMetrics(self.font).height()
1919 1927 minlines = self._control.viewport().height() / line_height
1920 1928 if self.paging != 'none' and \
1921 1929 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1922 1930 if self.paging == 'custom':
1923 1931 self.custom_page_requested.emit(text)
1924 1932 else:
1925 1933 # disable buffer truncation during paging
1926 1934 self._control.document().setMaximumBlockCount(0)
1927 1935 self._page_control.clear()
1928 1936 cursor = self._page_control.textCursor()
1929 1937 if html:
1930 1938 self._insert_html(cursor, text)
1931 1939 else:
1932 1940 self._insert_plain_text(cursor, text)
1933 1941 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1934 1942
1935 1943 self._page_control.viewport().resize(self._control.size())
1936 1944 if self._splitter:
1937 1945 self._page_control.show()
1938 1946 self._page_control.setFocus()
1939 1947 else:
1940 1948 self.layout().setCurrentWidget(self._page_control)
1941 1949 elif html:
1942 1950 self._append_html(text)
1943 1951 else:
1944 1952 self._append_plain_text(text)
1945 1953
1946 1954 def _set_paging(self, paging):
1947 1955 """
1948 1956 Change the pager to `paging` style.
1949 1957
1950 1958 Parameters
1951 1959 ----------
1952 1960 paging : string
1953 1961 Either "hsplit", "vsplit", or "inside"
1954 1962 """
1955 1963 if self._splitter is None:
1956 1964 raise NotImplementedError("""can only switch if --paging=hsplit or
1957 1965 --paging=vsplit is used.""")
1958 1966 if paging == 'hsplit':
1959 1967 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1960 1968 elif paging == 'vsplit':
1961 1969 self._splitter.setOrientation(QtCore.Qt.Vertical)
1962 1970 elif paging == 'inside':
1963 1971 raise NotImplementedError("""switching to 'inside' paging not
1964 1972 supported yet.""")
1965 1973 else:
1966 1974 raise ValueError("unknown paging method '%s'" % paging)
1967 1975 self.paging = paging
1968 1976
1969 1977 def _prompt_finished(self):
1970 1978 """ Called immediately after a prompt is finished, i.e. when some input
1971 1979 will be processed and a new prompt displayed.
1972 1980 """
1973 1981 self._control.setReadOnly(True)
1974 1982 self._prompt_finished_hook()
1975 1983
1976 1984 def _prompt_started(self):
1977 1985 """ Called immediately after a new prompt is displayed.
1978 1986 """
1979 1987 # Temporarily disable the maximum block count to permit undo/redo and
1980 1988 # to ensure that the prompt position does not change due to truncation.
1981 1989 self._control.document().setMaximumBlockCount(0)
1982 1990 self._control.setUndoRedoEnabled(True)
1983 1991
1984 1992 # Work around bug in QPlainTextEdit: input method is not re-enabled
1985 1993 # when read-only is disabled.
1986 1994 self._control.setReadOnly(False)
1987 1995 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1988 1996
1989 1997 if not self._reading:
1990 1998 self._executing = False
1991 1999 self._prompt_started_hook()
1992 2000
1993 2001 # If the input buffer has changed while executing, load it.
1994 2002 if self._input_buffer_pending:
1995 2003 self.input_buffer = self._input_buffer_pending
1996 2004 self._input_buffer_pending = ''
1997 2005
1998 2006 self._control.moveCursor(QtGui.QTextCursor.End)
1999 2007
2000 2008 def _readline(self, prompt='', callback=None):
2001 2009 """ Reads one line of input from the user.
2002 2010
2003 2011 Parameters
2004 2012 ----------
2005 2013 prompt : str, optional
2006 2014 The prompt to print before reading the line.
2007 2015
2008 2016 callback : callable, optional
2009 2017 A callback to execute with the read line. If not specified, input is
2010 2018 read *synchronously* and this method does not return until it has
2011 2019 been read.
2012 2020
2013 2021 Returns
2014 2022 -------
2015 2023 If a callback is specified, returns nothing. Otherwise, returns the
2016 2024 input string with the trailing newline stripped.
2017 2025 """
2018 2026 if self._reading:
2019 2027 raise RuntimeError('Cannot read a line. Widget is already reading.')
2020 2028
2021 2029 if not callback and not self.isVisible():
2022 2030 # If the user cannot see the widget, this function cannot return.
2023 2031 raise RuntimeError('Cannot synchronously read a line if the widget '
2024 2032 'is not visible!')
2025 2033
2026 2034 self._reading = True
2027 2035 self._show_prompt(prompt, newline=False)
2028 2036
2029 2037 if callback is None:
2030 2038 self._reading_callback = None
2031 2039 while self._reading:
2032 2040 QtCore.QCoreApplication.processEvents()
2033 2041 return self._get_input_buffer(force=True).rstrip('\n')
2034 2042
2035 2043 else:
2036 2044 self._reading_callback = lambda: \
2037 2045 callback(self._get_input_buffer(force=True).rstrip('\n'))
2038 2046
2039 2047 def _set_continuation_prompt(self, prompt, html=False):
2040 2048 """ Sets the continuation prompt.
2041 2049
2042 2050 Parameters
2043 2051 ----------
2044 2052 prompt : str
2045 2053 The prompt to show when more input is needed.
2046 2054
2047 2055 html : bool, optional (default False)
2048 2056 If set, the prompt will be inserted as formatted HTML. Otherwise,
2049 2057 the prompt will be treated as plain text, though ANSI color codes
2050 2058 will be handled.
2051 2059 """
2052 2060 if html:
2053 2061 self._continuation_prompt_html = prompt
2054 2062 else:
2055 2063 self._continuation_prompt = prompt
2056 2064 self._continuation_prompt_html = None
2057 2065
2058 2066 def _set_cursor(self, cursor):
2059 2067 """ Convenience method to set the current cursor.
2060 2068 """
2061 2069 self._control.setTextCursor(cursor)
2062 2070
2063 2071 def _set_top_cursor(self, cursor):
2064 2072 """ Scrolls the viewport so that the specified cursor is at the top.
2065 2073 """
2066 2074 scrollbar = self._control.verticalScrollBar()
2067 2075 scrollbar.setValue(scrollbar.maximum())
2068 2076 original_cursor = self._control.textCursor()
2069 2077 self._control.setTextCursor(cursor)
2070 2078 self._control.ensureCursorVisible()
2071 2079 self._control.setTextCursor(original_cursor)
2072 2080
2073 2081 def _show_prompt(self, prompt=None, html=False, newline=True):
2074 2082 """ Writes a new prompt at the end of the buffer.
2075 2083
2076 2084 Parameters
2077 2085 ----------
2078 2086 prompt : str, optional
2079 2087 The prompt to show. If not specified, the previous prompt is used.
2080 2088
2081 2089 html : bool, optional (default False)
2082 2090 Only relevant when a prompt is specified. If set, the prompt will
2083 2091 be inserted as formatted HTML. Otherwise, the prompt will be treated
2084 2092 as plain text, though ANSI color codes will be handled.
2085 2093
2086 2094 newline : bool, optional (default True)
2087 2095 If set, a new line will be written before showing the prompt if
2088 2096 there is not already a newline at the end of the buffer.
2089 2097 """
2090 2098 # Save the current end position to support _append*(before_prompt=True).
2091 2099 self._flush_pending_stream()
2092 2100 cursor = self._get_end_cursor()
2093 2101 self._append_before_prompt_pos = cursor.position()
2094 2102
2095 2103 # Insert a preliminary newline, if necessary.
2096 2104 if newline and cursor.position() > 0:
2097 2105 cursor.movePosition(QtGui.QTextCursor.Left,
2098 2106 QtGui.QTextCursor.KeepAnchor)
2099 2107 if cursor.selection().toPlainText() != '\n':
2100 2108 self._append_block()
2101 2109 self._append_before_prompt_pos += 1
2102 2110
2103 2111 # Write the prompt.
2104 2112 self._append_plain_text(self._prompt_sep)
2105 2113 if prompt is None:
2106 2114 if self._prompt_html is None:
2107 2115 self._append_plain_text(self._prompt)
2108 2116 else:
2109 2117 self._append_html(self._prompt_html)
2110 2118 else:
2111 2119 if html:
2112 2120 self._prompt = self._append_html_fetching_plain_text(prompt)
2113 2121 self._prompt_html = prompt
2114 2122 else:
2115 2123 self._append_plain_text(prompt)
2116 2124 self._prompt = prompt
2117 2125 self._prompt_html = None
2118 2126
2119 2127 self._flush_pending_stream()
2120 2128 self._prompt_pos = self._get_end_cursor().position()
2121 2129 self._prompt_started()
2122 2130
2123 2131 #------ Signal handlers ----------------------------------------------------
2124 2132
2125 2133 def _adjust_scrollbars(self):
2126 2134 """ Expands the vertical scrollbar beyond the range set by Qt.
2127 2135 """
2128 2136 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2129 2137 # and qtextedit.cpp.
2130 2138 document = self._control.document()
2131 2139 scrollbar = self._control.verticalScrollBar()
2132 2140 viewport_height = self._control.viewport().height()
2133 2141 if isinstance(self._control, QtGui.QPlainTextEdit):
2134 2142 maximum = max(0, document.lineCount() - 1)
2135 2143 step = viewport_height / self._control.fontMetrics().lineSpacing()
2136 2144 else:
2137 2145 # QTextEdit does not do line-based layout and blocks will not in
2138 2146 # general have the same height. Therefore it does not make sense to
2139 2147 # attempt to scroll in line height increments.
2140 2148 maximum = document.size().height()
2141 2149 step = viewport_height
2142 2150 diff = maximum - scrollbar.maximum()
2143 2151 scrollbar.setRange(0, maximum)
2144 2152 scrollbar.setPageStep(step)
2145 2153
2146 2154 # Compensate for undesirable scrolling that occurs automatically due to
2147 2155 # maximumBlockCount() text truncation.
2148 2156 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2149 2157 scrollbar.setValue(scrollbar.value() + diff)
2150 2158
2151 2159 def _custom_context_menu_requested(self, pos):
2152 2160 """ Shows a context menu at the given QPoint (in widget coordinates).
2153 2161 """
2154 2162 menu = self._context_menu_make(pos)
2155 2163 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,797 +1,797 b''
1 1 """Frontend widget for the Qt Console"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 from collections import namedtuple
9 9 import sys
10 10 import uuid
11 11
12 12 from IPython.external import qt
13 13 from IPython.external.qt import QtCore, QtGui
14 14 from IPython.utils import py3compat
15 15 from IPython.utils.importstring import import_item
16 16
17 17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
18 18 from IPython.core.inputtransformer import classic_prompt
19 19 from IPython.core.oinspect import call_tip
20 20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
21 21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
22 22 from .bracket_matcher import BracketMatcher
23 23 from .call_tip_widget import CallTipWidget
24 24 from .history_console_widget import HistoryConsoleWidget
25 25 from .pygments_highlighter import PygmentsHighlighter
26 26
27 27
28 28 class FrontendHighlighter(PygmentsHighlighter):
29 29 """ A PygmentsHighlighter that understands and ignores prompts.
30 30 """
31 31
32 32 def __init__(self, frontend, lexer=None):
33 33 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
34 34 self._current_offset = 0
35 35 self._frontend = frontend
36 36 self.highlighting_on = False
37 37
38 38 def highlightBlock(self, string):
39 39 """ Highlight a block of text. Reimplemented to highlight selectively.
40 40 """
41 41 if not self.highlighting_on:
42 42 return
43 43
44 44 # The input to this function is a unicode string that may contain
45 45 # paragraph break characters, non-breaking spaces, etc. Here we acquire
46 46 # the string as plain text so we can compare it.
47 47 current_block = self.currentBlock()
48 48 string = self._frontend._get_block_plain_text(current_block)
49 49
50 50 # Decide whether to check for the regular or continuation prompt.
51 51 if current_block.contains(self._frontend._prompt_pos):
52 52 prompt = self._frontend._prompt
53 53 else:
54 54 prompt = self._frontend._continuation_prompt
55 55
56 56 # Only highlight if we can identify a prompt, but make sure not to
57 57 # highlight the prompt.
58 58 if string.startswith(prompt):
59 59 self._current_offset = len(prompt)
60 60 string = string[len(prompt):]
61 61 super(FrontendHighlighter, self).highlightBlock(string)
62 62
63 63 def rehighlightBlock(self, block):
64 64 """ Reimplemented to temporarily enable highlighting if disabled.
65 65 """
66 66 old = self.highlighting_on
67 67 self.highlighting_on = True
68 68 super(FrontendHighlighter, self).rehighlightBlock(block)
69 69 self.highlighting_on = old
70 70
71 71 def setFormat(self, start, count, format):
72 72 """ Reimplemented to highlight selectively.
73 73 """
74 74 start += self._current_offset
75 75 super(FrontendHighlighter, self).setFormat(start, count, format)
76 76
77 77
78 78 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
79 79 """ A Qt frontend for a generic Python kernel.
80 80 """
81 81
82 82 # The text to show when the kernel is (re)started.
83 83 banner = Unicode(config=True)
84 84 kernel_banner = Unicode()
85 85
86 86 # An option and corresponding signal for overriding the default kernel
87 87 # interrupt behavior.
88 88 custom_interrupt = Bool(False)
89 89 custom_interrupt_requested = QtCore.Signal()
90 90
91 91 # An option and corresponding signals for overriding the default kernel
92 92 # restart behavior.
93 93 custom_restart = Bool(False)
94 94 custom_restart_kernel_died = QtCore.Signal(float)
95 95 custom_restart_requested = QtCore.Signal()
96 96
97 97 # Whether to automatically show calltips on open-parentheses.
98 98 enable_calltips = Bool(True, config=True,
99 99 help="Whether to draw information calltips on open-parentheses.")
100 100
101 101 clear_on_kernel_restart = Bool(True, config=True,
102 102 help="Whether to clear the console when the kernel is restarted")
103 103
104 104 confirm_restart = Bool(True, config=True,
105 105 help="Whether to ask for user confirmation when restarting kernel")
106 106
107 107 lexer_class = DottedObjectName(config=True,
108 108 help="The pygments lexer class to use."
109 109 )
110 110 def _lexer_class_changed(self, name, old, new):
111 111 lexer_class = import_item(new)
112 112 self.lexer = lexer_class()
113 113
114 114 def _lexer_class_default(self):
115 115 if py3compat.PY3:
116 116 return 'pygments.lexers.Python3Lexer'
117 117 else:
118 118 return 'pygments.lexers.PythonLexer'
119 119
120 120 lexer = Any()
121 121 def _lexer_default(self):
122 122 lexer_class = import_item(self.lexer_class)
123 123 return lexer_class()
124 124
125 125 # Emitted when a user visible 'execute_request' has been submitted to the
126 126 # kernel from the FrontendWidget. Contains the code to be executed.
127 127 executing = QtCore.Signal(object)
128 128
129 129 # Emitted when a user-visible 'execute_reply' has been received from the
130 130 # kernel and processed by the FrontendWidget. Contains the response message.
131 131 executed = QtCore.Signal(object)
132 132
133 133 # Emitted when an exit request has been received from the kernel.
134 134 exit_requested = QtCore.Signal(object)
135 135
136 136 # Protected class variables.
137 137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 138 logical_line_transforms=[],
139 139 python_line_transforms=[],
140 140 )
141 141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 144 _input_splitter_class = InputSplitter
145 145 _local_kernel = False
146 146 _highlighter = Instance(FrontendHighlighter)
147 147
148 148 #---------------------------------------------------------------------------
149 149 # 'object' interface
150 150 #---------------------------------------------------------------------------
151 151
152 152 def __init__(self, *args, **kw):
153 153 super(FrontendWidget, self).__init__(*args, **kw)
154 154 # FIXME: remove this when PySide min version is updated past 1.0.7
155 155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 156 if qt.QT_API == qt.QT_API_PYSIDE:
157 157 import PySide
158 158 if PySide.__version_info__ < (1,0,7):
159 159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 160 self.enable_calltips = False
161 161
162 162 # FrontendWidget protected variables.
163 163 self._bracket_matcher = BracketMatcher(self._control)
164 164 self._call_tip_widget = CallTipWidget(self._control)
165 165 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
166 166 self._hidden = False
167 167 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
168 168 self._input_splitter = self._input_splitter_class()
169 169 self._kernel_manager = None
170 170 self._kernel_client = None
171 171 self._request_info = {}
172 172 self._request_info['execute'] = {};
173 173 self._callback_dict = {}
174 174
175 175 # Configure the ConsoleWidget.
176 176 self.tab_width = 4
177 177 self._set_continuation_prompt('... ')
178 178
179 179 # Configure the CallTipWidget.
180 180 self._call_tip_widget.setFont(self.font)
181 181 self.font_changed.connect(self._call_tip_widget.setFont)
182 182
183 183 # Configure actions.
184 184 action = self._copy_raw_action
185 185 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
186 186 action.setEnabled(False)
187 187 action.setShortcut(QtGui.QKeySequence(key))
188 188 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
189 189 action.triggered.connect(self.copy_raw)
190 190 self.copy_available.connect(action.setEnabled)
191 191 self.addAction(action)
192 192
193 193 # Connect signal handlers.
194 194 document = self._control.document()
195 195 document.contentsChange.connect(self._document_contents_change)
196 196
197 197 # Set flag for whether we are connected via localhost.
198 198 self._local_kernel = kw.get('local_kernel',
199 199 FrontendWidget._local_kernel)
200 200
201 201 # Whether or not a clear_output call is pending new output.
202 202 self._pending_clearoutput = False
203 203
204 204 #---------------------------------------------------------------------------
205 205 # 'ConsoleWidget' public interface
206 206 #---------------------------------------------------------------------------
207 207
208 208 def copy(self):
209 209 """ Copy the currently selected text to the clipboard, removing prompts.
210 210 """
211 211 if self._page_control is not None and self._page_control.hasFocus():
212 212 self._page_control.copy()
213 213 elif self._control.hasFocus():
214 214 text = self._control.textCursor().selection().toPlainText()
215 215 if text:
216 216 was_newline = text[-1] == '\n'
217 217 text = self._prompt_transformer.transform_cell(text)
218 218 if not was_newline: # user doesn't need newline
219 219 text = text[:-1]
220 220 QtGui.QApplication.clipboard().setText(text)
221 221 else:
222 222 self.log.debug("frontend widget : unknown copy target")
223 223
224 224 #---------------------------------------------------------------------------
225 225 # 'ConsoleWidget' abstract interface
226 226 #---------------------------------------------------------------------------
227 227
228 228 def _is_complete(self, source, interactive):
229 229 """ Returns whether 'source' can be completely processed and a new
230 230 prompt created. When triggered by an Enter/Return key press,
231 231 'interactive' is True; otherwise, it is False.
232 232 """
233 233 self._input_splitter.reset()
234 234 try:
235 235 complete = self._input_splitter.push(source)
236 236 except SyntaxError:
237 237 return True
238 238 if interactive:
239 239 complete = not self._input_splitter.push_accepts_more()
240 240 return complete
241 241
242 242 def _execute(self, source, hidden):
243 243 """ Execute 'source'. If 'hidden', do not show any output.
244 244
245 245 See parent class :meth:`execute` docstring for full details.
246 246 """
247 247 msg_id = self.kernel_client.execute(source, hidden)
248 248 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
249 249 self._hidden = hidden
250 250 if not hidden:
251 251 self.executing.emit(source)
252 252
253 253 def _prompt_started_hook(self):
254 254 """ Called immediately after a new prompt is displayed.
255 255 """
256 256 if not self._reading:
257 257 self._highlighter.highlighting_on = True
258 258
259 259 def _prompt_finished_hook(self):
260 260 """ Called immediately after a prompt is finished, i.e. when some input
261 261 will be processed and a new prompt displayed.
262 262 """
263 263 # Flush all state from the input splitter so the next round of
264 264 # reading input starts with a clean buffer.
265 265 self._input_splitter.reset()
266 266
267 267 if not self._reading:
268 268 self._highlighter.highlighting_on = False
269 269
270 270 def _tab_pressed(self):
271 271 """ Called when the tab key is pressed. Returns whether to continue
272 272 processing the event.
273 273 """
274 274 # Perform tab completion if:
275 275 # 1) The cursor is in the input buffer.
276 276 # 2) There is a non-whitespace character before the cursor.
277 277 text = self._get_input_buffer_cursor_line()
278 278 if text is None:
279 279 return False
280 280 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
281 281 if complete:
282 282 self._complete()
283 283 return not complete
284 284
285 285 #---------------------------------------------------------------------------
286 286 # 'ConsoleWidget' protected interface
287 287 #---------------------------------------------------------------------------
288 288
289 289 def _context_menu_make(self, pos):
290 290 """ Reimplemented to add an action for raw copy.
291 291 """
292 292 menu = super(FrontendWidget, self)._context_menu_make(pos)
293 293 for before_action in menu.actions():
294 294 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
295 295 QtGui.QKeySequence.ExactMatch:
296 296 menu.insertAction(before_action, self._copy_raw_action)
297 297 break
298 298 return menu
299 299
300 300 def request_interrupt_kernel(self):
301 301 if self._executing:
302 302 self.interrupt_kernel()
303 303
304 304 def request_restart_kernel(self):
305 305 message = 'Are you sure you want to restart the kernel?'
306 306 self.restart_kernel(message, now=False)
307 307
308 308 def _event_filter_console_keypress(self, event):
309 309 """ Reimplemented for execution interruption and smart backspace.
310 310 """
311 311 key = event.key()
312 312 if self._control_key_down(event.modifiers(), include_command=False):
313 313
314 314 if key == QtCore.Qt.Key_C and self._executing:
315 315 self.request_interrupt_kernel()
316 316 return True
317 317
318 318 elif key == QtCore.Qt.Key_Period:
319 319 self.request_restart_kernel()
320 320 return True
321 321
322 322 elif not event.modifiers() & QtCore.Qt.AltModifier:
323 323
324 324 # Smart backspace: remove four characters in one backspace if:
325 325 # 1) everything left of the cursor is whitespace
326 326 # 2) the four characters immediately left of the cursor are spaces
327 327 if key == QtCore.Qt.Key_Backspace:
328 328 col = self._get_input_buffer_cursor_column()
329 329 cursor = self._control.textCursor()
330 330 if col > 3 and not cursor.hasSelection():
331 331 text = self._get_input_buffer_cursor_line()[:col]
332 332 if text.endswith(' ') and not text.strip():
333 333 cursor.movePosition(QtGui.QTextCursor.Left,
334 334 QtGui.QTextCursor.KeepAnchor, 4)
335 335 cursor.removeSelectedText()
336 336 return True
337 337
338 338 return super(FrontendWidget, self)._event_filter_console_keypress(event)
339 339
340 340 def _insert_continuation_prompt(self, cursor):
341 341 """ Reimplemented for auto-indentation.
342 342 """
343 343 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
344 344 cursor.insertText(' ' * self._input_splitter.indent_spaces)
345 345
346 346 #---------------------------------------------------------------------------
347 347 # 'BaseFrontendMixin' abstract interface
348 348 #---------------------------------------------------------------------------
349 349 def _handle_clear_output(self, msg):
350 350 """Handle clear output messages."""
351 if not self._hidden and self._is_from_this_session(msg):
351 if include_output(msg):
352 352 wait = msg['content'].get('wait', True)
353 353 if wait:
354 354 self._pending_clearoutput = True
355 355 else:
356 356 self.clear_output()
357 357
358 358 def _silent_exec_callback(self, expr, callback):
359 359 """Silently execute `expr` in the kernel and call `callback` with reply
360 360
361 361 the `expr` is evaluated silently in the kernel (without) output in
362 362 the frontend. Call `callback` with the
363 363 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
364 364
365 365 Parameters
366 366 ----------
367 367 expr : string
368 368 valid string to be executed by the kernel.
369 369 callback : function
370 370 function accepting one argument, as a string. The string will be
371 371 the `repr` of the result of evaluating `expr`
372 372
373 373 The `callback` is called with the `repr()` of the result of `expr` as
374 374 first argument. To get the object, do `eval()` on the passed value.
375 375
376 376 See Also
377 377 --------
378 378 _handle_exec_callback : private method, deal with calling callback with reply
379 379
380 380 """
381 381
382 382 # generate uuid, which would be used as an indication of whether or
383 383 # not the unique request originated from here (can use msg id ?)
384 384 local_uuid = str(uuid.uuid1())
385 385 msg_id = self.kernel_client.execute('',
386 386 silent=True, user_expressions={ local_uuid:expr })
387 387 self._callback_dict[local_uuid] = callback
388 388 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
389 389
390 390 def _handle_exec_callback(self, msg):
391 391 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
392 392
393 393 Parameters
394 394 ----------
395 395 msg : raw message send by the kernel containing an `user_expressions`
396 396 and having a 'silent_exec_callback' kind.
397 397
398 398 Notes
399 399 -----
400 400 This function will look for a `callback` associated with the
401 401 corresponding message id. Association has been made by
402 402 `_silent_exec_callback`. `callback` is then called with the `repr()`
403 403 of the value of corresponding `user_expressions` as argument.
404 404 `callback` is then removed from the known list so that any message
405 405 coming again with the same id won't trigger it.
406 406
407 407 """
408 408
409 409 user_exp = msg['content'].get('user_expressions')
410 410 if not user_exp:
411 411 return
412 412 for expression in user_exp:
413 413 if expression in self._callback_dict:
414 414 self._callback_dict.pop(expression)(user_exp[expression])
415 415
416 416 def _handle_execute_reply(self, msg):
417 417 """ Handles replies for code execution.
418 418 """
419 419 self.log.debug("execute: %s", msg.get('content', ''))
420 420 msg_id = msg['parent_header']['msg_id']
421 421 info = self._request_info['execute'].get(msg_id)
422 422 # unset reading flag, because if execute finished, raw_input can't
423 423 # still be pending.
424 424 self._reading = False
425 425 if info and info.kind == 'user' and not self._hidden:
426 426 # Make sure that all output from the SUB channel has been processed
427 427 # before writing a new prompt.
428 428 self.kernel_client.iopub_channel.flush()
429 429
430 430 # Reset the ANSI style information to prevent bad text in stdout
431 431 # from messing up our colors. We're not a true terminal so we're
432 432 # allowed to do this.
433 433 if self.ansi_codes:
434 434 self._ansi_processor.reset_sgr()
435 435
436 436 content = msg['content']
437 437 status = content['status']
438 438 if status == 'ok':
439 439 self._process_execute_ok(msg)
440 440 elif status == 'error':
441 441 self._process_execute_error(msg)
442 442 elif status == 'aborted':
443 443 self._process_execute_abort(msg)
444 444
445 445 self._show_interpreter_prompt_for_reply(msg)
446 446 self.executed.emit(msg)
447 447 self._request_info['execute'].pop(msg_id)
448 448 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
449 449 self._handle_exec_callback(msg)
450 450 self._request_info['execute'].pop(msg_id)
451 451 else:
452 452 super(FrontendWidget, self)._handle_execute_reply(msg)
453 453
454 454 def _handle_input_request(self, msg):
455 455 """ Handle requests for raw_input.
456 456 """
457 457 self.log.debug("input: %s", msg.get('content', ''))
458 458 if self._hidden:
459 459 raise RuntimeError('Request for raw input during hidden execution.')
460 460
461 461 # Make sure that all output from the SUB channel has been processed
462 462 # before entering readline mode.
463 463 self.kernel_client.iopub_channel.flush()
464 464
465 465 def callback(line):
466 466 self.kernel_client.stdin_channel.input(line)
467 467 if self._reading:
468 468 self.log.debug("Got second input request, assuming first was interrupted.")
469 469 self._reading = False
470 470 self._readline(msg['content']['prompt'], callback=callback)
471 471
472 472 def _kernel_restarted_message(self, died=True):
473 473 msg = "Kernel died, restarting" if died else "Kernel restarting"
474 474 self._append_html("<br>%s<hr><br>" % msg,
475 475 before_prompt=False
476 476 )
477 477
478 478 def _handle_kernel_died(self, since_last_heartbeat):
479 479 """Handle the kernel's death (if we do not own the kernel).
480 480 """
481 481 self.log.warn("kernel died: %s", since_last_heartbeat)
482 482 if self.custom_restart:
483 483 self.custom_restart_kernel_died.emit(since_last_heartbeat)
484 484 else:
485 485 self._kernel_restarted_message(died=True)
486 486 self.reset()
487 487
488 488 def _handle_kernel_restarted(self, died=True):
489 489 """Notice that the autorestarter restarted the kernel.
490 490
491 491 There's nothing to do but show a message.
492 492 """
493 493 self.log.warn("kernel restarted")
494 494 self._kernel_restarted_message(died=died)
495 495 self.reset()
496 496
497 497 def _handle_inspect_reply(self, rep):
498 498 """Handle replies for call tips."""
499 499 self.log.debug("oinfo: %s", rep.get('content', ''))
500 500 cursor = self._get_cursor()
501 501 info = self._request_info.get('call_tip')
502 502 if info and info.id == rep['parent_header']['msg_id'] and \
503 503 info.pos == cursor.position():
504 504 content = rep['content']
505 505 if content.get('status') == 'ok' and content.get('found', False):
506 506 self._call_tip_widget.show_inspect_data(content)
507 507
508 508 def _handle_execute_result(self, msg):
509 509 """ Handle display hook output.
510 510 """
511 511 self.log.debug("execute_result: %s", msg.get('content', ''))
512 if not self._hidden and self._is_from_this_session(msg):
512 if self.include_output(msg):
513 513 self.flush_clearoutput()
514 514 text = msg['content']['data']
515 515 self._append_plain_text(text + '\n', before_prompt=True)
516 516
517 517 def _handle_stream(self, msg):
518 518 """ Handle stdout, stderr, and stdin.
519 519 """
520 520 self.log.debug("stream: %s", msg.get('content', ''))
521 if not self._hidden and self._is_from_this_session(msg):
521 if self.include_output(msg):
522 522 self.flush_clearoutput()
523 523 self.append_stream(msg['content']['text'])
524 524
525 525 def _handle_shutdown_reply(self, msg):
526 526 """ Handle shutdown signal, only if from other console.
527 527 """
528 528 self.log.info("shutdown: %s", msg.get('content', ''))
529 529 restart = msg.get('content', {}).get('restart', False)
530 if not self._hidden and not self._is_from_this_session(msg):
530 if not self._hidden and not self.from_here(msg):
531 531 # got shutdown reply, request came from session other than ours
532 532 if restart:
533 533 # someone restarted the kernel, handle it
534 534 self._handle_kernel_restarted(died=False)
535 535 else:
536 536 # kernel was shutdown permanently
537 537 # this triggers exit_requested if the kernel was local,
538 538 # and a dialog if the kernel was remote,
539 539 # so we don't suddenly clear the qtconsole without asking.
540 540 if self._local_kernel:
541 541 self.exit_requested.emit(self)
542 542 else:
543 543 title = self.window().windowTitle()
544 544 reply = QtGui.QMessageBox.question(self, title,
545 545 "Kernel has been shutdown permanently. "
546 546 "Close the Console?",
547 547 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
548 548 if reply == QtGui.QMessageBox.Yes:
549 549 self.exit_requested.emit(self)
550 550
551 551 def _handle_status(self, msg):
552 552 """Handle status message"""
553 553 # This is where a busy/idle indicator would be triggered,
554 554 # when we make one.
555 555 state = msg['content'].get('execution_state', '')
556 556 if state == 'starting':
557 557 # kernel started while we were running
558 558 if self._executing:
559 559 self._handle_kernel_restarted(died=True)
560 560 elif state == 'idle':
561 561 pass
562 562 elif state == 'busy':
563 563 pass
564 564
565 565 def _started_channels(self):
566 566 """ Called when the KernelManager channels have started listening or
567 567 when the frontend is assigned an already listening KernelManager.
568 568 """
569 569 self.reset(clear=True)
570 570
571 571 #---------------------------------------------------------------------------
572 572 # 'FrontendWidget' public interface
573 573 #---------------------------------------------------------------------------
574 574
575 575 def copy_raw(self):
576 576 """ Copy the currently selected text to the clipboard without attempting
577 577 to remove prompts or otherwise alter the text.
578 578 """
579 579 self._control.copy()
580 580
581 581 def execute_file(self, path, hidden=False):
582 582 """ Attempts to execute file with 'path'. If 'hidden', no output is
583 583 shown.
584 584 """
585 585 self.execute('execfile(%r)' % path, hidden=hidden)
586 586
587 587 def interrupt_kernel(self):
588 588 """ Attempts to interrupt the running kernel.
589 589
590 590 Also unsets _reading flag, to avoid runtime errors
591 591 if raw_input is called again.
592 592 """
593 593 if self.custom_interrupt:
594 594 self._reading = False
595 595 self.custom_interrupt_requested.emit()
596 596 elif self.kernel_manager:
597 597 self._reading = False
598 598 self.kernel_manager.interrupt_kernel()
599 599 else:
600 600 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
601 601
602 602 def reset(self, clear=False):
603 603 """ Resets the widget to its initial state if ``clear`` parameter
604 604 is True, otherwise
605 605 prints a visual indication of the fact that the kernel restarted, but
606 606 does not clear the traces from previous usage of the kernel before it
607 607 was restarted. With ``clear=True``, it is similar to ``%clear``, but
608 608 also re-writes the banner and aborts execution if necessary.
609 609 """
610 610 if self._executing:
611 611 self._executing = False
612 612 self._request_info['execute'] = {}
613 613 self._reading = False
614 614 self._highlighter.highlighting_on = False
615 615
616 616 if clear:
617 617 self._control.clear()
618 618 self._append_plain_text(self.banner)
619 619 if self.kernel_banner:
620 620 self._append_plain_text(self.kernel_banner)
621 621
622 622 # update output marker for stdout/stderr, so that startup
623 623 # messages appear after banner:
624 624 self._append_before_prompt_pos = self._get_cursor().position()
625 625 self._show_interpreter_prompt()
626 626
627 627 def restart_kernel(self, message, now=False):
628 628 """ Attempts to restart the running kernel.
629 629 """
630 630 # FIXME: now should be configurable via a checkbox in the dialog. Right
631 631 # now at least the heartbeat path sets it to True and the manual restart
632 632 # to False. But those should just be the pre-selected states of a
633 633 # checkbox that the user could override if so desired. But I don't know
634 634 # enough Qt to go implementing the checkbox now.
635 635
636 636 if self.custom_restart:
637 637 self.custom_restart_requested.emit()
638 638 return
639 639
640 640 if self.kernel_manager:
641 641 # Pause the heart beat channel to prevent further warnings.
642 642 self.kernel_client.hb_channel.pause()
643 643
644 644 # Prompt the user to restart the kernel. Un-pause the heartbeat if
645 645 # they decline. (If they accept, the heartbeat will be un-paused
646 646 # automatically when the kernel is restarted.)
647 647 if self.confirm_restart:
648 648 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
649 649 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
650 650 message, buttons)
651 651 do_restart = result == QtGui.QMessageBox.Yes
652 652 else:
653 653 # confirm_restart is False, so we don't need to ask user
654 654 # anything, just do the restart
655 655 do_restart = True
656 656 if do_restart:
657 657 try:
658 658 self.kernel_manager.restart_kernel(now=now)
659 659 except RuntimeError as e:
660 660 self._append_plain_text(
661 661 'Error restarting kernel: %s\n' % e,
662 662 before_prompt=True
663 663 )
664 664 else:
665 665 self._append_html("<br>Restarting kernel...\n<hr><br>",
666 666 before_prompt=True,
667 667 )
668 668 else:
669 669 self.kernel_client.hb_channel.unpause()
670 670
671 671 else:
672 672 self._append_plain_text(
673 673 'Cannot restart a Kernel I did not start\n',
674 674 before_prompt=True
675 675 )
676 676
677 677 def append_stream(self, text):
678 678 """Appends text to the output stream."""
679 679 # Most consoles treat tabs as being 8 space characters. Convert tabs
680 680 # to spaces so that output looks as expected regardless of this
681 681 # widget's tab width.
682 682 text = text.expandtabs(8)
683 683 self._append_plain_text(text, before_prompt=True)
684 684 self._control.moveCursor(QtGui.QTextCursor.End)
685 685
686 686 def flush_clearoutput(self):
687 687 """If a clearoutput is pending, execute it."""
688 688 if self._pending_clearoutput:
689 689 self._pending_clearoutput = False
690 690 self.clear_output()
691 691
692 692 def clear_output(self):
693 693 """Clears the current line of output."""
694 694 cursor = self._control.textCursor()
695 695 cursor.beginEditBlock()
696 696 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
697 697 cursor.insertText('')
698 698 cursor.endEditBlock()
699 699
700 700 #---------------------------------------------------------------------------
701 701 # 'FrontendWidget' protected interface
702 702 #---------------------------------------------------------------------------
703 703
704 704 def _call_tip(self):
705 705 """ Shows a call tip, if appropriate, at the current cursor location.
706 706 """
707 707 # Decide if it makes sense to show a call tip
708 708 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
709 709 return False
710 710 cursor_pos = self._get_input_buffer_cursor_pos()
711 711 code = self.input_buffer
712 712 # Send the metadata request to the kernel
713 713 msg_id = self.kernel_client.inspect(code, cursor_pos)
714 714 pos = self._get_cursor().position()
715 715 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
716 716 return True
717 717
718 718 def _complete(self):
719 719 """ Performs completion at the current cursor location.
720 720 """
721 721 # Send the completion request to the kernel
722 722 msg_id = self.kernel_client.complete(
723 723 code=self.input_buffer,
724 724 cursor_pos=self._get_input_buffer_cursor_pos(),
725 725 )
726 726 pos = self._get_cursor().position()
727 727 info = self._CompletionRequest(msg_id, pos)
728 728 self._request_info['complete'] = info
729 729
730 730 def _process_execute_abort(self, msg):
731 731 """ Process a reply for an aborted execution request.
732 732 """
733 733 self._append_plain_text("ERROR: execution aborted\n")
734 734
735 735 def _process_execute_error(self, msg):
736 736 """ Process a reply for an execution request that resulted in an error.
737 737 """
738 738 content = msg['content']
739 739 # If a SystemExit is passed along, this means exit() was called - also
740 740 # all the ipython %exit magic syntax of '-k' to be used to keep
741 741 # the kernel running
742 742 if content['ename']=='SystemExit':
743 743 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
744 744 self._keep_kernel_on_exit = keepkernel
745 745 self.exit_requested.emit(self)
746 746 else:
747 747 traceback = ''.join(content['traceback'])
748 748 self._append_plain_text(traceback)
749 749
750 750 def _process_execute_ok(self, msg):
751 751 """ Process a reply for a successful execution request.
752 752 """
753 753 payload = msg['content']['payload']
754 754 for item in payload:
755 755 if not self._process_execute_payload(item):
756 756 warning = 'Warning: received unknown payload of type %s'
757 757 print(warning % repr(item['source']))
758 758
759 759 def _process_execute_payload(self, item):
760 760 """ Process a single payload item from the list of payload items in an
761 761 execution reply. Returns whether the payload was handled.
762 762 """
763 763 # The basic FrontendWidget doesn't handle payloads, as they are a
764 764 # mechanism for going beyond the standard Python interpreter model.
765 765 return False
766 766
767 767 def _show_interpreter_prompt(self):
768 768 """ Shows a prompt for the interpreter.
769 769 """
770 770 self._show_prompt('>>> ')
771 771
772 772 def _show_interpreter_prompt_for_reply(self, msg):
773 773 """ Shows a prompt for the interpreter given an 'execute_reply' message.
774 774 """
775 775 self._show_interpreter_prompt()
776 776
777 777 #------ Signal handlers ----------------------------------------------------
778 778
779 779 def _document_contents_change(self, position, removed, added):
780 780 """ Called whenever the document's content changes. Display a call tip
781 781 if appropriate.
782 782 """
783 783 # Calculate where the cursor should be *after* the change:
784 784 position += added
785 785
786 786 document = self._control.document()
787 787 if position == self._get_cursor().position():
788 788 self._call_tip()
789 789
790 790 #------ Trait default initializers -----------------------------------------
791 791
792 792 def _banner_default(self):
793 793 """ Returns the standard Python banner.
794 794 """
795 795 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
796 796 '"license" for more information.'
797 797 return banner % (sys.version, sys.platform)
@@ -1,581 +1,599 b''
1 1 """A FrontendWidget that emulates the interface of the console IPython.
2 2
3 3 This supports the additional functionality provided by the IPython kernel.
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 from collections import namedtuple
10 10 import os.path
11 11 import re
12 12 from subprocess import Popen
13 13 import sys
14 14 import time
15 15 from textwrap import dedent
16 16
17 17 from IPython.external.qt import QtCore, QtGui
18 18
19 19 from IPython.core.inputsplitter import IPythonInputSplitter
20 20 from IPython.core.release import version
21 21 from IPython.core.inputtransformer import ipy_prompt
22 22 from IPython.utils.traitlets import Bool, Unicode
23 23 from .frontend_widget import FrontendWidget
24 24 from . import styles
25 25
26 26 #-----------------------------------------------------------------------------
27 27 # Constants
28 28 #-----------------------------------------------------------------------------
29 29
30 30 # Default strings to build and display input and output prompts (and separators
31 31 # in between)
32 32 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
33 33 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
34 34 default_input_sep = '\n'
35 35 default_output_sep = ''
36 36 default_output_sep2 = ''
37 37
38 38 # Base path for most payload sources.
39 39 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
40 40
41 41 if sys.platform.startswith('win'):
42 42 default_editor = 'notepad'
43 43 else:
44 44 default_editor = ''
45 45
46 46 #-----------------------------------------------------------------------------
47 47 # IPythonWidget class
48 48 #-----------------------------------------------------------------------------
49 49
50 50 class IPythonWidget(FrontendWidget):
51 51 """ A FrontendWidget for an IPython kernel.
52 52 """
53 53
54 54 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
55 55 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
56 56 # settings.
57 57 custom_edit = Bool(False)
58 58 custom_edit_requested = QtCore.Signal(object, object)
59 59
60 60 editor = Unicode(default_editor, config=True,
61 61 help="""
62 62 A command for invoking a system text editor. If the string contains a
63 63 {filename} format specifier, it will be used. Otherwise, the filename
64 64 will be appended to the end the command.
65 65 """)
66 66
67 67 editor_line = Unicode(config=True,
68 68 help="""
69 69 The editor command to use when a specific line number is requested. The
70 70 string should contain two format specifiers: {line} and {filename}. If
71 71 this parameter is not specified, the line number option to the %edit
72 72 magic will be ignored.
73 73 """)
74 74
75 75 style_sheet = Unicode(config=True,
76 76 help="""
77 77 A CSS stylesheet. The stylesheet can contain classes for:
78 78 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
79 79 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
80 80 3. IPython: .error, .in-prompt, .out-prompt, etc
81 81 """)
82 82
83 83 syntax_style = Unicode(config=True,
84 84 help="""
85 85 If not empty, use this Pygments style for syntax highlighting.
86 86 Otherwise, the style sheet is queried for Pygments style
87 87 information.
88 88 """)
89 89
90 90 # Prompts.
91 91 in_prompt = Unicode(default_in_prompt, config=True)
92 92 out_prompt = Unicode(default_out_prompt, config=True)
93 93 input_sep = Unicode(default_input_sep, config=True)
94 94 output_sep = Unicode(default_output_sep, config=True)
95 95 output_sep2 = Unicode(default_output_sep2, config=True)
96 96
97 97 # FrontendWidget protected class variables.
98 98 _input_splitter_class = IPythonInputSplitter
99 99 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
100 100 logical_line_transforms=[],
101 101 python_line_transforms=[],
102 102 )
103 103
104 104 # IPythonWidget protected class variables.
105 105 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
106 106 _payload_source_edit = 'edit_magic'
107 107 _payload_source_exit = 'ask_exit'
108 108 _payload_source_next_input = 'set_next_input'
109 109 _payload_source_page = 'page'
110 110 _retrying_history_request = False
111 111 _starting = False
112 112
113 113 #---------------------------------------------------------------------------
114 114 # 'object' interface
115 115 #---------------------------------------------------------------------------
116 116
117 117 def __init__(self, *args, **kw):
118 118 super(IPythonWidget, self).__init__(*args, **kw)
119 119
120 120 # IPythonWidget protected variables.
121 121 self._payload_handlers = {
122 122 self._payload_source_edit : self._handle_payload_edit,
123 123 self._payload_source_exit : self._handle_payload_exit,
124 124 self._payload_source_page : self._handle_payload_page,
125 125 self._payload_source_next_input : self._handle_payload_next_input }
126 126 self._previous_prompt_obj = None
127 127 self._keep_kernel_on_exit = None
128 128
129 129 # Initialize widget styling.
130 130 if self.style_sheet:
131 131 self._style_sheet_changed()
132 132 self._syntax_style_changed()
133 133 else:
134 134 self.set_default_style()
135 135
136 136 self._guiref_loaded = False
137 137
138 138 #---------------------------------------------------------------------------
139 139 # 'BaseFrontendMixin' abstract interface
140 140 #---------------------------------------------------------------------------
141 141 def _handle_complete_reply(self, rep):
142 142 """ Reimplemented to support IPython's improved completion machinery.
143 143 """
144 144 self.log.debug("complete: %s", rep.get('content', ''))
145 145 cursor = self._get_cursor()
146 146 info = self._request_info.get('complete')
147 147 if info and info.id == rep['parent_header']['msg_id'] and \
148 148 info.pos == cursor.position():
149 149 content = rep['content']
150 150 matches = content['matches']
151 151 start = content['cursor_start']
152 152 end = content['cursor_end']
153 153
154 154 start = max(start, 0)
155 155 end = max(end, start)
156 156
157 157 # Move the control's cursor to the desired end point
158 158 cursor_pos = self._get_input_buffer_cursor_pos()
159 159 if end < cursor_pos:
160 160 cursor.movePosition(QtGui.QTextCursor.Left,
161 161 n=(cursor_pos - end))
162 162 elif end > cursor_pos:
163 163 cursor.movePosition(QtGui.QTextCursor.Right,
164 164 n=(end - cursor_pos))
165 165 # This line actually applies the move to control's cursor
166 166 self._control.setTextCursor(cursor)
167 167
168 168 offset = end - start
169 169 # Move the local cursor object to the start of the match and
170 170 # complete.
171 171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
172 172 self._complete_with_items(cursor, matches)
173 173
174 174 def _handle_execute_reply(self, msg):
175 175 """ Reimplemented to support prompt requests.
176 176 """
177 177 msg_id = msg['parent_header'].get('msg_id')
178 178 info = self._request_info['execute'].get(msg_id)
179 179 if info and info.kind == 'prompt':
180 180 content = msg['content']
181 181 if content['status'] == 'aborted':
182 182 self._show_interpreter_prompt()
183 183 else:
184 184 number = content['execution_count'] + 1
185 185 self._show_interpreter_prompt(number)
186 186 self._request_info['execute'].pop(msg_id)
187 187 else:
188 188 super(IPythonWidget, self)._handle_execute_reply(msg)
189 189
190 190 def _handle_history_reply(self, msg):
191 191 """ Implemented to handle history tail replies, which are only supported
192 192 by the IPython kernel.
193 193 """
194 194 content = msg['content']
195 195 if 'history' not in content:
196 196 self.log.error("History request failed: %r"%content)
197 197 if content.get('status', '') == 'aborted' and \
198 198 not self._retrying_history_request:
199 199 # a *different* action caused this request to be aborted, so
200 200 # we should try again.
201 201 self.log.error("Retrying aborted history request")
202 202 # prevent multiple retries of aborted requests:
203 203 self._retrying_history_request = True
204 204 # wait out the kernel's queue flush, which is currently timed at 0.1s
205 205 time.sleep(0.25)
206 206 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
207 207 else:
208 208 self._retrying_history_request = False
209 209 return
210 210 # reset retry flag
211 211 self._retrying_history_request = False
212 212 history_items = content['history']
213 213 self.log.debug("Received history reply with %i entries", len(history_items))
214 214 items = []
215 215 last_cell = u""
216 216 for _, _, cell in history_items:
217 217 cell = cell.rstrip()
218 218 if cell != last_cell:
219 219 items.append(cell)
220 220 last_cell = cell
221 221 self._set_history(items)
222
223 def _insert_other_input(self, cursor, content):
224 """Insert function for input from other frontends"""
225 cursor.beginEditBlock()
226 start = cursor.position()
227 n = content.get('execution_count', 0)
228 cursor.insertText('\n')
229 self._insert_html(cursor, self._make_in_prompt(n))
230 cursor.insertText(content['code'])
231 self._highlighter.rehighlightBlock(cursor.block())
232 cursor.endEditBlock()
233
234 def _handle_execute_input(self, msg):
235 """Handle an execute_input message"""
236 self.log.debug("execute_input: %s", msg.get('content', ''))
237 if self.include_output(msg):
238 self._append_custom(self._insert_other_input, msg['content'], before_prompt=True)
222 239
240
223 241 def _handle_execute_result(self, msg):
224 242 """ Reimplemented for IPython-style "display hook".
225 243 """
226 244 self.log.debug("execute_result: %s", msg.get('content', ''))
227 if not self._hidden and self._is_from_this_session(msg):
245 if self.include_output(msg):
228 246 self.flush_clearoutput()
229 247 content = msg['content']
230 248 prompt_number = content.get('execution_count', 0)
231 249 data = content['data']
232 250 if 'text/plain' in data:
233 251 self._append_plain_text(self.output_sep, True)
234 252 self._append_html(self._make_out_prompt(prompt_number), True)
235 253 text = data['text/plain']
236 254 # If the repr is multiline, make sure we start on a new line,
237 255 # so that its lines are aligned.
238 256 if "\n" in text and not self.output_sep.endswith("\n"):
239 257 self._append_plain_text('\n', True)
240 258 self._append_plain_text(text + self.output_sep2, True)
241 259
242 260 def _handle_display_data(self, msg):
243 261 """ The base handler for the ``display_data`` message.
244 262 """
245 263 self.log.debug("display: %s", msg.get('content', ''))
246 264 # For now, we don't display data from other frontends, but we
247 265 # eventually will as this allows all frontends to monitor the display
248 266 # data. But we need to figure out how to handle this in the GUI.
249 if not self._hidden and self._is_from_this_session(msg):
267 if self.include_output(msg):
250 268 self.flush_clearoutput()
251 269 data = msg['content']['data']
252 270 metadata = msg['content']['metadata']
253 271 # In the regular IPythonWidget, we simply print the plain text
254 272 # representation.
255 273 if 'text/plain' in data:
256 274 text = data['text/plain']
257 275 self._append_plain_text(text, True)
258 276 # This newline seems to be needed for text and html output.
259 277 self._append_plain_text(u'\n', True)
260 278
261 279 def _handle_kernel_info_reply(self, rep):
262 280 """Handle kernel info replies."""
263 281 content = rep['content']
264 282 if not self._guiref_loaded:
265 283 if content.get('language') == 'python':
266 284 self._load_guiref_magic()
267 285 self._guiref_loaded = True
268 286
269 287 self.kernel_banner = content.get('banner', '')
270 288 if self._starting:
271 289 # finish handling started channels
272 290 self._starting = False
273 291 super(IPythonWidget, self)._started_channels()
274 292
275 293 def _started_channels(self):
276 294 """Reimplemented to make a history request and load %guiref."""
277 295 self._starting = True
278 296 # The reply will trigger %guiref load provided language=='python'
279 297 self.kernel_client.kernel_info()
280 298
281 299 self.kernel_client.shell_channel.history(hist_access_type='tail',
282 300 n=1000)
283 301
284 302 def _load_guiref_magic(self):
285 303 """Load %guiref magic."""
286 304 self.kernel_client.shell_channel.execute('\n'.join([
287 305 "try:",
288 306 " _usage",
289 307 "except:",
290 308 " from IPython.core import usage as _usage",
291 309 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
292 310 " del _usage",
293 311 ]), silent=True)
294 312
295 313 #---------------------------------------------------------------------------
296 314 # 'ConsoleWidget' public interface
297 315 #---------------------------------------------------------------------------
298 316
299 317 #---------------------------------------------------------------------------
300 318 # 'FrontendWidget' public interface
301 319 #---------------------------------------------------------------------------
302 320
303 321 def execute_file(self, path, hidden=False):
304 322 """ Reimplemented to use the 'run' magic.
305 323 """
306 324 # Use forward slashes on Windows to avoid escaping each separator.
307 325 if sys.platform == 'win32':
308 326 path = os.path.normpath(path).replace('\\', '/')
309 327
310 328 # Perhaps we should not be using %run directly, but while we
311 329 # are, it is necessary to quote or escape filenames containing spaces
312 330 # or quotes.
313 331
314 332 # In earlier code here, to minimize escaping, we sometimes quoted the
315 333 # filename with single quotes. But to do this, this code must be
316 334 # platform-aware, because run uses shlex rather than python string
317 335 # parsing, so that:
318 336 # * In Win: single quotes can be used in the filename without quoting,
319 337 # and we cannot use single quotes to quote the filename.
320 338 # * In *nix: we can escape double quotes in a double quoted filename,
321 339 # but can't escape single quotes in a single quoted filename.
322 340
323 341 # So to keep this code non-platform-specific and simple, we now only
324 342 # use double quotes to quote filenames, and escape when needed:
325 343 if ' ' in path or "'" in path or '"' in path:
326 344 path = '"%s"' % path.replace('"', '\\"')
327 345 self.execute('%%run %s' % path, hidden=hidden)
328 346
329 347 #---------------------------------------------------------------------------
330 348 # 'FrontendWidget' protected interface
331 349 #---------------------------------------------------------------------------
332 350
333 351 def _process_execute_error(self, msg):
334 352 """ Reimplemented for IPython-style traceback formatting.
335 353 """
336 354 content = msg['content']
337 355 traceback = '\n'.join(content['traceback']) + '\n'
338 356 if False:
339 357 # FIXME: For now, tracebacks come as plain text, so we can't use
340 358 # the html renderer yet. Once we refactor ultratb to produce
341 359 # properly styled tracebacks, this branch should be the default
342 360 traceback = traceback.replace(' ', '&nbsp;')
343 361 traceback = traceback.replace('\n', '<br/>')
344 362
345 363 ename = content['ename']
346 364 ename_styled = '<span class="error">%s</span>' % ename
347 365 traceback = traceback.replace(ename, ename_styled)
348 366
349 367 self._append_html(traceback)
350 368 else:
351 369 # This is the fallback for now, using plain text with ansi escapes
352 370 self._append_plain_text(traceback)
353 371
354 372 def _process_execute_payload(self, item):
355 373 """ Reimplemented to dispatch payloads to handler methods.
356 374 """
357 375 handler = self._payload_handlers.get(item['source'])
358 376 if handler is None:
359 377 # We have no handler for this type of payload, simply ignore it
360 378 return False
361 379 else:
362 380 handler(item)
363 381 return True
364 382
365 383 def _show_interpreter_prompt(self, number=None):
366 384 """ Reimplemented for IPython-style prompts.
367 385 """
368 386 # If a number was not specified, make a prompt number request.
369 387 if number is None:
370 388 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
371 389 info = self._ExecutionRequest(msg_id, 'prompt')
372 390 self._request_info['execute'][msg_id] = info
373 391 return
374 392
375 393 # Show a new prompt and save information about it so that it can be
376 394 # updated later if the prompt number turns out to be wrong.
377 395 self._prompt_sep = self.input_sep
378 396 self._show_prompt(self._make_in_prompt(number), html=True)
379 397 block = self._control.document().lastBlock()
380 398 length = len(self._prompt)
381 399 self._previous_prompt_obj = self._PromptBlock(block, length, number)
382 400
383 401 # Update continuation prompt to reflect (possibly) new prompt length.
384 402 self._set_continuation_prompt(
385 403 self._make_continuation_prompt(self._prompt), html=True)
386 404
387 405 def _show_interpreter_prompt_for_reply(self, msg):
388 406 """ Reimplemented for IPython-style prompts.
389 407 """
390 408 # Update the old prompt number if necessary.
391 409 content = msg['content']
392 410 # abort replies do not have any keys:
393 411 if content['status'] == 'aborted':
394 412 if self._previous_prompt_obj:
395 413 previous_prompt_number = self._previous_prompt_obj.number
396 414 else:
397 415 previous_prompt_number = 0
398 416 else:
399 417 previous_prompt_number = content['execution_count']
400 418 if self._previous_prompt_obj and \
401 419 self._previous_prompt_obj.number != previous_prompt_number:
402 420 block = self._previous_prompt_obj.block
403 421
404 422 # Make sure the prompt block has not been erased.
405 423 if block.isValid() and block.text():
406 424
407 425 # Remove the old prompt and insert a new prompt.
408 426 cursor = QtGui.QTextCursor(block)
409 427 cursor.movePosition(QtGui.QTextCursor.Right,
410 428 QtGui.QTextCursor.KeepAnchor,
411 429 self._previous_prompt_obj.length)
412 430 prompt = self._make_in_prompt(previous_prompt_number)
413 431 self._prompt = self._insert_html_fetching_plain_text(
414 432 cursor, prompt)
415 433
416 434 # When the HTML is inserted, Qt blows away the syntax
417 435 # highlighting for the line, so we need to rehighlight it.
418 436 self._highlighter.rehighlightBlock(cursor.block())
419 437
420 438 self._previous_prompt_obj = None
421 439
422 440 # Show a new prompt with the kernel's estimated prompt number.
423 441 self._show_interpreter_prompt(previous_prompt_number + 1)
424 442
425 443 #---------------------------------------------------------------------------
426 444 # 'IPythonWidget' interface
427 445 #---------------------------------------------------------------------------
428 446
429 447 def set_default_style(self, colors='lightbg'):
430 448 """ Sets the widget style to the class defaults.
431 449
432 450 Parameters
433 451 ----------
434 452 colors : str, optional (default lightbg)
435 453 Whether to use the default IPython light background or dark
436 454 background or B&W style.
437 455 """
438 456 colors = colors.lower()
439 457 if colors=='lightbg':
440 458 self.style_sheet = styles.default_light_style_sheet
441 459 self.syntax_style = styles.default_light_syntax_style
442 460 elif colors=='linux':
443 461 self.style_sheet = styles.default_dark_style_sheet
444 462 self.syntax_style = styles.default_dark_syntax_style
445 463 elif colors=='nocolor':
446 464 self.style_sheet = styles.default_bw_style_sheet
447 465 self.syntax_style = styles.default_bw_syntax_style
448 466 else:
449 467 raise KeyError("No such color scheme: %s"%colors)
450 468
451 469 #---------------------------------------------------------------------------
452 470 # 'IPythonWidget' protected interface
453 471 #---------------------------------------------------------------------------
454 472
455 473 def _edit(self, filename, line=None):
456 474 """ Opens a Python script for editing.
457 475
458 476 Parameters
459 477 ----------
460 478 filename : str
461 479 A path to a local system file.
462 480
463 481 line : int, optional
464 482 A line of interest in the file.
465 483 """
466 484 if self.custom_edit:
467 485 self.custom_edit_requested.emit(filename, line)
468 486 elif not self.editor:
469 487 self._append_plain_text('No default editor available.\n'
470 488 'Specify a GUI text editor in the `IPythonWidget.editor` '
471 489 'configurable to enable the %edit magic')
472 490 else:
473 491 try:
474 492 filename = '"%s"' % filename
475 493 if line and self.editor_line:
476 494 command = self.editor_line.format(filename=filename,
477 495 line=line)
478 496 else:
479 497 try:
480 498 command = self.editor.format()
481 499 except KeyError:
482 500 command = self.editor.format(filename=filename)
483 501 else:
484 502 command += ' ' + filename
485 503 except KeyError:
486 504 self._append_plain_text('Invalid editor command.\n')
487 505 else:
488 506 try:
489 507 Popen(command, shell=True)
490 508 except OSError:
491 509 msg = 'Opening editor with command "%s" failed.\n'
492 510 self._append_plain_text(msg % command)
493 511
494 512 def _make_in_prompt(self, number):
495 513 """ Given a prompt number, returns an HTML In prompt.
496 514 """
497 515 try:
498 516 body = self.in_prompt % number
499 517 except TypeError:
500 518 # allow in_prompt to leave out number, e.g. '>>> '
501 519 from xml.sax.saxutils import escape
502 520 body = escape(self.in_prompt)
503 521 return '<span class="in-prompt">%s</span>' % body
504 522
505 523 def _make_continuation_prompt(self, prompt):
506 524 """ Given a plain text version of an In prompt, returns an HTML
507 525 continuation prompt.
508 526 """
509 527 end_chars = '...: '
510 528 space_count = len(prompt.lstrip('\n')) - len(end_chars)
511 529 body = '&nbsp;' * space_count + end_chars
512 530 return '<span class="in-prompt">%s</span>' % body
513 531
514 532 def _make_out_prompt(self, number):
515 533 """ Given a prompt number, returns an HTML Out prompt.
516 534 """
517 535 try:
518 536 body = self.out_prompt % number
519 537 except TypeError:
520 538 # allow out_prompt to leave out number, e.g. '<<< '
521 539 from xml.sax.saxutils import escape
522 540 body = escape(self.out_prompt)
523 541 return '<span class="out-prompt">%s</span>' % body
524 542
525 543 #------ Payload handlers --------------------------------------------------
526 544
527 545 # Payload handlers with a generic interface: each takes the opaque payload
528 546 # dict, unpacks it and calls the underlying functions with the necessary
529 547 # arguments.
530 548
531 549 def _handle_payload_edit(self, item):
532 550 self._edit(item['filename'], item['line_number'])
533 551
534 552 def _handle_payload_exit(self, item):
535 553 self._keep_kernel_on_exit = item['keepkernel']
536 554 self.exit_requested.emit(self)
537 555
538 556 def _handle_payload_next_input(self, item):
539 557 self.input_buffer = item['text']
540 558
541 559 def _handle_payload_page(self, item):
542 560 # Since the plain text widget supports only a very small subset of HTML
543 561 # and we have no control over the HTML source, we only page HTML
544 562 # payloads in the rich text widget.
545 563 data = item['data']
546 564 if 'text/html' in data and self.kind == 'rich':
547 565 self._page(data['text/html'], html=True)
548 566 else:
549 567 self._page(data['text/plain'], html=False)
550 568
551 569 #------ Trait change handlers --------------------------------------------
552 570
553 571 def _style_sheet_changed(self):
554 572 """ Set the style sheets of the underlying widgets.
555 573 """
556 574 self.setStyleSheet(self.style_sheet)
557 575 if self._control is not None:
558 576 self._control.document().setDefaultStyleSheet(self.style_sheet)
559 577 bg_color = self._control.palette().window().color()
560 578 self._ansi_processor.set_background_color(bg_color)
561 579
562 580 if self._page_control is not None:
563 581 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
564 582
565 583
566 584
567 585 def _syntax_style_changed(self):
568 586 """ Set the style for the syntax highlighter.
569 587 """
570 588 if self._highlighter is None:
571 589 # ignore premature calls
572 590 return
573 591 if self.syntax_style:
574 592 self._highlighter.set_style(self.syntax_style)
575 593 else:
576 594 self._highlighter.set_style_sheet(self.style_sheet)
577 595
578 596 #------ Trait default initializers -----------------------------------------
579 597
580 598 def _banner_default(self):
581 599 return "IPython QtConsole {version}\n".format(version=version)
@@ -1,347 +1,347 b''
1 1 # Copyright (c) IPython Development Team.
2 2 # Distributed under the terms of the Modified BSD License.
3 3
4 4 from base64 import decodestring
5 5 import os
6 6 import re
7 7
8 8 from IPython.external.qt import QtCore, QtGui
9 9
10 10 from IPython.lib.latextools import latex_to_png
11 11 from IPython.utils.path import ensure_dir_exists
12 12 from IPython.utils.traitlets import Bool
13 13 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
14 14 from .ipython_widget import IPythonWidget
15 15
16 16
17 17 class RichIPythonWidget(IPythonWidget):
18 18 """ An IPythonWidget that supports rich text, including lists, images, and
19 19 tables. Note that raw performance will be reduced compared to the plain
20 20 text version.
21 21 """
22 22
23 23 # RichIPythonWidget protected class variables.
24 24 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
25 25 _jpg_supported = Bool(False)
26 26
27 27 # Used to determine whether a given html export attempt has already
28 28 # displayed a warning about being unable to convert a png to svg.
29 29 _svg_warning_displayed = False
30 30
31 31 #---------------------------------------------------------------------------
32 32 # 'object' interface
33 33 #---------------------------------------------------------------------------
34 34
35 35 def __init__(self, *args, **kw):
36 36 """ Create a RichIPythonWidget.
37 37 """
38 38 kw['kind'] = 'rich'
39 39 super(RichIPythonWidget, self).__init__(*args, **kw)
40 40
41 41 # Configure the ConsoleWidget HTML exporter for our formats.
42 42 self._html_exporter.image_tag = self._get_image_tag
43 43
44 44 # Dictionary for resolving document resource names to SVG data.
45 45 self._name_to_svg_map = {}
46 46
47 47 # Do we support jpg ?
48 48 # it seems that sometime jpg support is a plugin of QT, so try to assume
49 49 # it is not always supported.
50 50 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
51 51 self._jpg_supported = 'jpeg' in _supported_format
52 52
53 53
54 54 #---------------------------------------------------------------------------
55 55 # 'ConsoleWidget' public interface overides
56 56 #---------------------------------------------------------------------------
57 57
58 58 def export_html(self):
59 59 """ Shows a dialog to export HTML/XML in various formats.
60 60
61 61 Overridden in order to reset the _svg_warning_displayed flag prior
62 62 to the export running.
63 63 """
64 64 self._svg_warning_displayed = False
65 65 super(RichIPythonWidget, self).export_html()
66 66
67 67
68 68 #---------------------------------------------------------------------------
69 69 # 'ConsoleWidget' protected interface
70 70 #---------------------------------------------------------------------------
71 71
72 72 def _context_menu_make(self, pos):
73 73 """ Reimplemented to return a custom context menu for images.
74 74 """
75 75 format = self._control.cursorForPosition(pos).charFormat()
76 76 name = format.stringProperty(QtGui.QTextFormat.ImageName)
77 77 if name:
78 78 menu = QtGui.QMenu()
79 79
80 80 menu.addAction('Copy Image', lambda: self._copy_image(name))
81 81 menu.addAction('Save Image As...', lambda: self._save_image(name))
82 82 menu.addSeparator()
83 83
84 84 svg = self._name_to_svg_map.get(name, None)
85 85 if svg is not None:
86 86 menu.addSeparator()
87 87 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
88 88 menu.addAction('Save SVG As...',
89 89 lambda: save_svg(svg, self._control))
90 90 else:
91 91 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
92 92 return menu
93 93
94 94 #---------------------------------------------------------------------------
95 95 # 'BaseFrontendMixin' abstract interface
96 96 #---------------------------------------------------------------------------
97 97 def _pre_image_append(self, msg, prompt_number):
98 98 """ Append the Out[] prompt and make the output nicer
99 99
100 100 Shared code for some the following if statement
101 101 """
102 102 self.log.debug("execute_result: %s", msg.get('content', ''))
103 103 self._append_plain_text(self.output_sep, True)
104 104 self._append_html(self._make_out_prompt(prompt_number), True)
105 105 self._append_plain_text('\n', True)
106 106
107 107 def _handle_execute_result(self, msg):
108 108 """ Overridden to handle rich data types, like SVG.
109 109 """
110 if not self._hidden and self._is_from_this_session(msg):
110 if self.include_output(msg):
111 111 self.flush_clearoutput()
112 112 content = msg['content']
113 113 prompt_number = content.get('execution_count', 0)
114 114 data = content['data']
115 115 metadata = msg['content']['metadata']
116 116 if 'image/svg+xml' in data:
117 117 self._pre_image_append(msg, prompt_number)
118 118 self._append_svg(data['image/svg+xml'], True)
119 119 self._append_html(self.output_sep2, True)
120 120 elif 'image/png' in data:
121 121 self._pre_image_append(msg, prompt_number)
122 122 png = decodestring(data['image/png'].encode('ascii'))
123 123 self._append_png(png, True, metadata=metadata.get('image/png', None))
124 124 self._append_html(self.output_sep2, True)
125 125 elif 'image/jpeg' in data and self._jpg_supported:
126 126 self._pre_image_append(msg, prompt_number)
127 127 jpg = decodestring(data['image/jpeg'].encode('ascii'))
128 128 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
129 129 self._append_html(self.output_sep2, True)
130 130 elif 'text/latex' in data:
131 131 self._pre_image_append(msg, prompt_number)
132 132 latex = data['text/latex'].encode('ascii')
133 133 # latex_to_png takes care of handling $
134 134 latex = latex.strip('$')
135 135 png = latex_to_png(latex, wrap=True)
136 136 if png is not None:
137 137 self._append_png(png, True)
138 138 self._append_html(self.output_sep2, True)
139 139 else:
140 140 # Print plain text if png can't be generated
141 141 return super(RichIPythonWidget, self)._handle_execute_result(msg)
142 142 else:
143 143 # Default back to the plain text representation.
144 144 return super(RichIPythonWidget, self)._handle_execute_result(msg)
145 145
146 146 def _handle_display_data(self, msg):
147 147 """ Overridden to handle rich data types, like SVG.
148 148 """
149 if not self._hidden and self._is_from_this_session(msg):
149 if self.include_output(msg):
150 150 self.flush_clearoutput()
151 151 data = msg['content']['data']
152 152 metadata = msg['content']['metadata']
153 153 # Try to use the svg or html representations.
154 154 # FIXME: Is this the right ordering of things to try?
155 155 if 'image/svg+xml' in data:
156 156 self.log.debug("display: %s", msg.get('content', ''))
157 157 svg = data['image/svg+xml']
158 158 self._append_svg(svg, True)
159 159 elif 'image/png' in data:
160 160 self.log.debug("display: %s", msg.get('content', ''))
161 161 # PNG data is base64 encoded as it passes over the network
162 162 # in a JSON structure so we decode it.
163 163 png = decodestring(data['image/png'].encode('ascii'))
164 164 self._append_png(png, True, metadata=metadata.get('image/png', None))
165 165 elif 'image/jpeg' in data and self._jpg_supported:
166 166 self.log.debug("display: %s", msg.get('content', ''))
167 167 jpg = decodestring(data['image/jpeg'].encode('ascii'))
168 168 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
169 169 else:
170 170 # Default back to the plain text representation.
171 171 return super(RichIPythonWidget, self)._handle_display_data(msg)
172 172
173 173 #---------------------------------------------------------------------------
174 174 # 'RichIPythonWidget' protected interface
175 175 #---------------------------------------------------------------------------
176 176
177 177 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
178 178 """ Append raw JPG data to the widget."""
179 179 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
180 180
181 181 def _append_png(self, png, before_prompt=False, metadata=None):
182 182 """ Append raw PNG data to the widget.
183 183 """
184 184 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
185 185
186 186 def _append_svg(self, svg, before_prompt=False):
187 187 """ Append raw SVG data to the widget.
188 188 """
189 189 self._append_custom(self._insert_svg, svg, before_prompt)
190 190
191 191 def _add_image(self, image):
192 192 """ Adds the specified QImage to the document and returns a
193 193 QTextImageFormat that references it.
194 194 """
195 195 document = self._control.document()
196 196 name = str(image.cacheKey())
197 197 document.addResource(QtGui.QTextDocument.ImageResource,
198 198 QtCore.QUrl(name), image)
199 199 format = QtGui.QTextImageFormat()
200 200 format.setName(name)
201 201 return format
202 202
203 203 def _copy_image(self, name):
204 204 """ Copies the ImageResource with 'name' to the clipboard.
205 205 """
206 206 image = self._get_image(name)
207 207 QtGui.QApplication.clipboard().setImage(image)
208 208
209 209 def _get_image(self, name):
210 210 """ Returns the QImage stored as the ImageResource with 'name'.
211 211 """
212 212 document = self._control.document()
213 213 image = document.resource(QtGui.QTextDocument.ImageResource,
214 214 QtCore.QUrl(name))
215 215 return image
216 216
217 217 def _get_image_tag(self, match, path = None, format = "png"):
218 218 """ Return (X)HTML mark-up for the image-tag given by match.
219 219
220 220 Parameters
221 221 ----------
222 222 match : re.SRE_Match
223 223 A match to an HTML image tag as exported by Qt, with
224 224 match.group("Name") containing the matched image ID.
225 225
226 226 path : string|None, optional [default None]
227 227 If not None, specifies a path to which supporting files may be
228 228 written (e.g., for linked images). If None, all images are to be
229 229 included inline.
230 230
231 231 format : "png"|"svg"|"jpg", optional [default "png"]
232 232 Format for returned or referenced images.
233 233 """
234 234 if format in ("png","jpg"):
235 235 try:
236 236 image = self._get_image(match.group("name"))
237 237 except KeyError:
238 238 return "<b>Couldn't find image %s</b>" % match.group("name")
239 239
240 240 if path is not None:
241 241 ensure_dir_exists(path)
242 242 relpath = os.path.basename(path)
243 243 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
244 244 "PNG"):
245 245 return '<img src="%s/qt_img%s.%s">' % (relpath,
246 246 match.group("name"),format)
247 247 else:
248 248 return "<b>Couldn't save image!</b>"
249 249 else:
250 250 ba = QtCore.QByteArray()
251 251 buffer_ = QtCore.QBuffer(ba)
252 252 buffer_.open(QtCore.QIODevice.WriteOnly)
253 253 image.save(buffer_, format.upper())
254 254 buffer_.close()
255 255 return '<img src="data:image/%s;base64,\n%s\n" />' % (
256 256 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
257 257
258 258 elif format == "svg":
259 259 try:
260 260 svg = str(self._name_to_svg_map[match.group("name")])
261 261 except KeyError:
262 262 if not self._svg_warning_displayed:
263 263 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
264 264 'Cannot convert PNG images to SVG, export with PNG figures instead. '
265 265 'If you want to export matplotlib figures as SVG, add '
266 266 'to your ipython config:\n\n'
267 267 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
268 268 'And regenerate the figures.',
269 269 QtGui.QMessageBox.Ok)
270 270 self._svg_warning_displayed = True
271 271 return ("<b>Cannot convert PNG images to SVG.</b> "
272 272 "You must export this session with PNG images. "
273 273 "If you want to export matplotlib figures as SVG, add to your config "
274 274 "<span>c.InlineBackend.figure_format = 'svg'</span> "
275 275 "and regenerate the figures.")
276 276
277 277 # Not currently checking path, because it's tricky to find a
278 278 # cross-browser way to embed external SVG images (e.g., via
279 279 # object or embed tags).
280 280
281 281 # Chop stand-alone header from matplotlib SVG
282 282 offset = svg.find("<svg")
283 283 assert(offset > -1)
284 284
285 285 return svg[offset:]
286 286
287 287 else:
288 288 return '<b>Unrecognized image format</b>'
289 289
290 290 def _insert_jpg(self, cursor, jpg, metadata=None):
291 291 """ Insert raw PNG data into the widget."""
292 292 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
293 293
294 294 def _insert_png(self, cursor, png, metadata=None):
295 295 """ Insert raw PNG data into the widget.
296 296 """
297 297 self._insert_img(cursor, png, 'png', metadata=metadata)
298 298
299 299 def _insert_img(self, cursor, img, fmt, metadata=None):
300 300 """ insert a raw image, jpg or png """
301 301 if metadata:
302 302 width = metadata.get('width', None)
303 303 height = metadata.get('height', None)
304 304 else:
305 305 width = height = None
306 306 try:
307 307 image = QtGui.QImage()
308 308 image.loadFromData(img, fmt.upper())
309 309 if width and height:
310 310 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
311 311 elif width and not height:
312 312 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
313 313 elif height and not width:
314 314 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
315 315 except ValueError:
316 316 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
317 317 else:
318 318 format = self._add_image(image)
319 319 cursor.insertBlock()
320 320 cursor.insertImage(format)
321 321 cursor.insertBlock()
322 322
323 323 def _insert_svg(self, cursor, svg):
324 324 """ Insert raw SVG data into the widet.
325 325 """
326 326 try:
327 327 image = svg_to_image(svg)
328 328 except ValueError:
329 329 self._insert_plain_text(cursor, 'Received invalid SVG data.')
330 330 else:
331 331 format = self._add_image(image)
332 332 self._name_to_svg_map[format.name()] = svg
333 333 cursor.insertBlock()
334 334 cursor.insertImage(format)
335 335 cursor.insertBlock()
336 336
337 337 def _save_image(self, name, format='PNG'):
338 338 """ Shows a save dialog for the ImageResource with 'name'.
339 339 """
340 340 dialog = QtGui.QFileDialog(self._control, 'Save Image')
341 341 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
342 342 dialog.setDefaultSuffix(format.lower())
343 343 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
344 344 if dialog.exec_():
345 345 filename = dialog.selectedFiles()[0]
346 346 image = self._get_image(name)
347 347 image.save(filename, format)
@@ -1,528 +1,570 b''
1 1 # -*- coding: utf-8 -*-
2 2 """terminal client to the IPython kernel"""
3 3
4 4 # Copyright (c) IPython Development Team.
5 5 # Distributed under the terms of the Modified BSD License.
6 6
7 7 from __future__ import print_function
8 8
9 9 import base64
10 10 import bdb
11 11 import signal
12 12 import os
13 13 import sys
14 14 import time
15 15 import subprocess
16 16 from getpass import getpass
17 17 from io import BytesIO
18 18
19 19 try:
20 20 from queue import Empty # Py 3
21 21 except ImportError:
22 22 from Queue import Empty # Py 2
23 23
24 24 from IPython.core import page
25 25 from IPython.core import release
26 26 from IPython.utils.warn import warn, error
27 27 from IPython.utils import io
28 28 from IPython.utils.py3compat import string_types, input
29 from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float
29 from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float, Bool
30 30 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
31 31
32 32 from IPython.terminal.interactiveshell import TerminalInteractiveShell
33 33 from IPython.terminal.console.completer import ZMQCompleter
34 34
35 35
36 36 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
37 37 """A subclass of TerminalInteractiveShell that uses the 0MQ kernel"""
38 38 _executing = False
39 39 _execution_state = Unicode('')
40 40 _pending_clearoutput = False
41 41 kernel_banner = Unicode('')
42 42 kernel_timeout = Float(60, config=True,
43 43 help="""Timeout for giving up on a kernel (in seconds).
44 44
45 45 On first connect and restart, the console tests whether the
46 46 kernel is running and responsive by sending kernel_info_requests.
47 47 This sets the timeout in seconds for how long the kernel can take
48 48 before being presumed dead.
49 49 """
50 50 )
51 51
52 52 image_handler = Enum(('PIL', 'stream', 'tempfile', 'callable'),
53 53 config=True, help=
54 54 """
55 55 Handler for image type output. This is useful, for example,
56 56 when connecting to the kernel in which pylab inline backend is
57 57 activated. There are four handlers defined. 'PIL': Use
58 58 Python Imaging Library to popup image; 'stream': Use an
59 59 external program to show the image. Image will be fed into
60 60 the STDIN of the program. You will need to configure
61 61 `stream_image_handler`; 'tempfile': Use an external program to
62 62 show the image. Image will be saved in a temporally file and
63 63 the program is called with the temporally file. You will need
64 64 to configure `tempfile_image_handler`; 'callable': You can set
65 65 any Python callable which is called with the image data. You
66 66 will need to configure `callable_image_handler`.
67 67 """
68 68 )
69 69
70 70 stream_image_handler = List(config=True, help=
71 71 """
72 72 Command to invoke an image viewer program when you are using
73 73 'stream' image handler. This option is a list of string where
74 74 the first element is the command itself and reminders are the
75 75 options for the command. Raw image data is given as STDIN to
76 76 the program.
77 77 """
78 78 )
79 79
80 80 tempfile_image_handler = List(config=True, help=
81 81 """
82 82 Command to invoke an image viewer program when you are using
83 83 'tempfile' image handler. This option is a list of string
84 84 where the first element is the command itself and reminders
85 85 are the options for the command. You can use {file} and
86 86 {format} in the string to represent the location of the
87 87 generated image file and image format.
88 88 """
89 89 )
90 90
91 91 callable_image_handler = Any(config=True, help=
92 92 """
93 93 Callable object called via 'callable' image handler with one
94 94 argument, `data`, which is `msg["content"]["data"]` where
95 95 `msg` is the message from iopub channel. For exmaple, you can
96 96 find base64 encoded PNG data as `data['image/png']`.
97 97 """
98 98 )
99 99
100 100 mime_preference = List(
101 101 default_value=['image/png', 'image/jpeg', 'image/svg+xml'],
102 102 config=True, allow_none=False, help=
103 103 """
104 104 Preferred object representation MIME type in order. First
105 105 matched MIME type will be used.
106 106 """
107 107 )
108 108
109 109 manager = Instance('IPython.kernel.KernelManager')
110 110 client = Instance('IPython.kernel.KernelClient')
111 111 def _client_changed(self, name, old, new):
112 112 self.session_id = new.session.session
113 113 session_id = Unicode()
114 114
115 115 def init_completer(self):
116 116 """Initialize the completion machinery.
117 117
118 118 This creates completion machinery that can be used by client code,
119 119 either interactively in-process (typically triggered by the readline
120 120 library), programmatically (such as in test suites) or out-of-process
121 121 (typically over the network by remote frontends).
122 122 """
123 123 from IPython.core.completerlib import (module_completer,
124 124 magic_run_completer, cd_completer)
125 125
126 126 self.Completer = ZMQCompleter(self, self.client, config=self.config)
127 127
128 128
129 129 self.set_hook('complete_command', module_completer, str_key = 'import')
130 130 self.set_hook('complete_command', module_completer, str_key = 'from')
131 131 self.set_hook('complete_command', magic_run_completer, str_key = '%run')
132 132 self.set_hook('complete_command', cd_completer, str_key = '%cd')
133 133
134 134 # Only configure readline if we truly are using readline. IPython can
135 135 # do tab-completion over the network, in GUIs, etc, where readline
136 136 # itself may be absent
137 137 if self.has_readline:
138 138 self.set_readline_completer()
139 139
140 140 def run_cell(self, cell, store_history=True):
141 141 """Run a complete IPython cell.
142 142
143 143 Parameters
144 144 ----------
145 145 cell : str
146 146 The code (including IPython code such as %magic functions) to run.
147 147 store_history : bool
148 148 If True, the raw and translated cell will be stored in IPython's
149 149 history. For user code calling back into IPython's machinery, this
150 150 should be set to False.
151 151 """
152 152 if (not cell) or cell.isspace():
153 153 # pressing enter flushes any pending display
154 154 self.handle_iopub()
155 155 return
156 156
157 157 # flush stale replies, which could have been ignored, due to missed heartbeats
158 158 while self.client.shell_channel.msg_ready():
159 159 self.client.shell_channel.get_msg()
160 160 # shell_channel.execute takes 'hidden', which is the inverse of store_hist
161 161 msg_id = self.client.shell_channel.execute(cell, not store_history)
162 162
163 163 # first thing is wait for any side effects (output, stdin, etc.)
164 164 self._executing = True
165 165 self._execution_state = "busy"
166 166 while self._execution_state != 'idle' and self.client.is_alive():
167 167 try:
168 168 self.handle_input_request(msg_id, timeout=0.05)
169 169 except Empty:
170 170 # display intermediate print statements, etc.
171 171 self.handle_iopub(msg_id)
172 172
173 173 # after all of that is done, wait for the execute reply
174 174 while self.client.is_alive():
175 175 try:
176 176 self.handle_execute_reply(msg_id, timeout=0.05)
177 177 except Empty:
178 178 pass
179 179 else:
180 180 break
181 181 self._executing = False
182 182
183 183 #-----------------
184 184 # message handlers
185 185 #-----------------
186 186
187 187 def handle_execute_reply(self, msg_id, timeout=None):
188 188 msg = self.client.shell_channel.get_msg(block=False, timeout=timeout)
189 189 if msg["parent_header"].get("msg_id", None) == msg_id:
190 190
191 191 self.handle_iopub(msg_id)
192 192
193 193 content = msg["content"]
194 194 status = content['status']
195 195
196 196 if status == 'aborted':
197 197 self.write('Aborted\n')
198 198 return
199 199 elif status == 'ok':
200 200 # handle payloads
201 201 for item in content["payload"]:
202 202 source = item['source']
203 203 if source == 'page':
204 204 page.page(item['data']['text/plain'])
205 205 elif source == 'set_next_input':
206 206 self.set_next_input(item['text'])
207 207 elif source == 'ask_exit':
208 208 self.ask_exit()
209 209
210 210 elif status == 'error':
211 211 for frame in content["traceback"]:
212 212 print(frame, file=io.stderr)
213 213
214 214 self.execution_count = int(content["execution_count"] + 1)
215
216
215
216 include_other_output = Bool(False, config=True,
217 help="""Whether to include output from clients
218 other than this one sharing the same kernel.
219
220 Outputs are not displayed until enter is pressed.
221 """
222 )
223 other_output_prefix = Unicode("[remote] ", config=True,
224 help="""Prefix to add to outputs coming from clients other than this one.
225
226 Only relevant if include_other_output is True.
227 """
228 )
229
230 def from_here(self, msg):
231 """Return whether a message is from this session"""
232 return msg['parent_header'].get("session", self.session_id) == self.session_id
233
234 def include_output(self, msg):
235 """Return whether we should include a given output message"""
236 from_here = self.from_here(msg)
237 if msg['msg_type'] == 'execute_input':
238 # only echo inputs not from here
239 return self.include_other_output and not from_here
240
241 if self.include_other_output:
242 return True
243 else:
244 return from_here
245
217 246 def handle_iopub(self, msg_id=''):
218 247 """Process messages on the IOPub channel
219 248
220 249 This method consumes and processes messages on the IOPub channel,
221 250 such as stdout, stderr, execute_result and status.
222 251
223 252 It only displays output that is caused by this session.
224 253 """
225 254 while self.client.iopub_channel.msg_ready():
226 255 sub_msg = self.client.iopub_channel.get_msg()
227 256 msg_type = sub_msg['header']['msg_type']
228 257 parent = sub_msg["parent_header"]
229 258
230 if parent.get("session", self.session_id) == self.session_id:
259 if self.include_output(sub_msg):
231 260 if msg_type == 'status':
232 261 self._execution_state = sub_msg["content"]["execution_state"]
233 262 elif msg_type == 'stream':
234 263 if sub_msg["content"]["name"] == "stdout":
235 264 if self._pending_clearoutput:
236 265 print("\r", file=io.stdout, end="")
237 266 self._pending_clearoutput = False
238 267 print(sub_msg["content"]["text"], file=io.stdout, end="")
239 268 io.stdout.flush()
240 269 elif sub_msg["content"]["name"] == "stderr":
241 270 if self._pending_clearoutput:
242 271 print("\r", file=io.stderr, end="")
243 272 self._pending_clearoutput = False
244 273 print(sub_msg["content"]["text"], file=io.stderr, end="")
245 274 io.stderr.flush()
246 275
247 276 elif msg_type == 'execute_result':
248 277 if self._pending_clearoutput:
249 278 print("\r", file=io.stdout, end="")
250 279 self._pending_clearoutput = False
251 280 self.execution_count = int(sub_msg["content"]["execution_count"])
281 if not self.from_here(sub_msg):
282 sys.stdout.write(self.other_output_prefix)
252 283 format_dict = sub_msg["content"]["data"]
253 284 self.handle_rich_data(format_dict)
285
254 286 # taken from DisplayHook.__call__:
255 287 hook = self.displayhook
256 288 hook.start_displayhook()
257 289 hook.write_output_prompt()
258 290 hook.write_format_data(format_dict)
259 291 hook.log_output(format_dict)
260 292 hook.finish_displayhook()
261 293
262 294 elif msg_type == 'display_data':
263 295 data = sub_msg["content"]["data"]
264 296 handled = self.handle_rich_data(data)
265 297 if not handled:
298 if not self.from_here(sub_msg):
299 sys.stdout.write(self.other_output_prefix)
266 300 # if it was an image, we handled it by now
267 301 if 'text/plain' in data:
268 302 print(data['text/plain'])
269
303
304 elif msg_type == 'execute_input':
305 content = sub_msg['content']
306 self.execution_count = content['execution_count']
307 if not self.from_here(sub_msg):
308 sys.stdout.write(self.other_output_prefix)
309 sys.stdout.write(self.prompt_manager.render('in'))
310 sys.stdout.write(content['code'])
311
270 312 elif msg_type == 'clear_output':
271 313 if sub_msg["content"]["wait"]:
272 314 self._pending_clearoutput = True
273 315 else:
274 316 print("\r", file=io.stdout, end="")
275 317
276 318 _imagemime = {
277 319 'image/png': 'png',
278 320 'image/jpeg': 'jpeg',
279 321 'image/svg+xml': 'svg',
280 322 }
281 323
282 324 def handle_rich_data(self, data):
283 325 for mime in self.mime_preference:
284 326 if mime in data and mime in self._imagemime:
285 327 self.handle_image(data, mime)
286 328 return True
287 329
288 330 def handle_image(self, data, mime):
289 331 handler = getattr(
290 332 self, 'handle_image_{0}'.format(self.image_handler), None)
291 333 if handler:
292 334 handler(data, mime)
293 335
294 336 def handle_image_PIL(self, data, mime):
295 337 if mime not in ('image/png', 'image/jpeg'):
296 338 return
297 339 import PIL.Image
298 340 raw = base64.decodestring(data[mime].encode('ascii'))
299 341 img = PIL.Image.open(BytesIO(raw))
300 342 img.show()
301 343
302 344 def handle_image_stream(self, data, mime):
303 345 raw = base64.decodestring(data[mime].encode('ascii'))
304 346 imageformat = self._imagemime[mime]
305 347 fmt = dict(format=imageformat)
306 348 args = [s.format(**fmt) for s in self.stream_image_handler]
307 349 with open(os.devnull, 'w') as devnull:
308 350 proc = subprocess.Popen(
309 351 args, stdin=subprocess.PIPE,
310 352 stdout=devnull, stderr=devnull)
311 353 proc.communicate(raw)
312 354
313 355 def handle_image_tempfile(self, data, mime):
314 356 raw = base64.decodestring(data[mime].encode('ascii'))
315 357 imageformat = self._imagemime[mime]
316 358 filename = 'tmp.{0}'.format(imageformat)
317 359 with NamedFileInTemporaryDirectory(filename) as f, \
318 360 open(os.devnull, 'w') as devnull:
319 361 f.write(raw)
320 362 f.flush()
321 363 fmt = dict(file=f.name, format=imageformat)
322 364 args = [s.format(**fmt) for s in self.tempfile_image_handler]
323 365 subprocess.call(args, stdout=devnull, stderr=devnull)
324 366
325 367 def handle_image_callable(self, data, mime):
326 368 self.callable_image_handler(data)
327 369
328 370 def handle_input_request(self, msg_id, timeout=0.1):
329 371 """ Method to capture raw_input
330 372 """
331 373 req = self.client.stdin_channel.get_msg(timeout=timeout)
332 374 # in case any iopub came while we were waiting:
333 375 self.handle_iopub(msg_id)
334 376 if msg_id == req["parent_header"].get("msg_id"):
335 377 # wrap SIGINT handler
336 378 real_handler = signal.getsignal(signal.SIGINT)
337 379 def double_int(sig,frame):
338 380 # call real handler (forwards sigint to kernel),
339 381 # then raise local interrupt, stopping local raw_input
340 382 real_handler(sig,frame)
341 383 raise KeyboardInterrupt
342 384 signal.signal(signal.SIGINT, double_int)
343 385 content = req['content']
344 386 read = getpass if content.get('password', False) else input
345 387 try:
346 388 raw_data = read(content["prompt"])
347 389 except EOFError:
348 390 # turn EOFError into EOF character
349 391 raw_data = '\x04'
350 392 except KeyboardInterrupt:
351 393 sys.stdout.write('\n')
352 394 return
353 395 finally:
354 396 # restore SIGINT handler
355 397 signal.signal(signal.SIGINT, real_handler)
356 398
357 399 # only send stdin reply if there *was not* another request
358 400 # or execution finished while we were reading.
359 401 if not (self.client.stdin_channel.msg_ready() or self.client.shell_channel.msg_ready()):
360 402 self.client.stdin_channel.input(raw_data)
361 403
362 404 def mainloop(self, display_banner=False):
363 405 while True:
364 406 try:
365 407 self.interact(display_banner=display_banner)
366 408 #self.interact_with_readline()
367 409 # XXX for testing of a readline-decoupled repl loop, call
368 410 # interact_with_readline above
369 411 break
370 412 except KeyboardInterrupt:
371 413 # this should not be necessary, but KeyboardInterrupt
372 414 # handling seems rather unpredictable...
373 415 self.write("\nKeyboardInterrupt in interact()\n")
374 416
375 417 def _banner1_default(self):
376 418 return "IPython Console {version}\n".format(version=release.version)
377 419
378 420 def compute_banner(self):
379 421 super(ZMQTerminalInteractiveShell, self).compute_banner()
380 422 if self.client and not self.kernel_banner:
381 423 msg_id = self.client.kernel_info()
382 424 while True:
383 425 try:
384 426 reply = self.client.get_shell_msg(timeout=1)
385 427 except Empty:
386 428 break
387 429 else:
388 430 if reply['parent_header'].get('msg_id') == msg_id:
389 431 self.kernel_banner = reply['content'].get('banner', '')
390 432 break
391 433 self.banner += self.kernel_banner
392 434
393 435 def wait_for_kernel(self, timeout=None):
394 436 """method to wait for a kernel to be ready"""
395 437 tic = time.time()
396 438 self.client.hb_channel.unpause()
397 439 while True:
398 440 msg_id = self.client.kernel_info()
399 441 reply = None
400 442 while True:
401 443 try:
402 444 reply = self.client.get_shell_msg(timeout=1)
403 445 except Empty:
404 446 break
405 447 else:
406 448 if reply['parent_header'].get('msg_id') == msg_id:
407 449 return True
408 450 if timeout is not None \
409 451 and (time.time() - tic) > timeout \
410 452 and not self.client.hb_channel.is_beating():
411 453 # heart failed
412 454 return False
413 455 return True
414 456
415 457 def interact(self, display_banner=None):
416 458 """Closely emulate the interactive Python console."""
417 459
418 460 # batch run -> do not interact
419 461 if self.exit_now:
420 462 return
421 463
422 464 if display_banner is None:
423 465 display_banner = self.display_banner
424 466
425 467 if isinstance(display_banner, string_types):
426 468 self.show_banner(display_banner)
427 469 elif display_banner:
428 470 self.show_banner()
429 471
430 472 more = False
431 473
432 474 # run a non-empty no-op, so that we don't get a prompt until
433 475 # we know the kernel is ready. This keeps the connection
434 476 # message above the first prompt.
435 477 if not self.wait_for_kernel(self.kernel_timeout):
436 478 error("Kernel did not respond\n")
437 479 return
438 480
439 481 if self.has_readline:
440 482 self.readline_startup_hook(self.pre_readline)
441 483 hlen_b4_cell = self.readline.get_current_history_length()
442 484 else:
443 485 hlen_b4_cell = 0
444 486 # exit_now is set by a call to %Exit or %Quit, through the
445 487 # ask_exit callback.
446 488
447 489 while not self.exit_now:
448 490 if not self.client.is_alive():
449 491 # kernel died, prompt for action or exit
450 492
451 493 action = "restart" if self.manager else "wait for restart"
452 494 ans = self.ask_yes_no("kernel died, %s ([y]/n)?" % action, default='y')
453 495 if ans:
454 496 if self.manager:
455 497 self.manager.restart_kernel(True)
456 498 self.wait_for_kernel(self.kernel_timeout)
457 499 else:
458 500 self.exit_now = True
459 501 continue
460 502 try:
461 503 # protect prompt block from KeyboardInterrupt
462 504 # when sitting on ctrl-C
463 505 self.hooks.pre_prompt_hook()
464 506 if more:
465 507 try:
466 508 prompt = self.prompt_manager.render('in2')
467 509 except Exception:
468 510 self.showtraceback()
469 511 if self.autoindent:
470 512 self.rl_do_indent = True
471 513
472 514 else:
473 515 try:
474 516 prompt = self.separate_in + self.prompt_manager.render('in')
475 517 except Exception:
476 518 self.showtraceback()
477 519
478 520 line = self.raw_input(prompt)
479 521 if self.exit_now:
480 522 # quick exit on sys.std[in|out] close
481 523 break
482 524 if self.autoindent:
483 525 self.rl_do_indent = False
484 526
485 527 except KeyboardInterrupt:
486 528 #double-guard against keyboardinterrupts during kbdint handling
487 529 try:
488 530 self.write('\nKeyboardInterrupt\n')
489 531 source_raw = self.input_splitter.raw_reset()
490 532 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
491 533 more = False
492 534 except KeyboardInterrupt:
493 535 pass
494 536 except EOFError:
495 537 if self.autoindent:
496 538 self.rl_do_indent = False
497 539 if self.has_readline:
498 540 self.readline_startup_hook(None)
499 541 self.write('\n')
500 542 self.exit()
501 543 except bdb.BdbQuit:
502 544 warn('The Python debugger has exited with a BdbQuit exception.\n'
503 545 'Because of how pdb handles the stack, it is impossible\n'
504 546 'for IPython to properly format this particular exception.\n'
505 547 'IPython will resume normal operation.')
506 548 except:
507 549 # exceptions here are VERY RARE, but they can be triggered
508 550 # asynchronously by signal handlers, for example.
509 551 self.showtraceback()
510 552 else:
511 553 try:
512 554 self.input_splitter.push(line)
513 555 more = self.input_splitter.push_accepts_more()
514 556 except SyntaxError:
515 557 # Run the code directly - run_cell takes care of displaying
516 558 # the exception.
517 559 more = False
518 560 if (self.SyntaxTB.last_syntax_error and
519 561 self.autoedit_syntax):
520 562 self.edit_syntax_error()
521 563 if not more:
522 564 source_raw = self.input_splitter.raw_reset()
523 565 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
524 566 self.run_cell(source_raw)
525 567
526 568
527 569 # Turn off the exit flag, so the mainloop can be restarted if desired
528 570 self.exit_now = False
General Comments 0
You need to be logged in to leave comments. Login now