##// END OF EJS Templates
Merge pull request #526 from epatters/append-before-prompt...
Evan Patterson -
r4065:889e008b merge
parent child Browse files
Show More
@@ -1,1797 +1,1816 b''
1 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os
8 import os
9 from os.path import commonprefix
9 from os.path import commonprefix
10 import re
10 import re
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13 from unicodedata import category
14
14
15 # System library imports
15 # System library imports
16 from IPython.external.qt import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import Configurable
19 from IPython.config.configurable import Configurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
20 from IPython.frontend.qt.rich_text import HtmlExporter
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
22 from IPython.utils.traitlets import Bool, Enum, Int, Unicode
23 from ansi_code_processor import QtAnsiCodeProcessor
23 from ansi_code_processor import QtAnsiCodeProcessor
24 from completion_widget import CompletionWidget
24 from completion_widget import CompletionWidget
25 from kill_ring import QtKillRing
25 from kill_ring import QtKillRing
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Functions
28 # Functions
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 def is_letter_or_number(char):
31 def is_letter_or_number(char):
32 """ Returns whether the specified unicode character is a letter or a number.
32 """ Returns whether the specified unicode character is a letter or a number.
33 """
33 """
34 cat = category(char)
34 cat = category(char)
35 return cat.startswith('L') or cat.startswith('N')
35 return cat.startswith('L') or cat.startswith('N')
36
36
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # Classes
38 # Classes
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40
40
41 class ConsoleWidget(Configurable, QtGui.QWidget):
41 class ConsoleWidget(Configurable, QtGui.QWidget):
42 """ An abstract base class for console-type widgets. This class has
42 """ An abstract base class for console-type widgets. This class has
43 functionality for:
43 functionality for:
44
44
45 * Maintaining a prompt and editing region
45 * Maintaining a prompt and editing region
46 * Providing the traditional Unix-style console keyboard shortcuts
46 * Providing the traditional Unix-style console keyboard shortcuts
47 * Performing tab completion
47 * Performing tab completion
48 * Paging text
48 * Paging text
49 * Handling ANSI escape codes
49 * Handling ANSI escape codes
50
50
51 ConsoleWidget also provides a number of utility methods that will be
51 ConsoleWidget also provides a number of utility methods that will be
52 convenient to implementors of a console-style widget.
52 convenient to implementors of a console-style widget.
53 """
53 """
54 __metaclass__ = MetaQObjectHasTraits
54 __metaclass__ = MetaQObjectHasTraits
55
55
56 #------ Configuration ------------------------------------------------------
56 #------ Configuration ------------------------------------------------------
57
57
58 ansi_codes = Bool(True, config=True,
58 ansi_codes = Bool(True, config=True,
59 help="Whether to process ANSI escape codes."
59 help="Whether to process ANSI escape codes."
60 )
60 )
61 buffer_size = Int(500, config=True,
61 buffer_size = Int(500, config=True,
62 help="""
62 help="""
63 The maximum number of lines of text before truncation. Specifying a
63 The maximum number of lines of text before truncation. Specifying a
64 non-positive number disables text truncation (not recommended).
64 non-positive number disables text truncation (not recommended).
65 """
65 """
66 )
66 )
67 gui_completion = Bool(False, config=True,
67 gui_completion = Bool(False, config=True,
68 help="Use a list widget instead of plain text output for tab completion."
68 help="""
69 Use a list widget instead of plain text output for tab completion.
70 """
69 )
71 )
70 # NOTE: this value can only be specified during initialization.
72 # NOTE: this value can only be specified during initialization.
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
73 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
72 help="""
74 help="""
73 The type of underlying text widget to use. Valid values are 'plain', which
75 The type of underlying text widget to use. Valid values are 'plain',
74 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
76 which specifies a QPlainTextEdit, and 'rich', which specifies a
77 QTextEdit.
75 """
78 """
76 )
79 )
77 # NOTE: this value can only be specified during initialization.
80 # NOTE: this value can only be specified during initialization.
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
81 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
79 default_value='inside', config=True,
82 default_value='inside', config=True,
80 help="""
83 help="""
81 The type of paging to use. Valid values are:
84 The type of paging to use. Valid values are:
82
85
83 'inside' : The widget pages like a traditional terminal.
86 'inside' : The widget pages like a traditional terminal.
84 'hsplit' : When paging is requested, the widget is split
87 'hsplit' : When paging is requested, the widget is split
85 horizontally. The top pane contains the console, and the
88 horizontally. The top pane contains the console, and the
86 bottom pane contains the paged text.
89 bottom pane contains the paged text.
87 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
90 'vsplit' : Similar to 'hsplit', except that a vertical splitter
91 used.
88 'custom' : No action is taken by the widget beyond emitting a
92 'custom' : No action is taken by the widget beyond emitting a
89 'custom_page_requested(str)' signal.
93 'custom_page_requested(str)' signal.
90 'none' : The text is written directly to the console.
94 'none' : The text is written directly to the console.
91 """)
95 """)
92
96
93 font_family = Unicode(config=True,
97 font_family = Unicode(config=True,
94 help="""The font family to use for the console.
98 help="""The font family to use for the console.
95 On OSX this defaults to Monaco, on Windows the default is
99 On OSX this defaults to Monaco, on Windows the default is
96 Consolas with fallback of Courier, and on other platforms
100 Consolas with fallback of Courier, and on other platforms
97 the default is Monospace.
101 the default is Monospace.
98 """)
102 """)
99 def _font_family_default(self):
103 def _font_family_default(self):
100 if sys.platform == 'win32':
104 if sys.platform == 'win32':
101 # Consolas ships with Vista/Win7, fallback to Courier if needed
105 # Consolas ships with Vista/Win7, fallback to Courier if needed
102 return 'Consolas'
106 return 'Consolas'
103 elif sys.platform == 'darwin':
107 elif sys.platform == 'darwin':
104 # OSX always has Monaco, no need for a fallback
108 # OSX always has Monaco, no need for a fallback
105 return 'Monaco'
109 return 'Monaco'
106 else:
110 else:
107 # Monospace should always exist, no need for a fallback
111 # Monospace should always exist, no need for a fallback
108 return 'Monospace'
112 return 'Monospace'
109
113
110 font_size = Int(config=True,
114 font_size = Int(config=True,
111 help="""The font size. If unconfigured, Qt will be entrusted
115 help="""The font size. If unconfigured, Qt will be entrusted
112 with the size of the font.
116 with the size of the font.
113 """)
117 """)
114
118
115 # Whether to override ShortcutEvents for the keybindings defined by this
119 # Whether to override ShortcutEvents for the keybindings defined by this
116 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
120 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
117 # priority (when it has focus) over, e.g., window-level menu shortcuts.
121 # priority (when it has focus) over, e.g., window-level menu shortcuts.
118 override_shortcuts = Bool(False)
122 override_shortcuts = Bool(False)
119
123
120 #------ Signals ------------------------------------------------------------
124 #------ Signals ------------------------------------------------------------
121
125
122 # Signals that indicate ConsoleWidget state.
126 # Signals that indicate ConsoleWidget state.
123 copy_available = QtCore.Signal(bool)
127 copy_available = QtCore.Signal(bool)
124 redo_available = QtCore.Signal(bool)
128 redo_available = QtCore.Signal(bool)
125 undo_available = QtCore.Signal(bool)
129 undo_available = QtCore.Signal(bool)
126
130
127 # Signal emitted when paging is needed and the paging style has been
131 # Signal emitted when paging is needed and the paging style has been
128 # specified as 'custom'.
132 # specified as 'custom'.
129 custom_page_requested = QtCore.Signal(object)
133 custom_page_requested = QtCore.Signal(object)
130
134
131 # Signal emitted when the font is changed.
135 # Signal emitted when the font is changed.
132 font_changed = QtCore.Signal(QtGui.QFont)
136 font_changed = QtCore.Signal(QtGui.QFont)
133
137
134 #------ Protected class variables ------------------------------------------
138 #------ Protected class variables ------------------------------------------
135
139
136 # When the control key is down, these keys are mapped.
140 # When the control key is down, these keys are mapped.
137 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
141 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
138 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
142 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
139 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
143 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
140 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
144 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
141 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
145 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
142 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
146 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
143 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
147 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
144 if not sys.platform == 'darwin':
148 if not sys.platform == 'darwin':
145 # On OS X, Ctrl-E already does the right thing, whereas End moves the
149 # On OS X, Ctrl-E already does the right thing, whereas End moves the
146 # cursor to the bottom of the buffer.
150 # cursor to the bottom of the buffer.
147 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
151 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
148
152
149 # The shortcuts defined by this widget. We need to keep track of these to
153 # The shortcuts defined by this widget. We need to keep track of these to
150 # support 'override_shortcuts' above.
154 # support 'override_shortcuts' above.
151 _shortcuts = set(_ctrl_down_remap.keys() +
155 _shortcuts = set(_ctrl_down_remap.keys() +
152 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
156 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
153 QtCore.Qt.Key_V ])
157 QtCore.Qt.Key_V ])
154
158
155 #---------------------------------------------------------------------------
159 #---------------------------------------------------------------------------
156 # 'QObject' interface
160 # 'QObject' interface
157 #---------------------------------------------------------------------------
161 #---------------------------------------------------------------------------
158
162
159 def __init__(self, parent=None, **kw):
163 def __init__(self, parent=None, **kw):
160 """ Create a ConsoleWidget.
164 """ Create a ConsoleWidget.
161
165
162 Parameters:
166 Parameters:
163 -----------
167 -----------
164 parent : QWidget, optional [default None]
168 parent : QWidget, optional [default None]
165 The parent for this widget.
169 The parent for this widget.
166 """
170 """
167 QtGui.QWidget.__init__(self, parent)
171 QtGui.QWidget.__init__(self, parent)
168 Configurable.__init__(self, **kw)
172 Configurable.__init__(self, **kw)
169
173
170 # Create the layout and underlying text widget.
174 # Create the layout and underlying text widget.
171 layout = QtGui.QStackedLayout(self)
175 layout = QtGui.QStackedLayout(self)
172 layout.setContentsMargins(0, 0, 0, 0)
176 layout.setContentsMargins(0, 0, 0, 0)
173 self._control = self._create_control()
177 self._control = self._create_control()
174 self._page_control = None
178 self._page_control = None
175 self._splitter = None
179 self._splitter = None
176 if self.paging in ('hsplit', 'vsplit'):
180 if self.paging in ('hsplit', 'vsplit'):
177 self._splitter = QtGui.QSplitter()
181 self._splitter = QtGui.QSplitter()
178 if self.paging == 'hsplit':
182 if self.paging == 'hsplit':
179 self._splitter.setOrientation(QtCore.Qt.Horizontal)
183 self._splitter.setOrientation(QtCore.Qt.Horizontal)
180 else:
184 else:
181 self._splitter.setOrientation(QtCore.Qt.Vertical)
185 self._splitter.setOrientation(QtCore.Qt.Vertical)
182 self._splitter.addWidget(self._control)
186 self._splitter.addWidget(self._control)
183 layout.addWidget(self._splitter)
187 layout.addWidget(self._splitter)
184 else:
188 else:
185 layout.addWidget(self._control)
189 layout.addWidget(self._control)
186
190
187 # Create the paging widget, if necessary.
191 # Create the paging widget, if necessary.
188 if self.paging in ('inside', 'hsplit', 'vsplit'):
192 if self.paging in ('inside', 'hsplit', 'vsplit'):
189 self._page_control = self._create_page_control()
193 self._page_control = self._create_page_control()
190 if self._splitter:
194 if self._splitter:
191 self._page_control.hide()
195 self._page_control.hide()
192 self._splitter.addWidget(self._page_control)
196 self._splitter.addWidget(self._page_control)
193 else:
197 else:
194 layout.addWidget(self._page_control)
198 layout.addWidget(self._page_control)
195
199
196 # Initialize protected variables. Some variables contain useful state
200 # Initialize protected variables. Some variables contain useful state
197 # information for subclasses; they should be considered read-only.
201 # information for subclasses; they should be considered read-only.
202 self._append_before_prompt_pos = 0
198 self._ansi_processor = QtAnsiCodeProcessor()
203 self._ansi_processor = QtAnsiCodeProcessor()
199 self._completion_widget = CompletionWidget(self._control)
204 self._completion_widget = CompletionWidget(self._control)
200 self._continuation_prompt = '> '
205 self._continuation_prompt = '> '
201 self._continuation_prompt_html = None
206 self._continuation_prompt_html = None
202 self._executing = False
207 self._executing = False
203 self._filter_drag = False
208 self._filter_drag = False
204 self._filter_resize = False
209 self._filter_resize = False
205 self._html_exporter = HtmlExporter(self._control)
210 self._html_exporter = HtmlExporter(self._control)
206 self._input_buffer_executing = ''
211 self._input_buffer_executing = ''
207 self._input_buffer_pending = ''
212 self._input_buffer_pending = ''
208 self._kill_ring = QtKillRing(self._control)
213 self._kill_ring = QtKillRing(self._control)
209 self._prompt = ''
214 self._prompt = ''
210 self._prompt_html = None
215 self._prompt_html = None
211 self._prompt_pos = 0
216 self._prompt_pos = 0
212 self._prompt_sep = ''
217 self._prompt_sep = ''
213 self._reading = False
218 self._reading = False
214 self._reading_callback = None
219 self._reading_callback = None
215 self._tab_width = 8
220 self._tab_width = 8
216 self._text_completing_pos = 0
221 self._text_completing_pos = 0
217
222
218 # Set a monospaced font.
223 # Set a monospaced font.
219 self.reset_font()
224 self.reset_font()
220
225
221 # Configure actions.
226 # Configure actions.
222 action = QtGui.QAction('Print', None)
227 action = QtGui.QAction('Print', None)
223 action.setEnabled(True)
228 action.setEnabled(True)
224 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
229 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
225 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
230 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
226 # Only override the default if there is a collision.
231 # Only override the default if there is a collision.
227 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
232 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
228 printkey = "Ctrl+Shift+P"
233 printkey = "Ctrl+Shift+P"
229 action.setShortcut(printkey)
234 action.setShortcut(printkey)
230 action.triggered.connect(self.print_)
235 action.triggered.connect(self.print_)
231 self.addAction(action)
236 self.addAction(action)
232 self._print_action = action
237 self._print_action = action
233
238
234 action = QtGui.QAction('Save as HTML/XML', None)
239 action = QtGui.QAction('Save as HTML/XML', None)
235 action.setShortcut(QtGui.QKeySequence.Save)
240 action.setShortcut(QtGui.QKeySequence.Save)
236 action.triggered.connect(self.export_html)
241 action.triggered.connect(self.export_html)
237 self.addAction(action)
242 self.addAction(action)
238 self._export_action = action
243 self._export_action = action
239
244
240 action = QtGui.QAction('Select All', None)
245 action = QtGui.QAction('Select All', None)
241 action.setEnabled(True)
246 action.setEnabled(True)
242 action.setShortcut(QtGui.QKeySequence.SelectAll)
247 action.setShortcut(QtGui.QKeySequence.SelectAll)
243 action.triggered.connect(self.select_all)
248 action.triggered.connect(self.select_all)
244 self.addAction(action)
249 self.addAction(action)
245 self._select_all_action = action
250 self._select_all_action = action
246
251
247 def eventFilter(self, obj, event):
252 def eventFilter(self, obj, event):
248 """ Reimplemented to ensure a console-like behavior in the underlying
253 """ Reimplemented to ensure a console-like behavior in the underlying
249 text widgets.
254 text widgets.
250 """
255 """
251 etype = event.type()
256 etype = event.type()
252 if etype == QtCore.QEvent.KeyPress:
257 if etype == QtCore.QEvent.KeyPress:
253
258
254 # Re-map keys for all filtered widgets.
259 # Re-map keys for all filtered widgets.
255 key = event.key()
260 key = event.key()
256 if self._control_key_down(event.modifiers()) and \
261 if self._control_key_down(event.modifiers()) and \
257 key in self._ctrl_down_remap:
262 key in self._ctrl_down_remap:
258 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
263 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
259 self._ctrl_down_remap[key],
264 self._ctrl_down_remap[key],
260 QtCore.Qt.NoModifier)
265 QtCore.Qt.NoModifier)
261 QtGui.qApp.sendEvent(obj, new_event)
266 QtGui.qApp.sendEvent(obj, new_event)
262 return True
267 return True
263
268
264 elif obj == self._control:
269 elif obj == self._control:
265 return self._event_filter_console_keypress(event)
270 return self._event_filter_console_keypress(event)
266
271
267 elif obj == self._page_control:
272 elif obj == self._page_control:
268 return self._event_filter_page_keypress(event)
273 return self._event_filter_page_keypress(event)
269
274
270 # Make middle-click paste safe.
275 # Make middle-click paste safe.
271 elif etype == QtCore.QEvent.MouseButtonRelease and \
276 elif etype == QtCore.QEvent.MouseButtonRelease and \
272 event.button() == QtCore.Qt.MidButton and \
277 event.button() == QtCore.Qt.MidButton and \
273 obj == self._control.viewport():
278 obj == self._control.viewport():
274 cursor = self._control.cursorForPosition(event.pos())
279 cursor = self._control.cursorForPosition(event.pos())
275 self._control.setTextCursor(cursor)
280 self._control.setTextCursor(cursor)
276 self.paste(QtGui.QClipboard.Selection)
281 self.paste(QtGui.QClipboard.Selection)
277 return True
282 return True
278
283
279 # Manually adjust the scrollbars *after* a resize event is dispatched.
284 # Manually adjust the scrollbars *after* a resize event is dispatched.
280 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
285 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
281 self._filter_resize = True
286 self._filter_resize = True
282 QtGui.qApp.sendEvent(obj, event)
287 QtGui.qApp.sendEvent(obj, event)
283 self._adjust_scrollbars()
288 self._adjust_scrollbars()
284 self._filter_resize = False
289 self._filter_resize = False
285 return True
290 return True
286
291
287 # Override shortcuts for all filtered widgets.
292 # Override shortcuts for all filtered widgets.
288 elif etype == QtCore.QEvent.ShortcutOverride and \
293 elif etype == QtCore.QEvent.ShortcutOverride and \
289 self.override_shortcuts and \
294 self.override_shortcuts and \
290 self._control_key_down(event.modifiers()) and \
295 self._control_key_down(event.modifiers()) and \
291 event.key() in self._shortcuts:
296 event.key() in self._shortcuts:
292 event.accept()
297 event.accept()
293
298
294 # Ensure that drags are safe. The problem is that the drag starting
299 # Ensure that drags are safe. The problem is that the drag starting
295 # logic, which determines whether the drag is a Copy or Move, is locked
300 # logic, which determines whether the drag is a Copy or Move, is locked
296 # down in QTextControl. If the widget is editable, which it must be if
301 # down in QTextControl. If the widget is editable, which it must be if
297 # we're not executing, the drag will be a Move. The following hack
302 # we're not executing, the drag will be a Move. The following hack
298 # prevents QTextControl from deleting the text by clearing the selection
303 # prevents QTextControl from deleting the text by clearing the selection
299 # when a drag leave event originating from this widget is dispatched.
304 # when a drag leave event originating from this widget is dispatched.
300 # The fact that we have to clear the user's selection is unfortunate,
305 # The fact that we have to clear the user's selection is unfortunate,
301 # but the alternative--trying to prevent Qt from using its hardwired
306 # but the alternative--trying to prevent Qt from using its hardwired
302 # drag logic and writing our own--is worse.
307 # drag logic and writing our own--is worse.
303 elif etype == QtCore.QEvent.DragEnter and \
308 elif etype == QtCore.QEvent.DragEnter and \
304 obj == self._control.viewport() and \
309 obj == self._control.viewport() and \
305 event.source() == self._control.viewport():
310 event.source() == self._control.viewport():
306 self._filter_drag = True
311 self._filter_drag = True
307 elif etype == QtCore.QEvent.DragLeave and \
312 elif etype == QtCore.QEvent.DragLeave and \
308 obj == self._control.viewport() and \
313 obj == self._control.viewport() and \
309 self._filter_drag:
314 self._filter_drag:
310 cursor = self._control.textCursor()
315 cursor = self._control.textCursor()
311 cursor.clearSelection()
316 cursor.clearSelection()
312 self._control.setTextCursor(cursor)
317 self._control.setTextCursor(cursor)
313 self._filter_drag = False
318 self._filter_drag = False
314
319
315 # Ensure that drops are safe.
320 # Ensure that drops are safe.
316 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
321 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
317 cursor = self._control.cursorForPosition(event.pos())
322 cursor = self._control.cursorForPosition(event.pos())
318 if self._in_buffer(cursor.position()):
323 if self._in_buffer(cursor.position()):
319 text = event.mimeData().text()
324 text = event.mimeData().text()
320 self._insert_plain_text_into_buffer(cursor, text)
325 self._insert_plain_text_into_buffer(cursor, text)
321
326
322 # Qt is expecting to get something here--drag and drop occurs in its
327 # Qt is expecting to get something here--drag and drop occurs in its
323 # own event loop. Send a DragLeave event to end it.
328 # own event loop. Send a DragLeave event to end it.
324 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
329 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
325 return True
330 return True
326
331
327 return super(ConsoleWidget, self).eventFilter(obj, event)
332 return super(ConsoleWidget, self).eventFilter(obj, event)
328
333
329 #---------------------------------------------------------------------------
334 #---------------------------------------------------------------------------
330 # 'QWidget' interface
335 # 'QWidget' interface
331 #---------------------------------------------------------------------------
336 #---------------------------------------------------------------------------
332
337
333 def sizeHint(self):
338 def sizeHint(self):
334 """ Reimplemented to suggest a size that is 80 characters wide and
339 """ Reimplemented to suggest a size that is 80 characters wide and
335 25 lines high.
340 25 lines high.
336 """
341 """
337 font_metrics = QtGui.QFontMetrics(self.font)
342 font_metrics = QtGui.QFontMetrics(self.font)
338 margin = (self._control.frameWidth() +
343 margin = (self._control.frameWidth() +
339 self._control.document().documentMargin()) * 2
344 self._control.document().documentMargin()) * 2
340 style = self.style()
345 style = self.style()
341 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
346 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
342
347
343 # Note 1: Despite my best efforts to take the various margins into
348 # Note 1: Despite my best efforts to take the various margins into
344 # account, the width is still coming out a bit too small, so we include
349 # account, the width is still coming out a bit too small, so we include
345 # a fudge factor of one character here.
350 # a fudge factor of one character here.
346 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
351 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
347 # to a Qt bug on certain Mac OS systems where it returns 0.
352 # to a Qt bug on certain Mac OS systems where it returns 0.
348 width = font_metrics.width(' ') * 81 + margin
353 width = font_metrics.width(' ') * 81 + margin
349 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
354 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
350 if self.paging == 'hsplit':
355 if self.paging == 'hsplit':
351 width = width * 2 + splitwidth
356 width = width * 2 + splitwidth
352
357
353 height = font_metrics.height() * 25 + margin
358 height = font_metrics.height() * 25 + margin
354 if self.paging == 'vsplit':
359 if self.paging == 'vsplit':
355 height = height * 2 + splitwidth
360 height = height * 2 + splitwidth
356
361
357 return QtCore.QSize(width, height)
362 return QtCore.QSize(width, height)
358
363
359 #---------------------------------------------------------------------------
364 #---------------------------------------------------------------------------
360 # 'ConsoleWidget' public interface
365 # 'ConsoleWidget' public interface
361 #---------------------------------------------------------------------------
366 #---------------------------------------------------------------------------
362
367
363 def can_copy(self):
368 def can_copy(self):
364 """ Returns whether text can be copied to the clipboard.
369 """ Returns whether text can be copied to the clipboard.
365 """
370 """
366 return self._control.textCursor().hasSelection()
371 return self._control.textCursor().hasSelection()
367
372
368 def can_cut(self):
373 def can_cut(self):
369 """ Returns whether text can be cut to the clipboard.
374 """ Returns whether text can be cut to the clipboard.
370 """
375 """
371 cursor = self._control.textCursor()
376 cursor = self._control.textCursor()
372 return (cursor.hasSelection() and
377 return (cursor.hasSelection() and
373 self._in_buffer(cursor.anchor()) and
378 self._in_buffer(cursor.anchor()) and
374 self._in_buffer(cursor.position()))
379 self._in_buffer(cursor.position()))
375
380
376 def can_paste(self):
381 def can_paste(self):
377 """ Returns whether text can be pasted from the clipboard.
382 """ Returns whether text can be pasted from the clipboard.
378 """
383 """
379 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
384 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
380 return bool(QtGui.QApplication.clipboard().text())
385 return bool(QtGui.QApplication.clipboard().text())
381 return False
386 return False
382
387
383 def clear(self, keep_input=True):
388 def clear(self, keep_input=True):
384 """ Clear the console.
389 """ Clear the console.
385
390
386 Parameters:
391 Parameters:
387 -----------
392 -----------
388 keep_input : bool, optional (default True)
393 keep_input : bool, optional (default True)
389 If set, restores the old input buffer if a new prompt is written.
394 If set, restores the old input buffer if a new prompt is written.
390 """
395 """
391 if self._executing:
396 if self._executing:
392 self._control.clear()
397 self._control.clear()
393 else:
398 else:
394 if keep_input:
399 if keep_input:
395 input_buffer = self.input_buffer
400 input_buffer = self.input_buffer
396 self._control.clear()
401 self._control.clear()
397 self._show_prompt()
402 self._show_prompt()
398 if keep_input:
403 if keep_input:
399 self.input_buffer = input_buffer
404 self.input_buffer = input_buffer
400
405
401 def copy(self):
406 def copy(self):
402 """ Copy the currently selected text to the clipboard.
407 """ Copy the currently selected text to the clipboard.
403 """
408 """
404 self._control.copy()
409 self._control.copy()
405
410
406 def cut(self):
411 def cut(self):
407 """ Copy the currently selected text to the clipboard and delete it
412 """ Copy the currently selected text to the clipboard and delete it
408 if it's inside the input buffer.
413 if it's inside the input buffer.
409 """
414 """
410 self.copy()
415 self.copy()
411 if self.can_cut():
416 if self.can_cut():
412 self._control.textCursor().removeSelectedText()
417 self._control.textCursor().removeSelectedText()
413
418
414 def execute(self, source=None, hidden=False, interactive=False):
419 def execute(self, source=None, hidden=False, interactive=False):
415 """ Executes source or the input buffer, possibly prompting for more
420 """ Executes source or the input buffer, possibly prompting for more
416 input.
421 input.
417
422
418 Parameters:
423 Parameters:
419 -----------
424 -----------
420 source : str, optional
425 source : str, optional
421
426
422 The source to execute. If not specified, the input buffer will be
427 The source to execute. If not specified, the input buffer will be
423 used. If specified and 'hidden' is False, the input buffer will be
428 used. If specified and 'hidden' is False, the input buffer will be
424 replaced with the source before execution.
429 replaced with the source before execution.
425
430
426 hidden : bool, optional (default False)
431 hidden : bool, optional (default False)
427
432
428 If set, no output will be shown and the prompt will not be modified.
433 If set, no output will be shown and the prompt will not be modified.
429 In other words, it will be completely invisible to the user that
434 In other words, it will be completely invisible to the user that
430 an execution has occurred.
435 an execution has occurred.
431
436
432 interactive : bool, optional (default False)
437 interactive : bool, optional (default False)
433
438
434 Whether the console is to treat the source as having been manually
439 Whether the console is to treat the source as having been manually
435 entered by the user. The effect of this parameter depends on the
440 entered by the user. The effect of this parameter depends on the
436 subclass implementation.
441 subclass implementation.
437
442
438 Raises:
443 Raises:
439 -------
444 -------
440 RuntimeError
445 RuntimeError
441 If incomplete input is given and 'hidden' is True. In this case,
446 If incomplete input is given and 'hidden' is True. In this case,
442 it is not possible to prompt for more input.
447 it is not possible to prompt for more input.
443
448
444 Returns:
449 Returns:
445 --------
450 --------
446 A boolean indicating whether the source was executed.
451 A boolean indicating whether the source was executed.
447 """
452 """
448 # WARNING: The order in which things happen here is very particular, in
453 # WARNING: The order in which things happen here is very particular, in
449 # large part because our syntax highlighting is fragile. If you change
454 # large part because our syntax highlighting is fragile. If you change
450 # something, test carefully!
455 # something, test carefully!
451
456
452 # Decide what to execute.
457 # Decide what to execute.
453 if source is None:
458 if source is None:
454 source = self.input_buffer
459 source = self.input_buffer
455 if not hidden:
460 if not hidden:
456 # A newline is appended later, but it should be considered part
461 # A newline is appended later, but it should be considered part
457 # of the input buffer.
462 # of the input buffer.
458 source += '\n'
463 source += '\n'
459 elif not hidden:
464 elif not hidden:
460 self.input_buffer = source
465 self.input_buffer = source
461
466
462 # Execute the source or show a continuation prompt if it is incomplete.
467 # Execute the source or show a continuation prompt if it is incomplete.
463 complete = self._is_complete(source, interactive)
468 complete = self._is_complete(source, interactive)
464 if hidden:
469 if hidden:
465 if complete:
470 if complete:
466 self._execute(source, hidden)
471 self._execute(source, hidden)
467 else:
472 else:
468 error = 'Incomplete noninteractive input: "%s"'
473 error = 'Incomplete noninteractive input: "%s"'
469 raise RuntimeError(error % source)
474 raise RuntimeError(error % source)
470 else:
475 else:
471 if complete:
476 if complete:
472 self._append_plain_text('\n')
477 self._append_plain_text('\n')
473 self._input_buffer_executing = self.input_buffer
478 self._input_buffer_executing = self.input_buffer
474 self._executing = True
479 self._executing = True
475 self._prompt_finished()
480 self._prompt_finished()
476
481
477 # The maximum block count is only in effect during execution.
482 # The maximum block count is only in effect during execution.
478 # This ensures that _prompt_pos does not become invalid due to
483 # This ensures that _prompt_pos does not become invalid due to
479 # text truncation.
484 # text truncation.
480 self._control.document().setMaximumBlockCount(self.buffer_size)
485 self._control.document().setMaximumBlockCount(self.buffer_size)
481
486
482 # Setting a positive maximum block count will automatically
487 # Setting a positive maximum block count will automatically
483 # disable the undo/redo history, but just to be safe:
488 # disable the undo/redo history, but just to be safe:
484 self._control.setUndoRedoEnabled(False)
489 self._control.setUndoRedoEnabled(False)
485
490
486 # Perform actual execution.
491 # Perform actual execution.
487 self._execute(source, hidden)
492 self._execute(source, hidden)
488
493
489 else:
494 else:
490 # Do this inside an edit block so continuation prompts are
495 # Do this inside an edit block so continuation prompts are
491 # removed seamlessly via undo/redo.
496 # removed seamlessly via undo/redo.
492 cursor = self._get_end_cursor()
497 cursor = self._get_end_cursor()
493 cursor.beginEditBlock()
498 cursor.beginEditBlock()
494 cursor.insertText('\n')
499 cursor.insertText('\n')
495 self._insert_continuation_prompt(cursor)
500 self._insert_continuation_prompt(cursor)
496 cursor.endEditBlock()
501 cursor.endEditBlock()
497
502
498 # Do not do this inside the edit block. It works as expected
503 # Do not do this inside the edit block. It works as expected
499 # when using a QPlainTextEdit control, but does not have an
504 # when using a QPlainTextEdit control, but does not have an
500 # effect when using a QTextEdit. I believe this is a Qt bug.
505 # effect when using a QTextEdit. I believe this is a Qt bug.
501 self._control.moveCursor(QtGui.QTextCursor.End)
506 self._control.moveCursor(QtGui.QTextCursor.End)
502
507
503 return complete
508 return complete
504
509
505 def export_html(self):
510 def export_html(self):
506 """ Shows a dialog to export HTML/XML in various formats.
511 """ Shows a dialog to export HTML/XML in various formats.
507 """
512 """
508 self._html_exporter.export()
513 self._html_exporter.export()
509
514
510 def _get_input_buffer(self):
515 def _get_input_buffer(self, force=False):
511 """ The text that the user has entered entered at the current prompt.
516 """ The text that the user has entered entered at the current prompt.
512
517
513 If the console is currently executing, the text that is executing will
518 If the console is currently executing, the text that is executing will
514 always be returned.
519 always be returned.
515 """
520 """
516 # If we're executing, the input buffer may not even exist anymore due to
521 # If we're executing, the input buffer may not even exist anymore due to
517 # the limit imposed by 'buffer_size'. Therefore, we store it.
522 # the limit imposed by 'buffer_size'. Therefore, we store it.
518 if self._executing:
523 if self._executing and not force:
519 return self._input_buffer_executing
524 return self._input_buffer_executing
520
525
521 cursor = self._get_end_cursor()
526 cursor = self._get_end_cursor()
522 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
527 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
523 input_buffer = cursor.selection().toPlainText()
528 input_buffer = cursor.selection().toPlainText()
524
529
525 # Strip out continuation prompts.
530 # Strip out continuation prompts.
526 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
531 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
527
532
528 def _set_input_buffer(self, string):
533 def _set_input_buffer(self, string):
529 """ Sets the text in the input buffer.
534 """ Sets the text in the input buffer.
530
535
531 If the console is currently executing, this call has no *immediate*
536 If the console is currently executing, this call has no *immediate*
532 effect. When the execution is finished, the input buffer will be updated
537 effect. When the execution is finished, the input buffer will be updated
533 appropriately.
538 appropriately.
534 """
539 """
535 # If we're executing, store the text for later.
540 # If we're executing, store the text for later.
536 if self._executing:
541 if self._executing:
537 self._input_buffer_pending = string
542 self._input_buffer_pending = string
538 return
543 return
539
544
540 # Remove old text.
545 # Remove old text.
541 cursor = self._get_end_cursor()
546 cursor = self._get_end_cursor()
542 cursor.beginEditBlock()
547 cursor.beginEditBlock()
543 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
548 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
544 cursor.removeSelectedText()
549 cursor.removeSelectedText()
545
550
546 # Insert new text with continuation prompts.
551 # Insert new text with continuation prompts.
547 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
552 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
548 cursor.endEditBlock()
553 cursor.endEditBlock()
549 self._control.moveCursor(QtGui.QTextCursor.End)
554 self._control.moveCursor(QtGui.QTextCursor.End)
550
555
551 input_buffer = property(_get_input_buffer, _set_input_buffer)
556 input_buffer = property(_get_input_buffer, _set_input_buffer)
552
557
553 def _get_font(self):
558 def _get_font(self):
554 """ The base font being used by the ConsoleWidget.
559 """ The base font being used by the ConsoleWidget.
555 """
560 """
556 return self._control.document().defaultFont()
561 return self._control.document().defaultFont()
557
562
558 def _set_font(self, font):
563 def _set_font(self, font):
559 """ Sets the base font for the ConsoleWidget to the specified QFont.
564 """ Sets the base font for the ConsoleWidget to the specified QFont.
560 """
565 """
561 font_metrics = QtGui.QFontMetrics(font)
566 font_metrics = QtGui.QFontMetrics(font)
562 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
567 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
563
568
564 self._completion_widget.setFont(font)
569 self._completion_widget.setFont(font)
565 self._control.document().setDefaultFont(font)
570 self._control.document().setDefaultFont(font)
566 if self._page_control:
571 if self._page_control:
567 self._page_control.document().setDefaultFont(font)
572 self._page_control.document().setDefaultFont(font)
568
573
569 self.font_changed.emit(font)
574 self.font_changed.emit(font)
570
575
571 font = property(_get_font, _set_font)
576 font = property(_get_font, _set_font)
572
577
573 def paste(self, mode=QtGui.QClipboard.Clipboard):
578 def paste(self, mode=QtGui.QClipboard.Clipboard):
574 """ Paste the contents of the clipboard into the input region.
579 """ Paste the contents of the clipboard into the input region.
575
580
576 Parameters:
581 Parameters:
577 -----------
582 -----------
578 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
583 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
579
584
580 Controls which part of the system clipboard is used. This can be
585 Controls which part of the system clipboard is used. This can be
581 used to access the selection clipboard in X11 and the Find buffer
586 used to access the selection clipboard in X11 and the Find buffer
582 in Mac OS. By default, the regular clipboard is used.
587 in Mac OS. By default, the regular clipboard is used.
583 """
588 """
584 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
589 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
585 # Make sure the paste is safe.
590 # Make sure the paste is safe.
586 self._keep_cursor_in_buffer()
591 self._keep_cursor_in_buffer()
587 cursor = self._control.textCursor()
592 cursor = self._control.textCursor()
588
593
589 # Remove any trailing newline, which confuses the GUI and forces the
594 # Remove any trailing newline, which confuses the GUI and forces the
590 # user to backspace.
595 # user to backspace.
591 text = QtGui.QApplication.clipboard().text(mode).rstrip()
596 text = QtGui.QApplication.clipboard().text(mode).rstrip()
592 self._insert_plain_text_into_buffer(cursor, dedent(text))
597 self._insert_plain_text_into_buffer(cursor, dedent(text))
593
598
594 def print_(self, printer = None):
599 def print_(self, printer = None):
595 """ Print the contents of the ConsoleWidget to the specified QPrinter.
600 """ Print the contents of the ConsoleWidget to the specified QPrinter.
596 """
601 """
597 if (not printer):
602 if (not printer):
598 printer = QtGui.QPrinter()
603 printer = QtGui.QPrinter()
599 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
604 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
600 return
605 return
601 self._control.print_(printer)
606 self._control.print_(printer)
602
607
603 def prompt_to_top(self):
608 def prompt_to_top(self):
604 """ Moves the prompt to the top of the viewport.
609 """ Moves the prompt to the top of the viewport.
605 """
610 """
606 if not self._executing:
611 if not self._executing:
607 prompt_cursor = self._get_prompt_cursor()
612 prompt_cursor = self._get_prompt_cursor()
608 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
613 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
609 self._set_cursor(prompt_cursor)
614 self._set_cursor(prompt_cursor)
610 self._set_top_cursor(prompt_cursor)
615 self._set_top_cursor(prompt_cursor)
611
616
612 def redo(self):
617 def redo(self):
613 """ Redo the last operation. If there is no operation to redo, nothing
618 """ Redo the last operation. If there is no operation to redo, nothing
614 happens.
619 happens.
615 """
620 """
616 self._control.redo()
621 self._control.redo()
617
622
618 def reset_font(self):
623 def reset_font(self):
619 """ Sets the font to the default fixed-width font for this platform.
624 """ Sets the font to the default fixed-width font for this platform.
620 """
625 """
621 if sys.platform == 'win32':
626 if sys.platform == 'win32':
622 # Consolas ships with Vista/Win7, fallback to Courier if needed
627 # Consolas ships with Vista/Win7, fallback to Courier if needed
623 fallback = 'Courier'
628 fallback = 'Courier'
624 elif sys.platform == 'darwin':
629 elif sys.platform == 'darwin':
625 # OSX always has Monaco
630 # OSX always has Monaco
626 fallback = 'Monaco'
631 fallback = 'Monaco'
627 else:
632 else:
628 # Monospace should always exist
633 # Monospace should always exist
629 fallback = 'Monospace'
634 fallback = 'Monospace'
630 font = get_font(self.font_family, fallback)
635 font = get_font(self.font_family, fallback)
631 if self.font_size:
636 if self.font_size:
632 font.setPointSize(self.font_size)
637 font.setPointSize(self.font_size)
633 else:
638 else:
634 font.setPointSize(QtGui.qApp.font().pointSize())
639 font.setPointSize(QtGui.qApp.font().pointSize())
635 font.setStyleHint(QtGui.QFont.TypeWriter)
640 font.setStyleHint(QtGui.QFont.TypeWriter)
636 self._set_font(font)
641 self._set_font(font)
637
642
638 def change_font_size(self, delta):
643 def change_font_size(self, delta):
639 """Change the font size by the specified amount (in points).
644 """Change the font size by the specified amount (in points).
640 """
645 """
641 font = self.font
646 font = self.font
642 size = max(font.pointSize() + delta, 1) # minimum 1 point
647 size = max(font.pointSize() + delta, 1) # minimum 1 point
643 font.setPointSize(size)
648 font.setPointSize(size)
644 self._set_font(font)
649 self._set_font(font)
645
650
646 def select_all(self):
651 def select_all(self):
647 """ Selects all the text in the buffer.
652 """ Selects all the text in the buffer.
648 """
653 """
649 self._control.selectAll()
654 self._control.selectAll()
650
655
651 def _get_tab_width(self):
656 def _get_tab_width(self):
652 """ The width (in terms of space characters) for tab characters.
657 """ The width (in terms of space characters) for tab characters.
653 """
658 """
654 return self._tab_width
659 return self._tab_width
655
660
656 def _set_tab_width(self, tab_width):
661 def _set_tab_width(self, tab_width):
657 """ Sets the width (in terms of space characters) for tab characters.
662 """ Sets the width (in terms of space characters) for tab characters.
658 """
663 """
659 font_metrics = QtGui.QFontMetrics(self.font)
664 font_metrics = QtGui.QFontMetrics(self.font)
660 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
665 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
661
666
662 self._tab_width = tab_width
667 self._tab_width = tab_width
663
668
664 tab_width = property(_get_tab_width, _set_tab_width)
669 tab_width = property(_get_tab_width, _set_tab_width)
665
670
666 def undo(self):
671 def undo(self):
667 """ Undo the last operation. If there is no operation to undo, nothing
672 """ Undo the last operation. If there is no operation to undo, nothing
668 happens.
673 happens.
669 """
674 """
670 self._control.undo()
675 self._control.undo()
671
676
672 #---------------------------------------------------------------------------
677 #---------------------------------------------------------------------------
673 # 'ConsoleWidget' abstract interface
678 # 'ConsoleWidget' abstract interface
674 #---------------------------------------------------------------------------
679 #---------------------------------------------------------------------------
675
680
676 def _is_complete(self, source, interactive):
681 def _is_complete(self, source, interactive):
677 """ Returns whether 'source' can be executed. When triggered by an
682 """ Returns whether 'source' can be executed. When triggered by an
678 Enter/Return key press, 'interactive' is True; otherwise, it is
683 Enter/Return key press, 'interactive' is True; otherwise, it is
679 False.
684 False.
680 """
685 """
681 raise NotImplementedError
686 raise NotImplementedError
682
687
683 def _execute(self, source, hidden):
688 def _execute(self, source, hidden):
684 """ Execute 'source'. If 'hidden', do not show any output.
689 """ Execute 'source'. If 'hidden', do not show any output.
685 """
690 """
686 raise NotImplementedError
691 raise NotImplementedError
687
692
688 def _prompt_started_hook(self):
693 def _prompt_started_hook(self):
689 """ Called immediately after a new prompt is displayed.
694 """ Called immediately after a new prompt is displayed.
690 """
695 """
691 pass
696 pass
692
697
693 def _prompt_finished_hook(self):
698 def _prompt_finished_hook(self):
694 """ Called immediately after a prompt is finished, i.e. when some input
699 """ Called immediately after a prompt is finished, i.e. when some input
695 will be processed and a new prompt displayed.
700 will be processed and a new prompt displayed.
696 """
701 """
697 pass
702 pass
698
703
699 def _up_pressed(self, shift_modifier):
704 def _up_pressed(self, shift_modifier):
700 """ Called when the up key is pressed. Returns whether to continue
705 """ Called when the up key is pressed. Returns whether to continue
701 processing the event.
706 processing the event.
702 """
707 """
703 return True
708 return True
704
709
705 def _down_pressed(self, shift_modifier):
710 def _down_pressed(self, shift_modifier):
706 """ Called when the down key is pressed. Returns whether to continue
711 """ Called when the down key is pressed. Returns whether to continue
707 processing the event.
712 processing the event.
708 """
713 """
709 return True
714 return True
710
715
711 def _tab_pressed(self):
716 def _tab_pressed(self):
712 """ Called when the tab key is pressed. Returns whether to continue
717 """ Called when the tab key is pressed. Returns whether to continue
713 processing the event.
718 processing the event.
714 """
719 """
715 return False
720 return False
716
721
717 #--------------------------------------------------------------------------
722 #--------------------------------------------------------------------------
718 # 'ConsoleWidget' protected interface
723 # 'ConsoleWidget' protected interface
719 #--------------------------------------------------------------------------
724 #--------------------------------------------------------------------------
720
725
721 def _append_html(self, html):
726 def _append_custom(self, insert, input, before_prompt=False):
722 """ Appends html at the end of the console buffer.
727 """ A low-level method for appending content to the end of the buffer.
728
729 If 'before_prompt' is enabled, the content will be inserted before the
730 current prompt, if there is one.
723 """
731 """
724 cursor = self._get_end_cursor()
732 # Determine where to insert the content.
725 self._insert_html(cursor, html)
733 cursor = self._control.textCursor()
734 if before_prompt and not self._executing:
735 cursor.setPosition(self._append_before_prompt_pos)
736 else:
737 cursor.movePosition(QtGui.QTextCursor.End)
738 start_pos = cursor.position()
726
739
727 def _append_html_fetching_plain_text(self, html):
740 # Perform the insertion.
728 """ Appends 'html', then returns the plain text version of it.
741 result = insert(cursor, input)
729 """
742
730 cursor = self._get_end_cursor()
743 # Adjust the prompt position if we have inserted before it. This is safe
731 return self._insert_html_fetching_plain_text(cursor, html)
744 # because buffer truncation is disabled when not executing.
745 if before_prompt and not self._executing:
746 diff = cursor.position() - start_pos
747 self._append_before_prompt_pos += diff
748 self._prompt_pos += diff
749
750 return result
732
751
733 def _append_plain_text(self, text):
752 def _append_html(self, html, before_prompt=False):
734 """ Appends plain text at the end of the console buffer, processing
753 """ Appends HTML at the end of the console buffer.
735 ANSI codes if enabled.
736 """
754 """
737 cursor = self._get_end_cursor()
755 self._append_custom(self._insert_html, html, before_prompt)
738 self._insert_plain_text(cursor, text)
739
756
740 def _append_plain_text_keeping_prompt(self, text):
757 def _append_html_fetching_plain_text(self, html, before_prompt=False):
741 """ Writes 'text' after the current prompt, then restores the old prompt
758 """ Appends HTML, then returns the plain text version of it.
742 with its old input buffer.
743 """
759 """
744 input_buffer = self.input_buffer
760 return self._append_custom(self._insert_html_fetching_plain_text,
745 self._append_plain_text('\n')
761 html, before_prompt)
746 self._prompt_finished()
747
762
748 self._append_plain_text(text)
763 def _append_plain_text(self, text, before_prompt=False):
749 self._show_prompt()
764 """ Appends plain text, processing ANSI codes if enabled.
750 self.input_buffer = input_buffer
765 """
766 self._append_custom(self._insert_plain_text, text, before_prompt)
751
767
752 def _cancel_text_completion(self):
768 def _cancel_text_completion(self):
753 """ If text completion is progress, cancel it.
769 """ If text completion is progress, cancel it.
754 """
770 """
755 if self._text_completing_pos:
771 if self._text_completing_pos:
756 self._clear_temporary_buffer()
772 self._clear_temporary_buffer()
757 self._text_completing_pos = 0
773 self._text_completing_pos = 0
758
774
759 def _clear_temporary_buffer(self):
775 def _clear_temporary_buffer(self):
760 """ Clears the "temporary text" buffer, i.e. all the text following
776 """ Clears the "temporary text" buffer, i.e. all the text following
761 the prompt region.
777 the prompt region.
762 """
778 """
763 # Select and remove all text below the input buffer.
779 # Select and remove all text below the input buffer.
764 cursor = self._get_prompt_cursor()
780 cursor = self._get_prompt_cursor()
765 prompt = self._continuation_prompt.lstrip()
781 prompt = self._continuation_prompt.lstrip()
766 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
782 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
767 temp_cursor = QtGui.QTextCursor(cursor)
783 temp_cursor = QtGui.QTextCursor(cursor)
768 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
784 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
769 text = temp_cursor.selection().toPlainText().lstrip()
785 text = temp_cursor.selection().toPlainText().lstrip()
770 if not text.startswith(prompt):
786 if not text.startswith(prompt):
771 break
787 break
772 else:
788 else:
773 # We've reached the end of the input buffer and no text follows.
789 # We've reached the end of the input buffer and no text follows.
774 return
790 return
775 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
791 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
776 cursor.movePosition(QtGui.QTextCursor.End,
792 cursor.movePosition(QtGui.QTextCursor.End,
777 QtGui.QTextCursor.KeepAnchor)
793 QtGui.QTextCursor.KeepAnchor)
778 cursor.removeSelectedText()
794 cursor.removeSelectedText()
779
795
780 # After doing this, we have no choice but to clear the undo/redo
796 # After doing this, we have no choice but to clear the undo/redo
781 # history. Otherwise, the text is not "temporary" at all, because it
797 # history. Otherwise, the text is not "temporary" at all, because it
782 # can be recalled with undo/redo. Unfortunately, Qt does not expose
798 # can be recalled with undo/redo. Unfortunately, Qt does not expose
783 # fine-grained control to the undo/redo system.
799 # fine-grained control to the undo/redo system.
784 if self._control.isUndoRedoEnabled():
800 if self._control.isUndoRedoEnabled():
785 self._control.setUndoRedoEnabled(False)
801 self._control.setUndoRedoEnabled(False)
786 self._control.setUndoRedoEnabled(True)
802 self._control.setUndoRedoEnabled(True)
787
803
788 def _complete_with_items(self, cursor, items):
804 def _complete_with_items(self, cursor, items):
789 """ Performs completion with 'items' at the specified cursor location.
805 """ Performs completion with 'items' at the specified cursor location.
790 """
806 """
791 self._cancel_text_completion()
807 self._cancel_text_completion()
792
808
793 if len(items) == 1:
809 if len(items) == 1:
794 cursor.setPosition(self._control.textCursor().position(),
810 cursor.setPosition(self._control.textCursor().position(),
795 QtGui.QTextCursor.KeepAnchor)
811 QtGui.QTextCursor.KeepAnchor)
796 cursor.insertText(items[0])
812 cursor.insertText(items[0])
797
813
798 elif len(items) > 1:
814 elif len(items) > 1:
799 current_pos = self._control.textCursor().position()
815 current_pos = self._control.textCursor().position()
800 prefix = commonprefix(items)
816 prefix = commonprefix(items)
801 if prefix:
817 if prefix:
802 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
818 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
803 cursor.insertText(prefix)
819 cursor.insertText(prefix)
804 current_pos = cursor.position()
820 current_pos = cursor.position()
805
821
806 if self.gui_completion:
822 if self.gui_completion:
807 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
823 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
808 self._completion_widget.show_items(cursor, items)
824 self._completion_widget.show_items(cursor, items)
809 else:
825 else:
810 cursor.beginEditBlock()
826 cursor.beginEditBlock()
811 self._append_plain_text('\n')
827 self._append_plain_text('\n')
812 self._page(self._format_as_columns(items))
828 self._page(self._format_as_columns(items))
813 cursor.endEditBlock()
829 cursor.endEditBlock()
814
830
815 cursor.setPosition(current_pos)
831 cursor.setPosition(current_pos)
816 self._control.moveCursor(QtGui.QTextCursor.End)
832 self._control.moveCursor(QtGui.QTextCursor.End)
817 self._control.setTextCursor(cursor)
833 self._control.setTextCursor(cursor)
818 self._text_completing_pos = current_pos
834 self._text_completing_pos = current_pos
819
835
820 def _context_menu_make(self, pos):
836 def _context_menu_make(self, pos):
821 """ Creates a context menu for the given QPoint (in widget coordinates).
837 """ Creates a context menu for the given QPoint (in widget coordinates).
822 """
838 """
823 menu = QtGui.QMenu(self)
839 menu = QtGui.QMenu(self)
824
840
825 cut_action = menu.addAction('Cut', self.cut)
841 cut_action = menu.addAction('Cut', self.cut)
826 cut_action.setEnabled(self.can_cut())
842 cut_action.setEnabled(self.can_cut())
827 cut_action.setShortcut(QtGui.QKeySequence.Cut)
843 cut_action.setShortcut(QtGui.QKeySequence.Cut)
828
844
829 copy_action = menu.addAction('Copy', self.copy)
845 copy_action = menu.addAction('Copy', self.copy)
830 copy_action.setEnabled(self.can_copy())
846 copy_action.setEnabled(self.can_copy())
831 copy_action.setShortcut(QtGui.QKeySequence.Copy)
847 copy_action.setShortcut(QtGui.QKeySequence.Copy)
832
848
833 paste_action = menu.addAction('Paste', self.paste)
849 paste_action = menu.addAction('Paste', self.paste)
834 paste_action.setEnabled(self.can_paste())
850 paste_action.setEnabled(self.can_paste())
835 paste_action.setShortcut(QtGui.QKeySequence.Paste)
851 paste_action.setShortcut(QtGui.QKeySequence.Paste)
836
852
837 menu.addSeparator()
853 menu.addSeparator()
838 menu.addAction(self._select_all_action)
854 menu.addAction(self._select_all_action)
839
855
840 menu.addSeparator()
856 menu.addSeparator()
841 menu.addAction(self._export_action)
857 menu.addAction(self._export_action)
842 menu.addAction(self._print_action)
858 menu.addAction(self._print_action)
843
859
844 return menu
860 return menu
845
861
846 def _control_key_down(self, modifiers, include_command=False):
862 def _control_key_down(self, modifiers, include_command=False):
847 """ Given a KeyboardModifiers flags object, return whether the Control
863 """ Given a KeyboardModifiers flags object, return whether the Control
848 key is down.
864 key is down.
849
865
850 Parameters:
866 Parameters:
851 -----------
867 -----------
852 include_command : bool, optional (default True)
868 include_command : bool, optional (default True)
853 Whether to treat the Command key as a (mutually exclusive) synonym
869 Whether to treat the Command key as a (mutually exclusive) synonym
854 for Control when in Mac OS.
870 for Control when in Mac OS.
855 """
871 """
856 # Note that on Mac OS, ControlModifier corresponds to the Command key
872 # Note that on Mac OS, ControlModifier corresponds to the Command key
857 # while MetaModifier corresponds to the Control key.
873 # while MetaModifier corresponds to the Control key.
858 if sys.platform == 'darwin':
874 if sys.platform == 'darwin':
859 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
875 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
860 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
876 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
861 else:
877 else:
862 return bool(modifiers & QtCore.Qt.ControlModifier)
878 return bool(modifiers & QtCore.Qt.ControlModifier)
863
879
864 def _create_control(self):
880 def _create_control(self):
865 """ Creates and connects the underlying text widget.
881 """ Creates and connects the underlying text widget.
866 """
882 """
867 # Create the underlying control.
883 # Create the underlying control.
868 if self.kind == 'plain':
884 if self.kind == 'plain':
869 control = QtGui.QPlainTextEdit()
885 control = QtGui.QPlainTextEdit()
870 elif self.kind == 'rich':
886 elif self.kind == 'rich':
871 control = QtGui.QTextEdit()
887 control = QtGui.QTextEdit()
872 control.setAcceptRichText(False)
888 control.setAcceptRichText(False)
873
889
874 # Install event filters. The filter on the viewport is needed for
890 # Install event filters. The filter on the viewport is needed for
875 # mouse events and drag events.
891 # mouse events and drag events.
876 control.installEventFilter(self)
892 control.installEventFilter(self)
877 control.viewport().installEventFilter(self)
893 control.viewport().installEventFilter(self)
878
894
879 # Connect signals.
895 # Connect signals.
880 control.cursorPositionChanged.connect(self._cursor_position_changed)
896 control.cursorPositionChanged.connect(self._cursor_position_changed)
881 control.customContextMenuRequested.connect(
897 control.customContextMenuRequested.connect(
882 self._custom_context_menu_requested)
898 self._custom_context_menu_requested)
883 control.copyAvailable.connect(self.copy_available)
899 control.copyAvailable.connect(self.copy_available)
884 control.redoAvailable.connect(self.redo_available)
900 control.redoAvailable.connect(self.redo_available)
885 control.undoAvailable.connect(self.undo_available)
901 control.undoAvailable.connect(self.undo_available)
886
902
887 # Hijack the document size change signal to prevent Qt from adjusting
903 # Hijack the document size change signal to prevent Qt from adjusting
888 # the viewport's scrollbar. We are relying on an implementation detail
904 # the viewport's scrollbar. We are relying on an implementation detail
889 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
905 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
890 # this functionality we cannot create a nice terminal interface.
906 # this functionality we cannot create a nice terminal interface.
891 layout = control.document().documentLayout()
907 layout = control.document().documentLayout()
892 layout.documentSizeChanged.disconnect()
908 layout.documentSizeChanged.disconnect()
893 layout.documentSizeChanged.connect(self._adjust_scrollbars)
909 layout.documentSizeChanged.connect(self._adjust_scrollbars)
894
910
895 # Configure the control.
911 # Configure the control.
896 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
912 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
897 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
913 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
898 control.setReadOnly(True)
914 control.setReadOnly(True)
899 control.setUndoRedoEnabled(False)
915 control.setUndoRedoEnabled(False)
900 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
916 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
901 return control
917 return control
902
918
903 def _create_page_control(self):
919 def _create_page_control(self):
904 """ Creates and connects the underlying paging widget.
920 """ Creates and connects the underlying paging widget.
905 """
921 """
906 if self.kind == 'plain':
922 if self.kind == 'plain':
907 control = QtGui.QPlainTextEdit()
923 control = QtGui.QPlainTextEdit()
908 elif self.kind == 'rich':
924 elif self.kind == 'rich':
909 control = QtGui.QTextEdit()
925 control = QtGui.QTextEdit()
910 control.installEventFilter(self)
926 control.installEventFilter(self)
911 control.setReadOnly(True)
927 control.setReadOnly(True)
912 control.setUndoRedoEnabled(False)
928 control.setUndoRedoEnabled(False)
913 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
929 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
914 return control
930 return control
915
931
916 def _event_filter_console_keypress(self, event):
932 def _event_filter_console_keypress(self, event):
917 """ Filter key events for the underlying text widget to create a
933 """ Filter key events for the underlying text widget to create a
918 console-like interface.
934 console-like interface.
919 """
935 """
920 intercepted = False
936 intercepted = False
921 cursor = self._control.textCursor()
937 cursor = self._control.textCursor()
922 position = cursor.position()
938 position = cursor.position()
923 key = event.key()
939 key = event.key()
924 ctrl_down = self._control_key_down(event.modifiers())
940 ctrl_down = self._control_key_down(event.modifiers())
925 alt_down = event.modifiers() & QtCore.Qt.AltModifier
941 alt_down = event.modifiers() & QtCore.Qt.AltModifier
926 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
942 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
927
943
928 #------ Special sequences ----------------------------------------------
944 #------ Special sequences ----------------------------------------------
929
945
930 if event.matches(QtGui.QKeySequence.Copy):
946 if event.matches(QtGui.QKeySequence.Copy):
931 self.copy()
947 self.copy()
932 intercepted = True
948 intercepted = True
933
949
934 elif event.matches(QtGui.QKeySequence.Cut):
950 elif event.matches(QtGui.QKeySequence.Cut):
935 self.cut()
951 self.cut()
936 intercepted = True
952 intercepted = True
937
953
938 elif event.matches(QtGui.QKeySequence.Paste):
954 elif event.matches(QtGui.QKeySequence.Paste):
939 self.paste()
955 self.paste()
940 intercepted = True
956 intercepted = True
941
957
942 #------ Special modifier logic -----------------------------------------
958 #------ Special modifier logic -----------------------------------------
943
959
944 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
960 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
945 intercepted = True
961 intercepted = True
946
962
947 # Special handling when tab completing in text mode.
963 # Special handling when tab completing in text mode.
948 self._cancel_text_completion()
964 self._cancel_text_completion()
949
965
950 if self._in_buffer(position):
966 if self._in_buffer(position):
951 if self._reading:
967 if self._reading:
952 self._append_plain_text('\n')
968 self._append_plain_text('\n')
953 self._reading = False
969 self._reading = False
954 if self._reading_callback:
970 if self._reading_callback:
955 self._reading_callback()
971 self._reading_callback()
956
972
957 # If the input buffer is a single line or there is only
973 # If the input buffer is a single line or there is only
958 # whitespace after the cursor, execute. Otherwise, split the
974 # whitespace after the cursor, execute. Otherwise, split the
959 # line with a continuation prompt.
975 # line with a continuation prompt.
960 elif not self._executing:
976 elif not self._executing:
961 cursor.movePosition(QtGui.QTextCursor.End,
977 cursor.movePosition(QtGui.QTextCursor.End,
962 QtGui.QTextCursor.KeepAnchor)
978 QtGui.QTextCursor.KeepAnchor)
963 at_end = len(cursor.selectedText().strip()) == 0
979 at_end = len(cursor.selectedText().strip()) == 0
964 single_line = (self._get_end_cursor().blockNumber() ==
980 single_line = (self._get_end_cursor().blockNumber() ==
965 self._get_prompt_cursor().blockNumber())
981 self._get_prompt_cursor().blockNumber())
966 if (at_end or shift_down or single_line) and not ctrl_down:
982 if (at_end or shift_down or single_line) and not ctrl_down:
967 self.execute(interactive = not shift_down)
983 self.execute(interactive = not shift_down)
968 else:
984 else:
969 # Do this inside an edit block for clean undo/redo.
985 # Do this inside an edit block for clean undo/redo.
970 cursor.beginEditBlock()
986 cursor.beginEditBlock()
971 cursor.setPosition(position)
987 cursor.setPosition(position)
972 cursor.insertText('\n')
988 cursor.insertText('\n')
973 self._insert_continuation_prompt(cursor)
989 self._insert_continuation_prompt(cursor)
974 cursor.endEditBlock()
990 cursor.endEditBlock()
975
991
976 # Ensure that the whole input buffer is visible.
992 # Ensure that the whole input buffer is visible.
977 # FIXME: This will not be usable if the input buffer is
993 # FIXME: This will not be usable if the input buffer is
978 # taller than the console widget.
994 # taller than the console widget.
979 self._control.moveCursor(QtGui.QTextCursor.End)
995 self._control.moveCursor(QtGui.QTextCursor.End)
980 self._control.setTextCursor(cursor)
996 self._control.setTextCursor(cursor)
981
997
982 #------ Control/Cmd modifier -------------------------------------------
998 #------ Control/Cmd modifier -------------------------------------------
983
999
984 elif ctrl_down:
1000 elif ctrl_down:
985 if key == QtCore.Qt.Key_G:
1001 if key == QtCore.Qt.Key_G:
986 self._keyboard_quit()
1002 self._keyboard_quit()
987 intercepted = True
1003 intercepted = True
988
1004
989 elif key == QtCore.Qt.Key_K:
1005 elif key == QtCore.Qt.Key_K:
990 if self._in_buffer(position):
1006 if self._in_buffer(position):
991 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1007 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
992 QtGui.QTextCursor.KeepAnchor)
1008 QtGui.QTextCursor.KeepAnchor)
993 if not cursor.hasSelection():
1009 if not cursor.hasSelection():
994 # Line deletion (remove continuation prompt)
1010 # Line deletion (remove continuation prompt)
995 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1011 cursor.movePosition(QtGui.QTextCursor.NextBlock,
996 QtGui.QTextCursor.KeepAnchor)
1012 QtGui.QTextCursor.KeepAnchor)
997 cursor.movePosition(QtGui.QTextCursor.Right,
1013 cursor.movePosition(QtGui.QTextCursor.Right,
998 QtGui.QTextCursor.KeepAnchor,
1014 QtGui.QTextCursor.KeepAnchor,
999 len(self._continuation_prompt))
1015 len(self._continuation_prompt))
1000 self._kill_ring.kill_cursor(cursor)
1016 self._kill_ring.kill_cursor(cursor)
1001 intercepted = True
1017 intercepted = True
1002
1018
1003 elif key == QtCore.Qt.Key_L:
1019 elif key == QtCore.Qt.Key_L:
1004 self.prompt_to_top()
1020 self.prompt_to_top()
1005 intercepted = True
1021 intercepted = True
1006
1022
1007 elif key == QtCore.Qt.Key_O:
1023 elif key == QtCore.Qt.Key_O:
1008 if self._page_control and self._page_control.isVisible():
1024 if self._page_control and self._page_control.isVisible():
1009 self._page_control.setFocus()
1025 self._page_control.setFocus()
1010 intercepted = True
1026 intercepted = True
1011
1027
1012 elif key == QtCore.Qt.Key_U:
1028 elif key == QtCore.Qt.Key_U:
1013 if self._in_buffer(position):
1029 if self._in_buffer(position):
1014 start_line = cursor.blockNumber()
1030 start_line = cursor.blockNumber()
1015 if start_line == self._get_prompt_cursor().blockNumber():
1031 if start_line == self._get_prompt_cursor().blockNumber():
1016 offset = len(self._prompt)
1032 offset = len(self._prompt)
1017 else:
1033 else:
1018 offset = len(self._continuation_prompt)
1034 offset = len(self._continuation_prompt)
1019 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1035 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1020 QtGui.QTextCursor.KeepAnchor)
1036 QtGui.QTextCursor.KeepAnchor)
1021 cursor.movePosition(QtGui.QTextCursor.Right,
1037 cursor.movePosition(QtGui.QTextCursor.Right,
1022 QtGui.QTextCursor.KeepAnchor, offset)
1038 QtGui.QTextCursor.KeepAnchor, offset)
1023 self._kill_ring.kill_cursor(cursor)
1039 self._kill_ring.kill_cursor(cursor)
1024 intercepted = True
1040 intercepted = True
1025
1041
1026 elif key == QtCore.Qt.Key_Y:
1042 elif key == QtCore.Qt.Key_Y:
1027 self._keep_cursor_in_buffer()
1043 self._keep_cursor_in_buffer()
1028 self._kill_ring.yank()
1044 self._kill_ring.yank()
1029 intercepted = True
1045 intercepted = True
1030
1046
1031 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1047 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1032 intercepted = True
1048 intercepted = True
1033
1049
1034 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1050 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1035 self.change_font_size(1)
1051 self.change_font_size(1)
1036 intercepted = True
1052 intercepted = True
1037
1053
1038 elif key == QtCore.Qt.Key_Minus:
1054 elif key == QtCore.Qt.Key_Minus:
1039 self.change_font_size(-1)
1055 self.change_font_size(-1)
1040 intercepted = True
1056 intercepted = True
1041
1057
1042 elif key == QtCore.Qt.Key_0:
1058 elif key == QtCore.Qt.Key_0:
1043 self.reset_font()
1059 self.reset_font()
1044 intercepted = True
1060 intercepted = True
1045
1061
1046 #------ Alt modifier ---------------------------------------------------
1062 #------ Alt modifier ---------------------------------------------------
1047
1063
1048 elif alt_down:
1064 elif alt_down:
1049 if key == QtCore.Qt.Key_B:
1065 if key == QtCore.Qt.Key_B:
1050 self._set_cursor(self._get_word_start_cursor(position))
1066 self._set_cursor(self._get_word_start_cursor(position))
1051 intercepted = True
1067 intercepted = True
1052
1068
1053 elif key == QtCore.Qt.Key_F:
1069 elif key == QtCore.Qt.Key_F:
1054 self._set_cursor(self._get_word_end_cursor(position))
1070 self._set_cursor(self._get_word_end_cursor(position))
1055 intercepted = True
1071 intercepted = True
1056
1072
1057 elif key == QtCore.Qt.Key_Y:
1073 elif key == QtCore.Qt.Key_Y:
1058 self._kill_ring.rotate()
1074 self._kill_ring.rotate()
1059 intercepted = True
1075 intercepted = True
1060
1076
1061 elif key == QtCore.Qt.Key_Backspace:
1077 elif key == QtCore.Qt.Key_Backspace:
1062 cursor = self._get_word_start_cursor(position)
1078 cursor = self._get_word_start_cursor(position)
1063 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1079 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1064 self._kill_ring.kill_cursor(cursor)
1080 self._kill_ring.kill_cursor(cursor)
1065 intercepted = True
1081 intercepted = True
1066
1082
1067 elif key == QtCore.Qt.Key_D:
1083 elif key == QtCore.Qt.Key_D:
1068 cursor = self._get_word_end_cursor(position)
1084 cursor = self._get_word_end_cursor(position)
1069 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1085 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1070 self._kill_ring.kill_cursor(cursor)
1086 self._kill_ring.kill_cursor(cursor)
1071 intercepted = True
1087 intercepted = True
1072
1088
1073 elif key == QtCore.Qt.Key_Delete:
1089 elif key == QtCore.Qt.Key_Delete:
1074 intercepted = True
1090 intercepted = True
1075
1091
1076 elif key == QtCore.Qt.Key_Greater:
1092 elif key == QtCore.Qt.Key_Greater:
1077 self._control.moveCursor(QtGui.QTextCursor.End)
1093 self._control.moveCursor(QtGui.QTextCursor.End)
1078 intercepted = True
1094 intercepted = True
1079
1095
1080 elif key == QtCore.Qt.Key_Less:
1096 elif key == QtCore.Qt.Key_Less:
1081 self._control.setTextCursor(self._get_prompt_cursor())
1097 self._control.setTextCursor(self._get_prompt_cursor())
1082 intercepted = True
1098 intercepted = True
1083
1099
1084 #------ No modifiers ---------------------------------------------------
1100 #------ No modifiers ---------------------------------------------------
1085
1101
1086 else:
1102 else:
1087 if shift_down:
1103 if shift_down:
1088 anchormode = QtGui.QTextCursor.KeepAnchor
1104 anchormode = QtGui.QTextCursor.KeepAnchor
1089 else:
1105 else:
1090 anchormode = QtGui.QTextCursor.MoveAnchor
1106 anchormode = QtGui.QTextCursor.MoveAnchor
1091
1107
1092 if key == QtCore.Qt.Key_Escape:
1108 if key == QtCore.Qt.Key_Escape:
1093 self._keyboard_quit()
1109 self._keyboard_quit()
1094 intercepted = True
1110 intercepted = True
1095
1111
1096 elif key == QtCore.Qt.Key_Up:
1112 elif key == QtCore.Qt.Key_Up:
1097 if self._reading or not self._up_pressed(shift_down):
1113 if self._reading or not self._up_pressed(shift_down):
1098 intercepted = True
1114 intercepted = True
1099 else:
1115 else:
1100 prompt_line = self._get_prompt_cursor().blockNumber()
1116 prompt_line = self._get_prompt_cursor().blockNumber()
1101 intercepted = cursor.blockNumber() <= prompt_line
1117 intercepted = cursor.blockNumber() <= prompt_line
1102
1118
1103 elif key == QtCore.Qt.Key_Down:
1119 elif key == QtCore.Qt.Key_Down:
1104 if self._reading or not self._down_pressed(shift_down):
1120 if self._reading or not self._down_pressed(shift_down):
1105 intercepted = True
1121 intercepted = True
1106 else:
1122 else:
1107 end_line = self._get_end_cursor().blockNumber()
1123 end_line = self._get_end_cursor().blockNumber()
1108 intercepted = cursor.blockNumber() == end_line
1124 intercepted = cursor.blockNumber() == end_line
1109
1125
1110 elif key == QtCore.Qt.Key_Tab:
1126 elif key == QtCore.Qt.Key_Tab:
1111 if not self._reading:
1127 if not self._reading:
1112 intercepted = not self._tab_pressed()
1128 intercepted = not self._tab_pressed()
1113
1129
1114 elif key == QtCore.Qt.Key_Left:
1130 elif key == QtCore.Qt.Key_Left:
1115
1131
1116 # Move to the previous line
1132 # Move to the previous line
1117 line, col = cursor.blockNumber(), cursor.columnNumber()
1133 line, col = cursor.blockNumber(), cursor.columnNumber()
1118 if line > self._get_prompt_cursor().blockNumber() and \
1134 if line > self._get_prompt_cursor().blockNumber() and \
1119 col == len(self._continuation_prompt):
1135 col == len(self._continuation_prompt):
1120 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1136 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1121 mode=anchormode)
1137 mode=anchormode)
1122 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1138 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1123 mode=anchormode)
1139 mode=anchormode)
1124 intercepted = True
1140 intercepted = True
1125
1141
1126 # Regular left movement
1142 # Regular left movement
1127 else:
1143 else:
1128 intercepted = not self._in_buffer(position - 1)
1144 intercepted = not self._in_buffer(position - 1)
1129
1145
1130 elif key == QtCore.Qt.Key_Right:
1146 elif key == QtCore.Qt.Key_Right:
1131 original_block_number = cursor.blockNumber()
1147 original_block_number = cursor.blockNumber()
1132 cursor.movePosition(QtGui.QTextCursor.Right,
1148 cursor.movePosition(QtGui.QTextCursor.Right,
1133 mode=anchormode)
1149 mode=anchormode)
1134 if cursor.blockNumber() != original_block_number:
1150 if cursor.blockNumber() != original_block_number:
1135 cursor.movePosition(QtGui.QTextCursor.Right,
1151 cursor.movePosition(QtGui.QTextCursor.Right,
1136 n=len(self._continuation_prompt),
1152 n=len(self._continuation_prompt),
1137 mode=anchormode)
1153 mode=anchormode)
1138 self._set_cursor(cursor)
1154 self._set_cursor(cursor)
1139 intercepted = True
1155 intercepted = True
1140
1156
1141 elif key == QtCore.Qt.Key_Home:
1157 elif key == QtCore.Qt.Key_Home:
1142 start_line = cursor.blockNumber()
1158 start_line = cursor.blockNumber()
1143 if start_line == self._get_prompt_cursor().blockNumber():
1159 if start_line == self._get_prompt_cursor().blockNumber():
1144 start_pos = self._prompt_pos
1160 start_pos = self._prompt_pos
1145 else:
1161 else:
1146 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1162 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1147 QtGui.QTextCursor.KeepAnchor)
1163 QtGui.QTextCursor.KeepAnchor)
1148 start_pos = cursor.position()
1164 start_pos = cursor.position()
1149 start_pos += len(self._continuation_prompt)
1165 start_pos += len(self._continuation_prompt)
1150 cursor.setPosition(position)
1166 cursor.setPosition(position)
1151 if shift_down and self._in_buffer(position):
1167 if shift_down and self._in_buffer(position):
1152 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1168 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1153 else:
1169 else:
1154 cursor.setPosition(start_pos)
1170 cursor.setPosition(start_pos)
1155 self._set_cursor(cursor)
1171 self._set_cursor(cursor)
1156 intercepted = True
1172 intercepted = True
1157
1173
1158 elif key == QtCore.Qt.Key_Backspace:
1174 elif key == QtCore.Qt.Key_Backspace:
1159
1175
1160 # Line deletion (remove continuation prompt)
1176 # Line deletion (remove continuation prompt)
1161 line, col = cursor.blockNumber(), cursor.columnNumber()
1177 line, col = cursor.blockNumber(), cursor.columnNumber()
1162 if not self._reading and \
1178 if not self._reading and \
1163 col == len(self._continuation_prompt) and \
1179 col == len(self._continuation_prompt) and \
1164 line > self._get_prompt_cursor().blockNumber():
1180 line > self._get_prompt_cursor().blockNumber():
1165 cursor.beginEditBlock()
1181 cursor.beginEditBlock()
1166 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1182 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1167 QtGui.QTextCursor.KeepAnchor)
1183 QtGui.QTextCursor.KeepAnchor)
1168 cursor.removeSelectedText()
1184 cursor.removeSelectedText()
1169 cursor.deletePreviousChar()
1185 cursor.deletePreviousChar()
1170 cursor.endEditBlock()
1186 cursor.endEditBlock()
1171 intercepted = True
1187 intercepted = True
1172
1188
1173 # Regular backwards deletion
1189 # Regular backwards deletion
1174 else:
1190 else:
1175 anchor = cursor.anchor()
1191 anchor = cursor.anchor()
1176 if anchor == position:
1192 if anchor == position:
1177 intercepted = not self._in_buffer(position - 1)
1193 intercepted = not self._in_buffer(position - 1)
1178 else:
1194 else:
1179 intercepted = not self._in_buffer(min(anchor, position))
1195 intercepted = not self._in_buffer(min(anchor, position))
1180
1196
1181 elif key == QtCore.Qt.Key_Delete:
1197 elif key == QtCore.Qt.Key_Delete:
1182
1198
1183 # Line deletion (remove continuation prompt)
1199 # Line deletion (remove continuation prompt)
1184 if not self._reading and self._in_buffer(position) and \
1200 if not self._reading and self._in_buffer(position) and \
1185 cursor.atBlockEnd() and not cursor.hasSelection():
1201 cursor.atBlockEnd() and not cursor.hasSelection():
1186 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1202 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1187 QtGui.QTextCursor.KeepAnchor)
1203 QtGui.QTextCursor.KeepAnchor)
1188 cursor.movePosition(QtGui.QTextCursor.Right,
1204 cursor.movePosition(QtGui.QTextCursor.Right,
1189 QtGui.QTextCursor.KeepAnchor,
1205 QtGui.QTextCursor.KeepAnchor,
1190 len(self._continuation_prompt))
1206 len(self._continuation_prompt))
1191 cursor.removeSelectedText()
1207 cursor.removeSelectedText()
1192 intercepted = True
1208 intercepted = True
1193
1209
1194 # Regular forwards deletion:
1210 # Regular forwards deletion:
1195 else:
1211 else:
1196 anchor = cursor.anchor()
1212 anchor = cursor.anchor()
1197 intercepted = (not self._in_buffer(anchor) or
1213 intercepted = (not self._in_buffer(anchor) or
1198 not self._in_buffer(position))
1214 not self._in_buffer(position))
1199
1215
1200 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1216 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1201 # using the keyboard in any part of the buffer.
1217 # using the keyboard in any part of the buffer.
1202 if not self._control_key_down(event.modifiers(), include_command=True):
1218 if not self._control_key_down(event.modifiers(), include_command=True):
1203 self._keep_cursor_in_buffer()
1219 self._keep_cursor_in_buffer()
1204
1220
1205 return intercepted
1221 return intercepted
1206
1222
1207 def _event_filter_page_keypress(self, event):
1223 def _event_filter_page_keypress(self, event):
1208 """ Filter key events for the paging widget to create console-like
1224 """ Filter key events for the paging widget to create console-like
1209 interface.
1225 interface.
1210 """
1226 """
1211 key = event.key()
1227 key = event.key()
1212 ctrl_down = self._control_key_down(event.modifiers())
1228 ctrl_down = self._control_key_down(event.modifiers())
1213 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1229 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1214
1230
1215 if ctrl_down:
1231 if ctrl_down:
1216 if key == QtCore.Qt.Key_O:
1232 if key == QtCore.Qt.Key_O:
1217 self._control.setFocus()
1233 self._control.setFocus()
1218 intercept = True
1234 intercept = True
1219
1235
1220 elif alt_down:
1236 elif alt_down:
1221 if key == QtCore.Qt.Key_Greater:
1237 if key == QtCore.Qt.Key_Greater:
1222 self._page_control.moveCursor(QtGui.QTextCursor.End)
1238 self._page_control.moveCursor(QtGui.QTextCursor.End)
1223 intercepted = True
1239 intercepted = True
1224
1240
1225 elif key == QtCore.Qt.Key_Less:
1241 elif key == QtCore.Qt.Key_Less:
1226 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1242 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1227 intercepted = True
1243 intercepted = True
1228
1244
1229 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1245 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1230 if self._splitter:
1246 if self._splitter:
1231 self._page_control.hide()
1247 self._page_control.hide()
1232 else:
1248 else:
1233 self.layout().setCurrentWidget(self._control)
1249 self.layout().setCurrentWidget(self._control)
1234 return True
1250 return True
1235
1251
1236 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1252 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1237 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1253 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1238 QtCore.Qt.Key_PageDown,
1254 QtCore.Qt.Key_PageDown,
1239 QtCore.Qt.NoModifier)
1255 QtCore.Qt.NoModifier)
1240 QtGui.qApp.sendEvent(self._page_control, new_event)
1256 QtGui.qApp.sendEvent(self._page_control, new_event)
1241 return True
1257 return True
1242
1258
1243 elif key == QtCore.Qt.Key_Backspace:
1259 elif key == QtCore.Qt.Key_Backspace:
1244 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1260 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1245 QtCore.Qt.Key_PageUp,
1261 QtCore.Qt.Key_PageUp,
1246 QtCore.Qt.NoModifier)
1262 QtCore.Qt.NoModifier)
1247 QtGui.qApp.sendEvent(self._page_control, new_event)
1263 QtGui.qApp.sendEvent(self._page_control, new_event)
1248 return True
1264 return True
1249
1265
1250 return False
1266 return False
1251
1267
1252 def _format_as_columns(self, items, separator=' '):
1268 def _format_as_columns(self, items, separator=' '):
1253 """ Transform a list of strings into a single string with columns.
1269 """ Transform a list of strings into a single string with columns.
1254
1270
1255 Parameters
1271 Parameters
1256 ----------
1272 ----------
1257 items : sequence of strings
1273 items : sequence of strings
1258 The strings to process.
1274 The strings to process.
1259
1275
1260 separator : str, optional [default is two spaces]
1276 separator : str, optional [default is two spaces]
1261 The string that separates columns.
1277 The string that separates columns.
1262
1278
1263 Returns
1279 Returns
1264 -------
1280 -------
1265 The formatted string.
1281 The formatted string.
1266 """
1282 """
1267 # Note: this code is adapted from columnize 0.3.2.
1283 # Note: this code is adapted from columnize 0.3.2.
1268 # See http://code.google.com/p/pycolumnize/
1284 # See http://code.google.com/p/pycolumnize/
1269
1285
1270 # Calculate the number of characters available.
1286 # Calculate the number of characters available.
1271 width = self._control.viewport().width()
1287 width = self._control.viewport().width()
1272 char_width = QtGui.QFontMetrics(self.font).width(' ')
1288 char_width = QtGui.QFontMetrics(self.font).width(' ')
1273 displaywidth = max(10, (width / char_width) - 1)
1289 displaywidth = max(10, (width / char_width) - 1)
1274
1290
1275 # Some degenerate cases.
1291 # Some degenerate cases.
1276 size = len(items)
1292 size = len(items)
1277 if size == 0:
1293 if size == 0:
1278 return '\n'
1294 return '\n'
1279 elif size == 1:
1295 elif size == 1:
1280 return '%s\n' % items[0]
1296 return '%s\n' % items[0]
1281
1297
1282 # Try every row count from 1 upwards
1298 # Try every row count from 1 upwards
1283 array_index = lambda nrows, row, col: nrows*col + row
1299 array_index = lambda nrows, row, col: nrows*col + row
1284 for nrows in range(1, size):
1300 for nrows in range(1, size):
1285 ncols = (size + nrows - 1) // nrows
1301 ncols = (size + nrows - 1) // nrows
1286 colwidths = []
1302 colwidths = []
1287 totwidth = -len(separator)
1303 totwidth = -len(separator)
1288 for col in range(ncols):
1304 for col in range(ncols):
1289 # Get max column width for this column
1305 # Get max column width for this column
1290 colwidth = 0
1306 colwidth = 0
1291 for row in range(nrows):
1307 for row in range(nrows):
1292 i = array_index(nrows, row, col)
1308 i = array_index(nrows, row, col)
1293 if i >= size: break
1309 if i >= size: break
1294 x = items[i]
1310 x = items[i]
1295 colwidth = max(colwidth, len(x))
1311 colwidth = max(colwidth, len(x))
1296 colwidths.append(colwidth)
1312 colwidths.append(colwidth)
1297 totwidth += colwidth + len(separator)
1313 totwidth += colwidth + len(separator)
1298 if totwidth > displaywidth:
1314 if totwidth > displaywidth:
1299 break
1315 break
1300 if totwidth <= displaywidth:
1316 if totwidth <= displaywidth:
1301 break
1317 break
1302
1318
1303 # The smallest number of rows computed and the max widths for each
1319 # The smallest number of rows computed and the max widths for each
1304 # column has been obtained. Now we just have to format each of the rows.
1320 # column has been obtained. Now we just have to format each of the rows.
1305 string = ''
1321 string = ''
1306 for row in range(nrows):
1322 for row in range(nrows):
1307 texts = []
1323 texts = []
1308 for col in range(ncols):
1324 for col in range(ncols):
1309 i = row + nrows*col
1325 i = row + nrows*col
1310 if i >= size:
1326 if i >= size:
1311 texts.append('')
1327 texts.append('')
1312 else:
1328 else:
1313 texts.append(items[i])
1329 texts.append(items[i])
1314 while texts and not texts[-1]:
1330 while texts and not texts[-1]:
1315 del texts[-1]
1331 del texts[-1]
1316 for col in range(len(texts)):
1332 for col in range(len(texts)):
1317 texts[col] = texts[col].ljust(colwidths[col])
1333 texts[col] = texts[col].ljust(colwidths[col])
1318 string += '%s\n' % separator.join(texts)
1334 string += '%s\n' % separator.join(texts)
1319 return string
1335 return string
1320
1336
1321 def _get_block_plain_text(self, block):
1337 def _get_block_plain_text(self, block):
1322 """ Given a QTextBlock, return its unformatted text.
1338 """ Given a QTextBlock, return its unformatted text.
1323 """
1339 """
1324 cursor = QtGui.QTextCursor(block)
1340 cursor = QtGui.QTextCursor(block)
1325 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1341 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1326 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1342 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1327 QtGui.QTextCursor.KeepAnchor)
1343 QtGui.QTextCursor.KeepAnchor)
1328 return cursor.selection().toPlainText()
1344 return cursor.selection().toPlainText()
1329
1345
1330 def _get_cursor(self):
1346 def _get_cursor(self):
1331 """ Convenience method that returns a cursor for the current position.
1347 """ Convenience method that returns a cursor for the current position.
1332 """
1348 """
1333 return self._control.textCursor()
1349 return self._control.textCursor()
1334
1350
1335 def _get_end_cursor(self):
1351 def _get_end_cursor(self):
1336 """ Convenience method that returns a cursor for the last character.
1352 """ Convenience method that returns a cursor for the last character.
1337 """
1353 """
1338 cursor = self._control.textCursor()
1354 cursor = self._control.textCursor()
1339 cursor.movePosition(QtGui.QTextCursor.End)
1355 cursor.movePosition(QtGui.QTextCursor.End)
1340 return cursor
1356 return cursor
1341
1357
1342 def _get_input_buffer_cursor_column(self):
1358 def _get_input_buffer_cursor_column(self):
1343 """ Returns the column of the cursor in the input buffer, excluding the
1359 """ Returns the column of the cursor in the input buffer, excluding the
1344 contribution by the prompt, or -1 if there is no such column.
1360 contribution by the prompt, or -1 if there is no such column.
1345 """
1361 """
1346 prompt = self._get_input_buffer_cursor_prompt()
1362 prompt = self._get_input_buffer_cursor_prompt()
1347 if prompt is None:
1363 if prompt is None:
1348 return -1
1364 return -1
1349 else:
1365 else:
1350 cursor = self._control.textCursor()
1366 cursor = self._control.textCursor()
1351 return cursor.columnNumber() - len(prompt)
1367 return cursor.columnNumber() - len(prompt)
1352
1368
1353 def _get_input_buffer_cursor_line(self):
1369 def _get_input_buffer_cursor_line(self):
1354 """ Returns the text of the line of the input buffer that contains the
1370 """ Returns the text of the line of the input buffer that contains the
1355 cursor, or None if there is no such line.
1371 cursor, or None if there is no such line.
1356 """
1372 """
1357 prompt = self._get_input_buffer_cursor_prompt()
1373 prompt = self._get_input_buffer_cursor_prompt()
1358 if prompt is None:
1374 if prompt is None:
1359 return None
1375 return None
1360 else:
1376 else:
1361 cursor = self._control.textCursor()
1377 cursor = self._control.textCursor()
1362 text = self._get_block_plain_text(cursor.block())
1378 text = self._get_block_plain_text(cursor.block())
1363 return text[len(prompt):]
1379 return text[len(prompt):]
1364
1380
1365 def _get_input_buffer_cursor_prompt(self):
1381 def _get_input_buffer_cursor_prompt(self):
1366 """ Returns the (plain text) prompt for line of the input buffer that
1382 """ Returns the (plain text) prompt for line of the input buffer that
1367 contains the cursor, or None if there is no such line.
1383 contains the cursor, or None if there is no such line.
1368 """
1384 """
1369 if self._executing:
1385 if self._executing:
1370 return None
1386 return None
1371 cursor = self._control.textCursor()
1387 cursor = self._control.textCursor()
1372 if cursor.position() >= self._prompt_pos:
1388 if cursor.position() >= self._prompt_pos:
1373 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1389 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1374 return self._prompt
1390 return self._prompt
1375 else:
1391 else:
1376 return self._continuation_prompt
1392 return self._continuation_prompt
1377 else:
1393 else:
1378 return None
1394 return None
1379
1395
1380 def _get_prompt_cursor(self):
1396 def _get_prompt_cursor(self):
1381 """ Convenience method that returns a cursor for the prompt position.
1397 """ Convenience method that returns a cursor for the prompt position.
1382 """
1398 """
1383 cursor = self._control.textCursor()
1399 cursor = self._control.textCursor()
1384 cursor.setPosition(self._prompt_pos)
1400 cursor.setPosition(self._prompt_pos)
1385 return cursor
1401 return cursor
1386
1402
1387 def _get_selection_cursor(self, start, end):
1403 def _get_selection_cursor(self, start, end):
1388 """ Convenience method that returns a cursor with text selected between
1404 """ Convenience method that returns a cursor with text selected between
1389 the positions 'start' and 'end'.
1405 the positions 'start' and 'end'.
1390 """
1406 """
1391 cursor = self._control.textCursor()
1407 cursor = self._control.textCursor()
1392 cursor.setPosition(start)
1408 cursor.setPosition(start)
1393 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1409 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1394 return cursor
1410 return cursor
1395
1411
1396 def _get_word_start_cursor(self, position):
1412 def _get_word_start_cursor(self, position):
1397 """ Find the start of the word to the left the given position. If a
1413 """ Find the start of the word to the left the given position. If a
1398 sequence of non-word characters precedes the first word, skip over
1414 sequence of non-word characters precedes the first word, skip over
1399 them. (This emulates the behavior of bash, emacs, etc.)
1415 them. (This emulates the behavior of bash, emacs, etc.)
1400 """
1416 """
1401 document = self._control.document()
1417 document = self._control.document()
1402 position -= 1
1418 position -= 1
1403 while position >= self._prompt_pos and \
1419 while position >= self._prompt_pos and \
1404 not is_letter_or_number(document.characterAt(position)):
1420 not is_letter_or_number(document.characterAt(position)):
1405 position -= 1
1421 position -= 1
1406 while position >= self._prompt_pos and \
1422 while position >= self._prompt_pos and \
1407 is_letter_or_number(document.characterAt(position)):
1423 is_letter_or_number(document.characterAt(position)):
1408 position -= 1
1424 position -= 1
1409 cursor = self._control.textCursor()
1425 cursor = self._control.textCursor()
1410 cursor.setPosition(position + 1)
1426 cursor.setPosition(position + 1)
1411 return cursor
1427 return cursor
1412
1428
1413 def _get_word_end_cursor(self, position):
1429 def _get_word_end_cursor(self, position):
1414 """ Find the end of the word to the right the given position. If a
1430 """ Find the end of the word to the right the given position. If a
1415 sequence of non-word characters precedes the first word, skip over
1431 sequence of non-word characters precedes the first word, skip over
1416 them. (This emulates the behavior of bash, emacs, etc.)
1432 them. (This emulates the behavior of bash, emacs, etc.)
1417 """
1433 """
1418 document = self._control.document()
1434 document = self._control.document()
1419 end = self._get_end_cursor().position()
1435 end = self._get_end_cursor().position()
1420 while position < end and \
1436 while position < end and \
1421 not is_letter_or_number(document.characterAt(position)):
1437 not is_letter_or_number(document.characterAt(position)):
1422 position += 1
1438 position += 1
1423 while position < end and \
1439 while position < end and \
1424 is_letter_or_number(document.characterAt(position)):
1440 is_letter_or_number(document.characterAt(position)):
1425 position += 1
1441 position += 1
1426 cursor = self._control.textCursor()
1442 cursor = self._control.textCursor()
1427 cursor.setPosition(position)
1443 cursor.setPosition(position)
1428 return cursor
1444 return cursor
1429
1445
1430 def _insert_continuation_prompt(self, cursor):
1446 def _insert_continuation_prompt(self, cursor):
1431 """ Inserts new continuation prompt using the specified cursor.
1447 """ Inserts new continuation prompt using the specified cursor.
1432 """
1448 """
1433 if self._continuation_prompt_html is None:
1449 if self._continuation_prompt_html is None:
1434 self._insert_plain_text(cursor, self._continuation_prompt)
1450 self._insert_plain_text(cursor, self._continuation_prompt)
1435 else:
1451 else:
1436 self._continuation_prompt = self._insert_html_fetching_plain_text(
1452 self._continuation_prompt = self._insert_html_fetching_plain_text(
1437 cursor, self._continuation_prompt_html)
1453 cursor, self._continuation_prompt_html)
1438
1454
1439 def _insert_html(self, cursor, html):
1455 def _insert_html(self, cursor, html):
1440 """ Inserts HTML using the specified cursor in such a way that future
1456 """ Inserts HTML using the specified cursor in such a way that future
1441 formatting is unaffected.
1457 formatting is unaffected.
1442 """
1458 """
1443 cursor.beginEditBlock()
1459 cursor.beginEditBlock()
1444 cursor.insertHtml(html)
1460 cursor.insertHtml(html)
1445
1461
1446 # After inserting HTML, the text document "remembers" it's in "html
1462 # After inserting HTML, the text document "remembers" it's in "html
1447 # mode", which means that subsequent calls adding plain text will result
1463 # mode", which means that subsequent calls adding plain text will result
1448 # in unwanted formatting, lost tab characters, etc. The following code
1464 # in unwanted formatting, lost tab characters, etc. The following code
1449 # hacks around this behavior, which I consider to be a bug in Qt, by
1465 # hacks around this behavior, which I consider to be a bug in Qt, by
1450 # (crudely) resetting the document's style state.
1466 # (crudely) resetting the document's style state.
1451 cursor.movePosition(QtGui.QTextCursor.Left,
1467 cursor.movePosition(QtGui.QTextCursor.Left,
1452 QtGui.QTextCursor.KeepAnchor)
1468 QtGui.QTextCursor.KeepAnchor)
1453 if cursor.selection().toPlainText() == ' ':
1469 if cursor.selection().toPlainText() == ' ':
1454 cursor.removeSelectedText()
1470 cursor.removeSelectedText()
1455 else:
1471 else:
1456 cursor.movePosition(QtGui.QTextCursor.Right)
1472 cursor.movePosition(QtGui.QTextCursor.Right)
1457 cursor.insertText(' ', QtGui.QTextCharFormat())
1473 cursor.insertText(' ', QtGui.QTextCharFormat())
1458 cursor.endEditBlock()
1474 cursor.endEditBlock()
1459
1475
1460 def _insert_html_fetching_plain_text(self, cursor, html):
1476 def _insert_html_fetching_plain_text(self, cursor, html):
1461 """ Inserts HTML using the specified cursor, then returns its plain text
1477 """ Inserts HTML using the specified cursor, then returns its plain text
1462 version.
1478 version.
1463 """
1479 """
1464 cursor.beginEditBlock()
1480 cursor.beginEditBlock()
1465 cursor.removeSelectedText()
1481 cursor.removeSelectedText()
1466
1482
1467 start = cursor.position()
1483 start = cursor.position()
1468 self._insert_html(cursor, html)
1484 self._insert_html(cursor, html)
1469 end = cursor.position()
1485 end = cursor.position()
1470 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1486 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1471 text = cursor.selection().toPlainText()
1487 text = cursor.selection().toPlainText()
1472
1488
1473 cursor.setPosition(end)
1489 cursor.setPosition(end)
1474 cursor.endEditBlock()
1490 cursor.endEditBlock()
1475 return text
1491 return text
1476
1492
1477 def _insert_plain_text(self, cursor, text):
1493 def _insert_plain_text(self, cursor, text):
1478 """ Inserts plain text using the specified cursor, processing ANSI codes
1494 """ Inserts plain text using the specified cursor, processing ANSI codes
1479 if enabled.
1495 if enabled.
1480 """
1496 """
1481 cursor.beginEditBlock()
1497 cursor.beginEditBlock()
1482 if self.ansi_codes:
1498 if self.ansi_codes:
1483 for substring in self._ansi_processor.split_string(text):
1499 for substring in self._ansi_processor.split_string(text):
1484 for act in self._ansi_processor.actions:
1500 for act in self._ansi_processor.actions:
1485
1501
1486 # Unlike real terminal emulators, we don't distinguish
1502 # Unlike real terminal emulators, we don't distinguish
1487 # between the screen and the scrollback buffer. A screen
1503 # between the screen and the scrollback buffer. A screen
1488 # erase request clears everything.
1504 # erase request clears everything.
1489 if act.action == 'erase' and act.area == 'screen':
1505 if act.action == 'erase' and act.area == 'screen':
1490 cursor.select(QtGui.QTextCursor.Document)
1506 cursor.select(QtGui.QTextCursor.Document)
1491 cursor.removeSelectedText()
1507 cursor.removeSelectedText()
1492
1508
1493 # Simulate a form feed by scrolling just past the last line.
1509 # Simulate a form feed by scrolling just past the last line.
1494 elif act.action == 'scroll' and act.unit == 'page':
1510 elif act.action == 'scroll' and act.unit == 'page':
1495 cursor.insertText('\n')
1511 cursor.insertText('\n')
1496 cursor.endEditBlock()
1512 cursor.endEditBlock()
1497 self._set_top_cursor(cursor)
1513 self._set_top_cursor(cursor)
1498 cursor.joinPreviousEditBlock()
1514 cursor.joinPreviousEditBlock()
1499 cursor.deletePreviousChar()
1515 cursor.deletePreviousChar()
1500
1516
1501 format = self._ansi_processor.get_format()
1517 format = self._ansi_processor.get_format()
1502 cursor.insertText(substring, format)
1518 cursor.insertText(substring, format)
1503 else:
1519 else:
1504 cursor.insertText(text)
1520 cursor.insertText(text)
1505 cursor.endEditBlock()
1521 cursor.endEditBlock()
1506
1522
1507 def _insert_plain_text_into_buffer(self, cursor, text):
1523 def _insert_plain_text_into_buffer(self, cursor, text):
1508 """ Inserts text into the input buffer using the specified cursor (which
1524 """ Inserts text into the input buffer using the specified cursor (which
1509 must be in the input buffer), ensuring that continuation prompts are
1525 must be in the input buffer), ensuring that continuation prompts are
1510 inserted as necessary.
1526 inserted as necessary.
1511 """
1527 """
1512 lines = text.splitlines(True)
1528 lines = text.splitlines(True)
1513 if lines:
1529 if lines:
1514 cursor.beginEditBlock()
1530 cursor.beginEditBlock()
1515 cursor.insertText(lines[0])
1531 cursor.insertText(lines[0])
1516 for line in lines[1:]:
1532 for line in lines[1:]:
1517 if self._continuation_prompt_html is None:
1533 if self._continuation_prompt_html is None:
1518 cursor.insertText(self._continuation_prompt)
1534 cursor.insertText(self._continuation_prompt)
1519 else:
1535 else:
1520 self._continuation_prompt = \
1536 self._continuation_prompt = \
1521 self._insert_html_fetching_plain_text(
1537 self._insert_html_fetching_plain_text(
1522 cursor, self._continuation_prompt_html)
1538 cursor, self._continuation_prompt_html)
1523 cursor.insertText(line)
1539 cursor.insertText(line)
1524 cursor.endEditBlock()
1540 cursor.endEditBlock()
1525
1541
1526 def _in_buffer(self, position=None):
1542 def _in_buffer(self, position=None):
1527 """ Returns whether the current cursor (or, if specified, a position) is
1543 """ Returns whether the current cursor (or, if specified, a position) is
1528 inside the editing region.
1544 inside the editing region.
1529 """
1545 """
1530 cursor = self._control.textCursor()
1546 cursor = self._control.textCursor()
1531 if position is None:
1547 if position is None:
1532 position = cursor.position()
1548 position = cursor.position()
1533 else:
1549 else:
1534 cursor.setPosition(position)
1550 cursor.setPosition(position)
1535 line = cursor.blockNumber()
1551 line = cursor.blockNumber()
1536 prompt_line = self._get_prompt_cursor().blockNumber()
1552 prompt_line = self._get_prompt_cursor().blockNumber()
1537 if line == prompt_line:
1553 if line == prompt_line:
1538 return position >= self._prompt_pos
1554 return position >= self._prompt_pos
1539 elif line > prompt_line:
1555 elif line > prompt_line:
1540 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1556 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1541 prompt_pos = cursor.position() + len(self._continuation_prompt)
1557 prompt_pos = cursor.position() + len(self._continuation_prompt)
1542 return position >= prompt_pos
1558 return position >= prompt_pos
1543 return False
1559 return False
1544
1560
1545 def _keep_cursor_in_buffer(self):
1561 def _keep_cursor_in_buffer(self):
1546 """ Ensures that the cursor is inside the editing region. Returns
1562 """ Ensures that the cursor is inside the editing region. Returns
1547 whether the cursor was moved.
1563 whether the cursor was moved.
1548 """
1564 """
1549 moved = not self._in_buffer()
1565 moved = not self._in_buffer()
1550 if moved:
1566 if moved:
1551 cursor = self._control.textCursor()
1567 cursor = self._control.textCursor()
1552 cursor.movePosition(QtGui.QTextCursor.End)
1568 cursor.movePosition(QtGui.QTextCursor.End)
1553 self._control.setTextCursor(cursor)
1569 self._control.setTextCursor(cursor)
1554 return moved
1570 return moved
1555
1571
1556 def _keyboard_quit(self):
1572 def _keyboard_quit(self):
1557 """ Cancels the current editing task ala Ctrl-G in Emacs.
1573 """ Cancels the current editing task ala Ctrl-G in Emacs.
1558 """
1574 """
1559 if self._text_completing_pos:
1575 if self._text_completing_pos:
1560 self._cancel_text_completion()
1576 self._cancel_text_completion()
1561 else:
1577 else:
1562 self.input_buffer = ''
1578 self.input_buffer = ''
1563
1579
1564 def _page(self, text, html=False):
1580 def _page(self, text, html=False):
1565 """ Displays text using the pager if it exceeds the height of the
1581 """ Displays text using the pager if it exceeds the height of the
1566 viewport.
1582 viewport.
1567
1583
1568 Parameters:
1584 Parameters:
1569 -----------
1585 -----------
1570 html : bool, optional (default False)
1586 html : bool, optional (default False)
1571 If set, the text will be interpreted as HTML instead of plain text.
1587 If set, the text will be interpreted as HTML instead of plain text.
1572 """
1588 """
1573 line_height = QtGui.QFontMetrics(self.font).height()
1589 line_height = QtGui.QFontMetrics(self.font).height()
1574 minlines = self._control.viewport().height() / line_height
1590 minlines = self._control.viewport().height() / line_height
1575 if self.paging != 'none' and \
1591 if self.paging != 'none' and \
1576 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1592 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1577 if self.paging == 'custom':
1593 if self.paging == 'custom':
1578 self.custom_page_requested.emit(text)
1594 self.custom_page_requested.emit(text)
1579 else:
1595 else:
1580 self._page_control.clear()
1596 self._page_control.clear()
1581 cursor = self._page_control.textCursor()
1597 cursor = self._page_control.textCursor()
1582 if html:
1598 if html:
1583 self._insert_html(cursor, text)
1599 self._insert_html(cursor, text)
1584 else:
1600 else:
1585 self._insert_plain_text(cursor, text)
1601 self._insert_plain_text(cursor, text)
1586 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1602 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1587
1603
1588 self._page_control.viewport().resize(self._control.size())
1604 self._page_control.viewport().resize(self._control.size())
1589 if self._splitter:
1605 if self._splitter:
1590 self._page_control.show()
1606 self._page_control.show()
1591 self._page_control.setFocus()
1607 self._page_control.setFocus()
1592 else:
1608 else:
1593 self.layout().setCurrentWidget(self._page_control)
1609 self.layout().setCurrentWidget(self._page_control)
1594 elif html:
1610 elif html:
1595 self._append_plain_html(text)
1611 self._append_plain_html(text)
1596 else:
1612 else:
1597 self._append_plain_text(text)
1613 self._append_plain_text(text)
1598
1614
1599 def _prompt_finished(self):
1615 def _prompt_finished(self):
1600 """ Called immediately after a prompt is finished, i.e. when some input
1616 """ Called immediately after a prompt is finished, i.e. when some input
1601 will be processed and a new prompt displayed.
1617 will be processed and a new prompt displayed.
1602 """
1618 """
1603 self._control.setReadOnly(True)
1619 self._control.setReadOnly(True)
1604 self._prompt_finished_hook()
1620 self._prompt_finished_hook()
1605
1621
1606 def _prompt_started(self):
1622 def _prompt_started(self):
1607 """ Called immediately after a new prompt is displayed.
1623 """ Called immediately after a new prompt is displayed.
1608 """
1624 """
1609 # Temporarily disable the maximum block count to permit undo/redo and
1625 # Temporarily disable the maximum block count to permit undo/redo and
1610 # to ensure that the prompt position does not change due to truncation.
1626 # to ensure that the prompt position does not change due to truncation.
1611 self._control.document().setMaximumBlockCount(0)
1627 self._control.document().setMaximumBlockCount(0)
1612 self._control.setUndoRedoEnabled(True)
1628 self._control.setUndoRedoEnabled(True)
1613
1629
1614 # Work around bug in QPlainTextEdit: input method is not re-enabled
1630 # Work around bug in QPlainTextEdit: input method is not re-enabled
1615 # when read-only is disabled.
1631 # when read-only is disabled.
1616 self._control.setReadOnly(False)
1632 self._control.setReadOnly(False)
1617 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1633 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1618
1634
1619 self._executing = False
1635 if not self._reading:
1636 self._executing = False
1620 self._prompt_started_hook()
1637 self._prompt_started_hook()
1621
1638
1622 # If the input buffer has changed while executing, load it.
1639 # If the input buffer has changed while executing, load it.
1623 if self._input_buffer_pending:
1640 if self._input_buffer_pending:
1624 self.input_buffer = self._input_buffer_pending
1641 self.input_buffer = self._input_buffer_pending
1625 self._input_buffer_pending = ''
1642 self._input_buffer_pending = ''
1626
1643
1627 self._control.moveCursor(QtGui.QTextCursor.End)
1644 self._control.moveCursor(QtGui.QTextCursor.End)
1628
1645
1629 def _readline(self, prompt='', callback=None):
1646 def _readline(self, prompt='', callback=None):
1630 """ Reads one line of input from the user.
1647 """ Reads one line of input from the user.
1631
1648
1632 Parameters
1649 Parameters
1633 ----------
1650 ----------
1634 prompt : str, optional
1651 prompt : str, optional
1635 The prompt to print before reading the line.
1652 The prompt to print before reading the line.
1636
1653
1637 callback : callable, optional
1654 callback : callable, optional
1638 A callback to execute with the read line. If not specified, input is
1655 A callback to execute with the read line. If not specified, input is
1639 read *synchronously* and this method does not return until it has
1656 read *synchronously* and this method does not return until it has
1640 been read.
1657 been read.
1641
1658
1642 Returns
1659 Returns
1643 -------
1660 -------
1644 If a callback is specified, returns nothing. Otherwise, returns the
1661 If a callback is specified, returns nothing. Otherwise, returns the
1645 input string with the trailing newline stripped.
1662 input string with the trailing newline stripped.
1646 """
1663 """
1647 if self._reading:
1664 if self._reading:
1648 raise RuntimeError('Cannot read a line. Widget is already reading.')
1665 raise RuntimeError('Cannot read a line. Widget is already reading.')
1649
1666
1650 if not callback and not self.isVisible():
1667 if not callback and not self.isVisible():
1651 # If the user cannot see the widget, this function cannot return.
1668 # If the user cannot see the widget, this function cannot return.
1652 raise RuntimeError('Cannot synchronously read a line if the widget '
1669 raise RuntimeError('Cannot synchronously read a line if the widget '
1653 'is not visible!')
1670 'is not visible!')
1654
1671
1655 self._reading = True
1672 self._reading = True
1656 self._show_prompt(prompt, newline=False)
1673 self._show_prompt(prompt, newline=False)
1657
1674
1658 if callback is None:
1675 if callback is None:
1659 self._reading_callback = None
1676 self._reading_callback = None
1660 while self._reading:
1677 while self._reading:
1661 QtCore.QCoreApplication.processEvents()
1678 QtCore.QCoreApplication.processEvents()
1662 return self.input_buffer.rstrip('\n')
1679 return self._get_input_buffer(force=True).rstrip('\n')
1663
1680
1664 else:
1681 else:
1665 self._reading_callback = lambda: \
1682 self._reading_callback = lambda: \
1666 callback(self.input_buffer.rstrip('\n'))
1683 callback(self._get_input_buffer(force=True).rstrip('\n'))
1667
1684
1668 def _set_continuation_prompt(self, prompt, html=False):
1685 def _set_continuation_prompt(self, prompt, html=False):
1669 """ Sets the continuation prompt.
1686 """ Sets the continuation prompt.
1670
1687
1671 Parameters
1688 Parameters
1672 ----------
1689 ----------
1673 prompt : str
1690 prompt : str
1674 The prompt to show when more input is needed.
1691 The prompt to show when more input is needed.
1675
1692
1676 html : bool, optional (default False)
1693 html : bool, optional (default False)
1677 If set, the prompt will be inserted as formatted HTML. Otherwise,
1694 If set, the prompt will be inserted as formatted HTML. Otherwise,
1678 the prompt will be treated as plain text, though ANSI color codes
1695 the prompt will be treated as plain text, though ANSI color codes
1679 will be handled.
1696 will be handled.
1680 """
1697 """
1681 if html:
1698 if html:
1682 self._continuation_prompt_html = prompt
1699 self._continuation_prompt_html = prompt
1683 else:
1700 else:
1684 self._continuation_prompt = prompt
1701 self._continuation_prompt = prompt
1685 self._continuation_prompt_html = None
1702 self._continuation_prompt_html = None
1686
1703
1687 def _set_cursor(self, cursor):
1704 def _set_cursor(self, cursor):
1688 """ Convenience method to set the current cursor.
1705 """ Convenience method to set the current cursor.
1689 """
1706 """
1690 self._control.setTextCursor(cursor)
1707 self._control.setTextCursor(cursor)
1691
1708
1692 def _set_top_cursor(self, cursor):
1709 def _set_top_cursor(self, cursor):
1693 """ Scrolls the viewport so that the specified cursor is at the top.
1710 """ Scrolls the viewport so that the specified cursor is at the top.
1694 """
1711 """
1695 scrollbar = self._control.verticalScrollBar()
1712 scrollbar = self._control.verticalScrollBar()
1696 scrollbar.setValue(scrollbar.maximum())
1713 scrollbar.setValue(scrollbar.maximum())
1697 original_cursor = self._control.textCursor()
1714 original_cursor = self._control.textCursor()
1698 self._control.setTextCursor(cursor)
1715 self._control.setTextCursor(cursor)
1699 self._control.ensureCursorVisible()
1716 self._control.ensureCursorVisible()
1700 self._control.setTextCursor(original_cursor)
1717 self._control.setTextCursor(original_cursor)
1701
1718
1702 def _show_prompt(self, prompt=None, html=False, newline=True):
1719 def _show_prompt(self, prompt=None, html=False, newline=True):
1703 """ Writes a new prompt at the end of the buffer.
1720 """ Writes a new prompt at the end of the buffer.
1704
1721
1705 Parameters
1722 Parameters
1706 ----------
1723 ----------
1707 prompt : str, optional
1724 prompt : str, optional
1708 The prompt to show. If not specified, the previous prompt is used.
1725 The prompt to show. If not specified, the previous prompt is used.
1709
1726
1710 html : bool, optional (default False)
1727 html : bool, optional (default False)
1711 Only relevant when a prompt is specified. If set, the prompt will
1728 Only relevant when a prompt is specified. If set, the prompt will
1712 be inserted as formatted HTML. Otherwise, the prompt will be treated
1729 be inserted as formatted HTML. Otherwise, the prompt will be treated
1713 as plain text, though ANSI color codes will be handled.
1730 as plain text, though ANSI color codes will be handled.
1714
1731
1715 newline : bool, optional (default True)
1732 newline : bool, optional (default True)
1716 If set, a new line will be written before showing the prompt if
1733 If set, a new line will be written before showing the prompt if
1717 there is not already a newline at the end of the buffer.
1734 there is not already a newline at the end of the buffer.
1718 """
1735 """
1736 # Save the current end position to support _append*(before_prompt=True).
1737 cursor = self._get_end_cursor()
1738 self._append_before_prompt_pos = cursor.position()
1739
1719 # Insert a preliminary newline, if necessary.
1740 # Insert a preliminary newline, if necessary.
1720 if newline:
1741 if newline and cursor.position() > 0:
1721 cursor = self._get_end_cursor()
1742 cursor.movePosition(QtGui.QTextCursor.Left,
1722 if cursor.position() > 0:
1743 QtGui.QTextCursor.KeepAnchor)
1723 cursor.movePosition(QtGui.QTextCursor.Left,
1744 if cursor.selection().toPlainText() != '\n':
1724 QtGui.QTextCursor.KeepAnchor)
1745 self._append_plain_text('\n')
1725 if cursor.selection().toPlainText() != '\n':
1726 self._append_plain_text('\n')
1727
1746
1728 # Write the prompt.
1747 # Write the prompt.
1729 self._append_plain_text(self._prompt_sep)
1748 self._append_plain_text(self._prompt_sep)
1730 if prompt is None:
1749 if prompt is None:
1731 if self._prompt_html is None:
1750 if self._prompt_html is None:
1732 self._append_plain_text(self._prompt)
1751 self._append_plain_text(self._prompt)
1733 else:
1752 else:
1734 self._append_html(self._prompt_html)
1753 self._append_html(self._prompt_html)
1735 else:
1754 else:
1736 if html:
1755 if html:
1737 self._prompt = self._append_html_fetching_plain_text(prompt)
1756 self._prompt = self._append_html_fetching_plain_text(prompt)
1738 self._prompt_html = prompt
1757 self._prompt_html = prompt
1739 else:
1758 else:
1740 self._append_plain_text(prompt)
1759 self._append_plain_text(prompt)
1741 self._prompt = prompt
1760 self._prompt = prompt
1742 self._prompt_html = None
1761 self._prompt_html = None
1743
1762
1744 self._prompt_pos = self._get_end_cursor().position()
1763 self._prompt_pos = self._get_end_cursor().position()
1745 self._prompt_started()
1764 self._prompt_started()
1746
1765
1747 #------ Signal handlers ----------------------------------------------------
1766 #------ Signal handlers ----------------------------------------------------
1748
1767
1749 def _adjust_scrollbars(self):
1768 def _adjust_scrollbars(self):
1750 """ Expands the vertical scrollbar beyond the range set by Qt.
1769 """ Expands the vertical scrollbar beyond the range set by Qt.
1751 """
1770 """
1752 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1771 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1753 # and qtextedit.cpp.
1772 # and qtextedit.cpp.
1754 document = self._control.document()
1773 document = self._control.document()
1755 scrollbar = self._control.verticalScrollBar()
1774 scrollbar = self._control.verticalScrollBar()
1756 viewport_height = self._control.viewport().height()
1775 viewport_height = self._control.viewport().height()
1757 if isinstance(self._control, QtGui.QPlainTextEdit):
1776 if isinstance(self._control, QtGui.QPlainTextEdit):
1758 maximum = max(0, document.lineCount() - 1)
1777 maximum = max(0, document.lineCount() - 1)
1759 step = viewport_height / self._control.fontMetrics().lineSpacing()
1778 step = viewport_height / self._control.fontMetrics().lineSpacing()
1760 else:
1779 else:
1761 # QTextEdit does not do line-based layout and blocks will not in
1780 # QTextEdit does not do line-based layout and blocks will not in
1762 # general have the same height. Therefore it does not make sense to
1781 # general have the same height. Therefore it does not make sense to
1763 # attempt to scroll in line height increments.
1782 # attempt to scroll in line height increments.
1764 maximum = document.size().height()
1783 maximum = document.size().height()
1765 step = viewport_height
1784 step = viewport_height
1766 diff = maximum - scrollbar.maximum()
1785 diff = maximum - scrollbar.maximum()
1767 scrollbar.setRange(0, maximum)
1786 scrollbar.setRange(0, maximum)
1768 scrollbar.setPageStep(step)
1787 scrollbar.setPageStep(step)
1769
1788
1770 # Compensate for undesirable scrolling that occurs automatically due to
1789 # Compensate for undesirable scrolling that occurs automatically due to
1771 # maximumBlockCount() text truncation.
1790 # maximumBlockCount() text truncation.
1772 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1791 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1773 scrollbar.setValue(scrollbar.value() + diff)
1792 scrollbar.setValue(scrollbar.value() + diff)
1774
1793
1775 def _cursor_position_changed(self):
1794 def _cursor_position_changed(self):
1776 """ Clears the temporary buffer based on the cursor position.
1795 """ Clears the temporary buffer based on the cursor position.
1777 """
1796 """
1778 if self._text_completing_pos:
1797 if self._text_completing_pos:
1779 document = self._control.document()
1798 document = self._control.document()
1780 if self._text_completing_pos < document.characterCount():
1799 if self._text_completing_pos < document.characterCount():
1781 cursor = self._control.textCursor()
1800 cursor = self._control.textCursor()
1782 pos = cursor.position()
1801 pos = cursor.position()
1783 text_cursor = self._control.textCursor()
1802 text_cursor = self._control.textCursor()
1784 text_cursor.setPosition(self._text_completing_pos)
1803 text_cursor.setPosition(self._text_completing_pos)
1785 if pos < self._text_completing_pos or \
1804 if pos < self._text_completing_pos or \
1786 cursor.blockNumber() > text_cursor.blockNumber():
1805 cursor.blockNumber() > text_cursor.blockNumber():
1787 self._clear_temporary_buffer()
1806 self._clear_temporary_buffer()
1788 self._text_completing_pos = 0
1807 self._text_completing_pos = 0
1789 else:
1808 else:
1790 self._clear_temporary_buffer()
1809 self._clear_temporary_buffer()
1791 self._text_completing_pos = 0
1810 self._text_completing_pos = 0
1792
1811
1793 def _custom_context_menu_requested(self, pos):
1812 def _custom_context_menu_requested(self, pos):
1794 """ Shows a context menu at the given QPoint (in widget coordinates).
1813 """ Shows a context menu at the given QPoint (in widget coordinates).
1795 """
1814 """
1796 menu = self._context_menu_make(pos)
1815 menu = self._context_menu_make(pos)
1797 menu.exec_(self._control.mapToGlobal(pos))
1816 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,626 +1,617 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external.qt import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11
11
12 # Local imports
12 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
14 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool, Instance
16 from IPython.utils.traitlets import Bool, Instance
17 from bracket_matcher import BracketMatcher
17 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
19 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
20 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
21 from pygments_highlighter import PygmentsHighlighter
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
25 """ A PygmentsHighlighter that understands and ignores prompts.
26 prompts.
27 """
26 """
28
27
29 def __init__(self, frontend):
28 def __init__(self, frontend):
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 self._current_offset = 0
30 self._current_offset = 0
32 self._frontend = frontend
31 self._frontend = frontend
33 self.highlighting_on = False
32 self.highlighting_on = False
34
33
35 def highlightBlock(self, string):
34 def highlightBlock(self, string):
36 """ Highlight a block of text. Reimplemented to highlight selectively.
35 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """
36 """
38 if not self.highlighting_on:
37 if not self.highlighting_on:
39 return
38 return
40
39
41 # The input to this function is a unicode string that may contain
40 # The input to this function is a unicode string that may contain
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # the string as plain text so we can compare it.
42 # the string as plain text so we can compare it.
44 current_block = self.currentBlock()
43 current_block = self.currentBlock()
45 string = self._frontend._get_block_plain_text(current_block)
44 string = self._frontend._get_block_plain_text(current_block)
46
45
47 # Decide whether to check for the regular or continuation prompt.
46 # Decide whether to check for the regular or continuation prompt.
48 if current_block.contains(self._frontend._prompt_pos):
47 if current_block.contains(self._frontend._prompt_pos):
49 prompt = self._frontend._prompt
48 prompt = self._frontend._prompt
50 else:
49 else:
51 prompt = self._frontend._continuation_prompt
50 prompt = self._frontend._continuation_prompt
52
51
53 # Don't highlight the part of the string that contains the prompt.
52 # Only highlight if we can identify a prompt, but make sure not to
53 # highlight the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 string = string[len(prompt):]
56 string = string[len(prompt):]
57 else:
57 super(FrontendHighlighter, self).highlightBlock(string)
58 self._current_offset = 0
59
60 PygmentsHighlighter.highlightBlock(self, string)
61
58
62 def rehighlightBlock(self, block):
59 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
60 """ Reimplemented to temporarily enable highlighting if disabled.
64 """
61 """
65 old = self.highlighting_on
62 old = self.highlighting_on
66 self.highlighting_on = True
63 self.highlighting_on = True
67 super(FrontendHighlighter, self).rehighlightBlock(block)
64 super(FrontendHighlighter, self).rehighlightBlock(block)
68 self.highlighting_on = old
65 self.highlighting_on = old
69
66
70 def setFormat(self, start, count, format):
67 def setFormat(self, start, count, format):
71 """ Reimplemented to highlight selectively.
68 """ Reimplemented to highlight selectively.
72 """
69 """
73 start += self._current_offset
70 start += self._current_offset
74 PygmentsHighlighter.setFormat(self, start, count, format)
71 super(FrontendHighlighter, self).setFormat(start, count, format)
75
72
76
73
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 """ A Qt frontend for a generic Python kernel.
75 """ A Qt frontend for a generic Python kernel.
79 """
76 """
80
77
81 # An option and corresponding signal for overriding the default kernel
78 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
79 # interrupt behavior.
83 custom_interrupt = Bool(False)
80 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.Signal()
81 custom_interrupt_requested = QtCore.Signal()
85
82
86 # An option and corresponding signals for overriding the default kernel
83 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
84 # restart behavior.
88 custom_restart = Bool(False)
85 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.Signal(float)
86 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
87 custom_restart_requested = QtCore.Signal()
91
88
92 # Emitted when a user visible 'execute_request' has been submitted to the
89 # Emitted when a user visible 'execute_request' has been submitted to the
93 # kernel from the FrontendWidget. Contains the code to be executed.
90 # kernel from the FrontendWidget. Contains the code to be executed.
94 executing = QtCore.Signal(object)
91 executing = QtCore.Signal(object)
95
92
96 # Emitted when a user-visible 'execute_reply' has been received from the
93 # Emitted when a user-visible 'execute_reply' has been received from the
97 # kernel and processed by the FrontendWidget. Contains the response message.
94 # kernel and processed by the FrontendWidget. Contains the response message.
98 executed = QtCore.Signal(object)
95 executed = QtCore.Signal(object)
99
96
100 # Emitted when an exit request has been received from the kernel.
97 # Emitted when an exit request has been received from the kernel.
101 exit_requested = QtCore.Signal()
98 exit_requested = QtCore.Signal()
102
99
103 # Protected class variables.
100 # Protected class variables.
104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
107 _input_splitter_class = InputSplitter
104 _input_splitter_class = InputSplitter
108 _local_kernel = False
105 _local_kernel = False
109 _highlighter = Instance(FrontendHighlighter)
106 _highlighter = Instance(FrontendHighlighter)
110
107
111 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
112 # 'object' interface
109 # 'object' interface
113 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
114
111
115 def __init__(self, *args, **kw):
112 def __init__(self, *args, **kw):
116 super(FrontendWidget, self).__init__(*args, **kw)
113 super(FrontendWidget, self).__init__(*args, **kw)
117
114
118 # FrontendWidget protected variables.
115 # FrontendWidget protected variables.
119 self._bracket_matcher = BracketMatcher(self._control)
116 self._bracket_matcher = BracketMatcher(self._control)
120 self._call_tip_widget = CallTipWidget(self._control)
117 self._call_tip_widget = CallTipWidget(self._control)
121 self._completion_lexer = CompletionLexer(PythonLexer())
118 self._completion_lexer = CompletionLexer(PythonLexer())
122 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
119 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
123 self._hidden = False
120 self._hidden = False
124 self._highlighter = FrontendHighlighter(self)
121 self._highlighter = FrontendHighlighter(self)
125 self._input_splitter = self._input_splitter_class(input_mode='cell')
122 self._input_splitter = self._input_splitter_class(input_mode='cell')
126 self._kernel_manager = None
123 self._kernel_manager = None
127 self._request_info = {}
124 self._request_info = {}
128
125
129 # Configure the ConsoleWidget.
126 # Configure the ConsoleWidget.
130 self.tab_width = 4
127 self.tab_width = 4
131 self._set_continuation_prompt('... ')
128 self._set_continuation_prompt('... ')
132
129
133 # Configure the CallTipWidget.
130 # Configure the CallTipWidget.
134 self._call_tip_widget.setFont(self.font)
131 self._call_tip_widget.setFont(self.font)
135 self.font_changed.connect(self._call_tip_widget.setFont)
132 self.font_changed.connect(self._call_tip_widget.setFont)
136
133
137 # Configure actions.
134 # Configure actions.
138 action = self._copy_raw_action
135 action = self._copy_raw_action
139 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
136 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
140 action.setEnabled(False)
137 action.setEnabled(False)
141 action.setShortcut(QtGui.QKeySequence(key))
138 action.setShortcut(QtGui.QKeySequence(key))
142 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
139 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
143 action.triggered.connect(self.copy_raw)
140 action.triggered.connect(self.copy_raw)
144 self.copy_available.connect(action.setEnabled)
141 self.copy_available.connect(action.setEnabled)
145 self.addAction(action)
142 self.addAction(action)
146
143
147 # Connect signal handlers.
144 # Connect signal handlers.
148 document = self._control.document()
145 document = self._control.document()
149 document.contentsChange.connect(self._document_contents_change)
146 document.contentsChange.connect(self._document_contents_change)
150
147
151 # Set flag for whether we are connected via localhost.
148 # Set flag for whether we are connected via localhost.
152 self._local_kernel = kw.get('local_kernel',
149 self._local_kernel = kw.get('local_kernel',
153 FrontendWidget._local_kernel)
150 FrontendWidget._local_kernel)
154
151
155 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
156 # 'ConsoleWidget' public interface
153 # 'ConsoleWidget' public interface
157 #---------------------------------------------------------------------------
154 #---------------------------------------------------------------------------
158
155
159 def copy(self):
156 def copy(self):
160 """ Copy the currently selected text to the clipboard, removing prompts.
157 """ Copy the currently selected text to the clipboard, removing prompts.
161 """
158 """
162 text = self._control.textCursor().selection().toPlainText()
159 text = self._control.textCursor().selection().toPlainText()
163 if text:
160 if text:
164 lines = map(transform_classic_prompt, text.splitlines())
161 lines = map(transform_classic_prompt, text.splitlines())
165 text = '\n'.join(lines)
162 text = '\n'.join(lines)
166 QtGui.QApplication.clipboard().setText(text)
163 QtGui.QApplication.clipboard().setText(text)
167
164
168 #---------------------------------------------------------------------------
165 #---------------------------------------------------------------------------
169 # 'ConsoleWidget' abstract interface
166 # 'ConsoleWidget' abstract interface
170 #---------------------------------------------------------------------------
167 #---------------------------------------------------------------------------
171
168
172 def _is_complete(self, source, interactive):
169 def _is_complete(self, source, interactive):
173 """ Returns whether 'source' can be completely processed and a new
170 """ Returns whether 'source' can be completely processed and a new
174 prompt created. When triggered by an Enter/Return key press,
171 prompt created. When triggered by an Enter/Return key press,
175 'interactive' is True; otherwise, it is False.
172 'interactive' is True; otherwise, it is False.
176 """
173 """
177 complete = self._input_splitter.push(source)
174 complete = self._input_splitter.push(source)
178 if interactive:
175 if interactive:
179 complete = not self._input_splitter.push_accepts_more()
176 complete = not self._input_splitter.push_accepts_more()
180 return complete
177 return complete
181
178
182 def _execute(self, source, hidden):
179 def _execute(self, source, hidden):
183 """ Execute 'source'. If 'hidden', do not show any output.
180 """ Execute 'source'. If 'hidden', do not show any output.
184
181
185 See parent class :meth:`execute` docstring for full details.
182 See parent class :meth:`execute` docstring for full details.
186 """
183 """
187 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
184 msg_id = self.kernel_manager.shell_channel.execute(source, hidden)
188 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
185 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
189 self._hidden = hidden
186 self._hidden = hidden
190 if not hidden:
187 if not hidden:
191 self.executing.emit(source)
188 self.executing.emit(source)
192
189
193 def _prompt_started_hook(self):
190 def _prompt_started_hook(self):
194 """ Called immediately after a new prompt is displayed.
191 """ Called immediately after a new prompt is displayed.
195 """
192 """
196 if not self._reading:
193 if not self._reading:
197 self._highlighter.highlighting_on = True
194 self._highlighter.highlighting_on = True
198
195
199 def _prompt_finished_hook(self):
196 def _prompt_finished_hook(self):
200 """ Called immediately after a prompt is finished, i.e. when some input
197 """ Called immediately after a prompt is finished, i.e. when some input
201 will be processed and a new prompt displayed.
198 will be processed and a new prompt displayed.
202 """
199 """
203 # Flush all state from the input splitter so the next round of
200 # Flush all state from the input splitter so the next round of
204 # reading input starts with a clean buffer.
201 # reading input starts with a clean buffer.
205 self._input_splitter.reset()
202 self._input_splitter.reset()
206
203
207 if not self._reading:
204 if not self._reading:
208 self._highlighter.highlighting_on = False
205 self._highlighter.highlighting_on = False
209
206
210 def _tab_pressed(self):
207 def _tab_pressed(self):
211 """ Called when the tab key is pressed. Returns whether to continue
208 """ Called when the tab key is pressed. Returns whether to continue
212 processing the event.
209 processing the event.
213 """
210 """
214 # Perform tab completion if:
211 # Perform tab completion if:
215 # 1) The cursor is in the input buffer.
212 # 1) The cursor is in the input buffer.
216 # 2) There is a non-whitespace character before the cursor.
213 # 2) There is a non-whitespace character before the cursor.
217 text = self._get_input_buffer_cursor_line()
214 text = self._get_input_buffer_cursor_line()
218 if text is None:
215 if text is None:
219 return False
216 return False
220 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
217 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
221 if complete:
218 if complete:
222 self._complete()
219 self._complete()
223 return not complete
220 return not complete
224
221
225 #---------------------------------------------------------------------------
222 #---------------------------------------------------------------------------
226 # 'ConsoleWidget' protected interface
223 # 'ConsoleWidget' protected interface
227 #---------------------------------------------------------------------------
224 #---------------------------------------------------------------------------
228
225
229 def _context_menu_make(self, pos):
226 def _context_menu_make(self, pos):
230 """ Reimplemented to add an action for raw copy.
227 """ Reimplemented to add an action for raw copy.
231 """
228 """
232 menu = super(FrontendWidget, self)._context_menu_make(pos)
229 menu = super(FrontendWidget, self)._context_menu_make(pos)
233 for before_action in menu.actions():
230 for before_action in menu.actions():
234 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
231 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
235 QtGui.QKeySequence.ExactMatch:
232 QtGui.QKeySequence.ExactMatch:
236 menu.insertAction(before_action, self._copy_raw_action)
233 menu.insertAction(before_action, self._copy_raw_action)
237 break
234 break
238 return menu
235 return menu
239
236
240 def _event_filter_console_keypress(self, event):
237 def _event_filter_console_keypress(self, event):
241 """ Reimplemented for execution interruption and smart backspace.
238 """ Reimplemented for execution interruption and smart backspace.
242 """
239 """
243 key = event.key()
240 key = event.key()
244 if self._control_key_down(event.modifiers(), include_command=False):
241 if self._control_key_down(event.modifiers(), include_command=False):
245
242
246 if key == QtCore.Qt.Key_C and self._executing:
243 if key == QtCore.Qt.Key_C and self._executing:
247 self.interrupt_kernel()
244 self.interrupt_kernel()
248 return True
245 return True
249
246
250 elif key == QtCore.Qt.Key_Period:
247 elif key == QtCore.Qt.Key_Period:
251 message = 'Are you sure you want to restart the kernel?'
248 message = 'Are you sure you want to restart the kernel?'
252 self.restart_kernel(message, now=False)
249 self.restart_kernel(message, now=False)
253 return True
250 return True
254
251
255 elif not event.modifiers() & QtCore.Qt.AltModifier:
252 elif not event.modifiers() & QtCore.Qt.AltModifier:
256
253
257 # Smart backspace: remove four characters in one backspace if:
254 # Smart backspace: remove four characters in one backspace if:
258 # 1) everything left of the cursor is whitespace
255 # 1) everything left of the cursor is whitespace
259 # 2) the four characters immediately left of the cursor are spaces
256 # 2) the four characters immediately left of the cursor are spaces
260 if key == QtCore.Qt.Key_Backspace:
257 if key == QtCore.Qt.Key_Backspace:
261 col = self._get_input_buffer_cursor_column()
258 col = self._get_input_buffer_cursor_column()
262 cursor = self._control.textCursor()
259 cursor = self._control.textCursor()
263 if col > 3 and not cursor.hasSelection():
260 if col > 3 and not cursor.hasSelection():
264 text = self._get_input_buffer_cursor_line()[:col]
261 text = self._get_input_buffer_cursor_line()[:col]
265 if text.endswith(' ') and not text.strip():
262 if text.endswith(' ') and not text.strip():
266 cursor.movePosition(QtGui.QTextCursor.Left,
263 cursor.movePosition(QtGui.QTextCursor.Left,
267 QtGui.QTextCursor.KeepAnchor, 4)
264 QtGui.QTextCursor.KeepAnchor, 4)
268 cursor.removeSelectedText()
265 cursor.removeSelectedText()
269 return True
266 return True
270
267
271 return super(FrontendWidget, self)._event_filter_console_keypress(event)
268 return super(FrontendWidget, self)._event_filter_console_keypress(event)
272
269
273 def _insert_continuation_prompt(self, cursor):
270 def _insert_continuation_prompt(self, cursor):
274 """ Reimplemented for auto-indentation.
271 """ Reimplemented for auto-indentation.
275 """
272 """
276 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
273 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
277 cursor.insertText(' ' * self._input_splitter.indent_spaces)
274 cursor.insertText(' ' * self._input_splitter.indent_spaces)
278
275
279 #---------------------------------------------------------------------------
276 #---------------------------------------------------------------------------
280 # 'BaseFrontendMixin' abstract interface
277 # 'BaseFrontendMixin' abstract interface
281 #---------------------------------------------------------------------------
278 #---------------------------------------------------------------------------
282
279
283 def _handle_complete_reply(self, rep):
280 def _handle_complete_reply(self, rep):
284 """ Handle replies for tab completion.
281 """ Handle replies for tab completion.
285 """
282 """
286 cursor = self._get_cursor()
283 cursor = self._get_cursor()
287 info = self._request_info.get('complete')
284 info = self._request_info.get('complete')
288 if info and info.id == rep['parent_header']['msg_id'] and \
285 if info and info.id == rep['parent_header']['msg_id'] and \
289 info.pos == cursor.position():
286 info.pos == cursor.position():
290 text = '.'.join(self._get_context())
287 text = '.'.join(self._get_context())
291 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
288 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
292 self._complete_with_items(cursor, rep['content']['matches'])
289 self._complete_with_items(cursor, rep['content']['matches'])
293
290
294 def _handle_execute_reply(self, msg):
291 def _handle_execute_reply(self, msg):
295 """ Handles replies for code execution.
292 """ Handles replies for code execution.
296 """
293 """
297 info = self._request_info.get('execute')
294 info = self._request_info.get('execute')
298 if info and info.id == msg['parent_header']['msg_id'] and \
295 if info and info.id == msg['parent_header']['msg_id'] and \
299 info.kind == 'user' and not self._hidden:
296 info.kind == 'user' and not self._hidden:
300 # Make sure that all output from the SUB channel has been processed
297 # Make sure that all output from the SUB channel has been processed
301 # before writing a new prompt.
298 # before writing a new prompt.
302 self.kernel_manager.sub_channel.flush()
299 self.kernel_manager.sub_channel.flush()
303
300
304 # Reset the ANSI style information to prevent bad text in stdout
301 # Reset the ANSI style information to prevent bad text in stdout
305 # from messing up our colors. We're not a true terminal so we're
302 # from messing up our colors. We're not a true terminal so we're
306 # allowed to do this.
303 # allowed to do this.
307 if self.ansi_codes:
304 if self.ansi_codes:
308 self._ansi_processor.reset_sgr()
305 self._ansi_processor.reset_sgr()
309
306
310 content = msg['content']
307 content = msg['content']
311 status = content['status']
308 status = content['status']
312 if status == 'ok':
309 if status == 'ok':
313 self._process_execute_ok(msg)
310 self._process_execute_ok(msg)
314 elif status == 'error':
311 elif status == 'error':
315 self._process_execute_error(msg)
312 self._process_execute_error(msg)
316 elif status == 'abort':
313 elif status == 'abort':
317 self._process_execute_abort(msg)
314 self._process_execute_abort(msg)
318
315
319 self._show_interpreter_prompt_for_reply(msg)
316 self._show_interpreter_prompt_for_reply(msg)
320 self.executed.emit(msg)
317 self.executed.emit(msg)
321
318
322 def _handle_input_request(self, msg):
319 def _handle_input_request(self, msg):
323 """ Handle requests for raw_input.
320 """ Handle requests for raw_input.
324 """
321 """
325 if self._hidden:
322 if self._hidden:
326 raise RuntimeError('Request for raw input during hidden execution.')
323 raise RuntimeError('Request for raw input during hidden execution.')
327
324
328 # Make sure that all output from the SUB channel has been processed
325 # Make sure that all output from the SUB channel has been processed
329 # before entering readline mode.
326 # before entering readline mode.
330 self.kernel_manager.sub_channel.flush()
327 self.kernel_manager.sub_channel.flush()
331
328
332 def callback(line):
329 def callback(line):
333 self.kernel_manager.stdin_channel.input(line)
330 self.kernel_manager.stdin_channel.input(line)
334 self._readline(msg['content']['prompt'], callback=callback)
331 self._readline(msg['content']['prompt'], callback=callback)
335
332
336 def _handle_kernel_died(self, since_last_heartbeat):
333 def _handle_kernel_died(self, since_last_heartbeat):
337 """ Handle the kernel's death by asking if the user wants to restart.
334 """ Handle the kernel's death by asking if the user wants to restart.
338 """
335 """
339 if self.custom_restart:
336 if self.custom_restart:
340 self.custom_restart_kernel_died.emit(since_last_heartbeat)
337 self.custom_restart_kernel_died.emit(since_last_heartbeat)
341 else:
338 else:
342 message = 'The kernel heartbeat has been inactive for %.2f ' \
339 message = 'The kernel heartbeat has been inactive for %.2f ' \
343 'seconds. Do you want to restart the kernel? You may ' \
340 'seconds. Do you want to restart the kernel? You may ' \
344 'first want to check the network connection.' % \
341 'first want to check the network connection.' % \
345 since_last_heartbeat
342 since_last_heartbeat
346 self.restart_kernel(message, now=True)
343 self.restart_kernel(message, now=True)
347
344
348 def _handle_object_info_reply(self, rep):
345 def _handle_object_info_reply(self, rep):
349 """ Handle replies for call tips.
346 """ Handle replies for call tips.
350 """
347 """
351 cursor = self._get_cursor()
348 cursor = self._get_cursor()
352 info = self._request_info.get('call_tip')
349 info = self._request_info.get('call_tip')
353 if info and info.id == rep['parent_header']['msg_id'] and \
350 if info and info.id == rep['parent_header']['msg_id'] and \
354 info.pos == cursor.position():
351 info.pos == cursor.position():
355 # Get the information for a call tip. For now we format the call
352 # Get the information for a call tip. For now we format the call
356 # line as string, later we can pass False to format_call and
353 # line as string, later we can pass False to format_call and
357 # syntax-highlight it ourselves for nicer formatting in the
354 # syntax-highlight it ourselves for nicer formatting in the
358 # calltip.
355 # calltip.
359 content = rep['content']
356 content = rep['content']
360 # if this is from pykernel, 'docstring' will be the only key
357 # if this is from pykernel, 'docstring' will be the only key
361 if content.get('ismagic', False):
358 if content.get('ismagic', False):
362 # Don't generate a call-tip for magics. Ideally, we should
359 # Don't generate a call-tip for magics. Ideally, we should
363 # generate a tooltip, but not on ( like we do for actual
360 # generate a tooltip, but not on ( like we do for actual
364 # callables.
361 # callables.
365 call_info, doc = None, None
362 call_info, doc = None, None
366 else:
363 else:
367 call_info, doc = call_tip(content, format_call=True)
364 call_info, doc = call_tip(content, format_call=True)
368 if call_info or doc:
365 if call_info or doc:
369 self._call_tip_widget.show_call_info(call_info, doc)
366 self._call_tip_widget.show_call_info(call_info, doc)
370
367
371 def _handle_pyout(self, msg):
368 def _handle_pyout(self, msg):
372 """ Handle display hook output.
369 """ Handle display hook output.
373 """
370 """
374 if not self._hidden and self._is_from_this_session(msg):
371 if not self._hidden and self._is_from_this_session(msg):
375 data = msg['content']['data']
372 text = msg['content']['data']
376 if isinstance(data, basestring):
373 self._append_plain_text(text + '\n', before_prompt=True)
377 # plaintext data from pure Python kernel
378 text = data
379 else:
380 # formatted output from DisplayFormatter (IPython kernel)
381 text = data.get('text/plain', '')
382 self._append_plain_text(text + '\n')
383
374
384 def _handle_stream(self, msg):
375 def _handle_stream(self, msg):
385 """ Handle stdout, stderr, and stdin.
376 """ Handle stdout, stderr, and stdin.
386 """
377 """
387 if not self._hidden and self._is_from_this_session(msg):
378 if not self._hidden and self._is_from_this_session(msg):
388 # Most consoles treat tabs as being 8 space characters. Convert tabs
379 # Most consoles treat tabs as being 8 space characters. Convert tabs
389 # to spaces so that output looks as expected regardless of this
380 # to spaces so that output looks as expected regardless of this
390 # widget's tab width.
381 # widget's tab width.
391 text = msg['content']['data'].expandtabs(8)
382 text = msg['content']['data'].expandtabs(8)
392
383
393 self._append_plain_text(text)
384 self._append_plain_text(text, before_prompt=True)
394 self._control.moveCursor(QtGui.QTextCursor.End)
385 self._control.moveCursor(QtGui.QTextCursor.End)
395
386
396 def _handle_shutdown_reply(self, msg):
387 def _handle_shutdown_reply(self, msg):
397 """ Handle shutdown signal, only if from other console.
388 """ Handle shutdown signal, only if from other console.
398 """
389 """
399 if not self._hidden and not self._is_from_this_session(msg):
390 if not self._hidden and not self._is_from_this_session(msg):
400 if self._local_kernel:
391 if self._local_kernel:
401 if not msg['content']['restart']:
392 if not msg['content']['restart']:
402 sys.exit(0)
393 sys.exit(0)
403 else:
394 else:
404 # we just got notified of a restart!
395 # we just got notified of a restart!
405 time.sleep(0.25) # wait 1/4 sec to reset
396 time.sleep(0.25) # wait 1/4 sec to reset
406 # lest the request for a new prompt
397 # lest the request for a new prompt
407 # goes to the old kernel
398 # goes to the old kernel
408 self.reset()
399 self.reset()
409 else: # remote kernel, prompt on Kernel shutdown/reset
400 else: # remote kernel, prompt on Kernel shutdown/reset
410 title = self.window().windowTitle()
401 title = self.window().windowTitle()
411 if not msg['content']['restart']:
402 if not msg['content']['restart']:
412 reply = QtGui.QMessageBox.question(self, title,
403 reply = QtGui.QMessageBox.question(self, title,
413 "Kernel has been shutdown permanently. "
404 "Kernel has been shutdown permanently. "
414 "Close the Console?",
405 "Close the Console?",
415 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
406 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
416 if reply == QtGui.QMessageBox.Yes:
407 if reply == QtGui.QMessageBox.Yes:
417 sys.exit(0)
408 sys.exit(0)
418 else:
409 else:
419 reply = QtGui.QMessageBox.question(self, title,
410 reply = QtGui.QMessageBox.question(self, title,
420 "Kernel has been reset. Clear the Console?",
411 "Kernel has been reset. Clear the Console?",
421 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
412 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
422 if reply == QtGui.QMessageBox.Yes:
413 if reply == QtGui.QMessageBox.Yes:
423 time.sleep(0.25) # wait 1/4 sec to reset
414 time.sleep(0.25) # wait 1/4 sec to reset
424 # lest the request for a new prompt
415 # lest the request for a new prompt
425 # goes to the old kernel
416 # goes to the old kernel
426 self.reset()
417 self.reset()
427
418
428 def _started_channels(self):
419 def _started_channels(self):
429 """ Called when the KernelManager channels have started listening or
420 """ Called when the KernelManager channels have started listening or
430 when the frontend is assigned an already listening KernelManager.
421 when the frontend is assigned an already listening KernelManager.
431 """
422 """
432 self.reset()
423 self.reset()
433
424
434 #---------------------------------------------------------------------------
425 #---------------------------------------------------------------------------
435 # 'FrontendWidget' public interface
426 # 'FrontendWidget' public interface
436 #---------------------------------------------------------------------------
427 #---------------------------------------------------------------------------
437
428
438 def copy_raw(self):
429 def copy_raw(self):
439 """ Copy the currently selected text to the clipboard without attempting
430 """ Copy the currently selected text to the clipboard without attempting
440 to remove prompts or otherwise alter the text.
431 to remove prompts or otherwise alter the text.
441 """
432 """
442 self._control.copy()
433 self._control.copy()
443
434
444 def execute_file(self, path, hidden=False):
435 def execute_file(self, path, hidden=False):
445 """ Attempts to execute file with 'path'. If 'hidden', no output is
436 """ Attempts to execute file with 'path'. If 'hidden', no output is
446 shown.
437 shown.
447 """
438 """
448 self.execute('execfile(%r)' % path, hidden=hidden)
439 self.execute('execfile(%r)' % path, hidden=hidden)
449
440
450 def interrupt_kernel(self):
441 def interrupt_kernel(self):
451 """ Attempts to interrupt the running kernel.
442 """ Attempts to interrupt the running kernel.
452 """
443 """
453 if self.custom_interrupt:
444 if self.custom_interrupt:
454 self.custom_interrupt_requested.emit()
445 self.custom_interrupt_requested.emit()
455 elif self.kernel_manager.has_kernel:
446 elif self.kernel_manager.has_kernel:
456 self.kernel_manager.interrupt_kernel()
447 self.kernel_manager.interrupt_kernel()
457 else:
448 else:
458 self._append_plain_text('Kernel process is either remote or '
449 self._append_plain_text('Kernel process is either remote or '
459 'unspecified. Cannot interrupt.\n')
450 'unspecified. Cannot interrupt.\n')
460
451
461 def reset(self):
452 def reset(self):
462 """ Resets the widget to its initial state. Similar to ``clear``, but
453 """ Resets the widget to its initial state. Similar to ``clear``, but
463 also re-writes the banner and aborts execution if necessary.
454 also re-writes the banner and aborts execution if necessary.
464 """
455 """
465 if self._executing:
456 if self._executing:
466 self._executing = False
457 self._executing = False
467 self._request_info['execute'] = None
458 self._request_info['execute'] = None
468 self._reading = False
459 self._reading = False
469 self._highlighter.highlighting_on = False
460 self._highlighter.highlighting_on = False
470
461
471 self._control.clear()
462 self._control.clear()
472 self._append_plain_text(self._get_banner())
463 self._append_plain_text(self._get_banner())
473 self._show_interpreter_prompt()
464 self._show_interpreter_prompt()
474
465
475 def restart_kernel(self, message, now=False):
466 def restart_kernel(self, message, now=False):
476 """ Attempts to restart the running kernel.
467 """ Attempts to restart the running kernel.
477 """
468 """
478 # FIXME: now should be configurable via a checkbox in the dialog. Right
469 # FIXME: now should be configurable via a checkbox in the dialog. Right
479 # now at least the heartbeat path sets it to True and the manual restart
470 # now at least the heartbeat path sets it to True and the manual restart
480 # to False. But those should just be the pre-selected states of a
471 # to False. But those should just be the pre-selected states of a
481 # checkbox that the user could override if so desired. But I don't know
472 # checkbox that the user could override if so desired. But I don't know
482 # enough Qt to go implementing the checkbox now.
473 # enough Qt to go implementing the checkbox now.
483
474
484 if self.custom_restart:
475 if self.custom_restart:
485 self.custom_restart_requested.emit()
476 self.custom_restart_requested.emit()
486
477
487 elif self.kernel_manager.has_kernel:
478 elif self.kernel_manager.has_kernel:
488 # Pause the heart beat channel to prevent further warnings.
479 # Pause the heart beat channel to prevent further warnings.
489 self.kernel_manager.hb_channel.pause()
480 self.kernel_manager.hb_channel.pause()
490
481
491 # Prompt the user to restart the kernel. Un-pause the heartbeat if
482 # Prompt the user to restart the kernel. Un-pause the heartbeat if
492 # they decline. (If they accept, the heartbeat will be un-paused
483 # they decline. (If they accept, the heartbeat will be un-paused
493 # automatically when the kernel is restarted.)
484 # automatically when the kernel is restarted.)
494 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
485 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
495 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
486 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
496 message, buttons)
487 message, buttons)
497 if result == QtGui.QMessageBox.Yes:
488 if result == QtGui.QMessageBox.Yes:
498 try:
489 try:
499 self.kernel_manager.restart_kernel(now=now)
490 self.kernel_manager.restart_kernel(now=now)
500 except RuntimeError:
491 except RuntimeError:
501 self._append_plain_text('Kernel started externally. '
492 self._append_plain_text('Kernel started externally. '
502 'Cannot restart.\n')
493 'Cannot restart.\n')
503 else:
494 else:
504 self.reset()
495 self.reset()
505 else:
496 else:
506 self.kernel_manager.hb_channel.unpause()
497 self.kernel_manager.hb_channel.unpause()
507
498
508 else:
499 else:
509 self._append_plain_text('Kernel process is either remote or '
500 self._append_plain_text('Kernel process is either remote or '
510 'unspecified. Cannot restart.\n')
501 'unspecified. Cannot restart.\n')
511
502
512 #---------------------------------------------------------------------------
503 #---------------------------------------------------------------------------
513 # 'FrontendWidget' protected interface
504 # 'FrontendWidget' protected interface
514 #---------------------------------------------------------------------------
505 #---------------------------------------------------------------------------
515
506
516 def _call_tip(self):
507 def _call_tip(self):
517 """ Shows a call tip, if appropriate, at the current cursor location.
508 """ Shows a call tip, if appropriate, at the current cursor location.
518 """
509 """
519 # Decide if it makes sense to show a call tip
510 # Decide if it makes sense to show a call tip
520 cursor = self._get_cursor()
511 cursor = self._get_cursor()
521 cursor.movePosition(QtGui.QTextCursor.Left)
512 cursor.movePosition(QtGui.QTextCursor.Left)
522 if cursor.document().characterAt(cursor.position()) != '(':
513 if cursor.document().characterAt(cursor.position()) != '(':
523 return False
514 return False
524 context = self._get_context(cursor)
515 context = self._get_context(cursor)
525 if not context:
516 if not context:
526 return False
517 return False
527
518
528 # Send the metadata request to the kernel
519 # Send the metadata request to the kernel
529 name = '.'.join(context)
520 name = '.'.join(context)
530 msg_id = self.kernel_manager.shell_channel.object_info(name)
521 msg_id = self.kernel_manager.shell_channel.object_info(name)
531 pos = self._get_cursor().position()
522 pos = self._get_cursor().position()
532 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
523 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
533 return True
524 return True
534
525
535 def _complete(self):
526 def _complete(self):
536 """ Performs completion at the current cursor location.
527 """ Performs completion at the current cursor location.
537 """
528 """
538 context = self._get_context()
529 context = self._get_context()
539 if context:
530 if context:
540 # Send the completion request to the kernel
531 # Send the completion request to the kernel
541 msg_id = self.kernel_manager.shell_channel.complete(
532 msg_id = self.kernel_manager.shell_channel.complete(
542 '.'.join(context), # text
533 '.'.join(context), # text
543 self._get_input_buffer_cursor_line(), # line
534 self._get_input_buffer_cursor_line(), # line
544 self._get_input_buffer_cursor_column(), # cursor_pos
535 self._get_input_buffer_cursor_column(), # cursor_pos
545 self.input_buffer) # block
536 self.input_buffer) # block
546 pos = self._get_cursor().position()
537 pos = self._get_cursor().position()
547 info = self._CompletionRequest(msg_id, pos)
538 info = self._CompletionRequest(msg_id, pos)
548 self._request_info['complete'] = info
539 self._request_info['complete'] = info
549
540
550 def _get_banner(self):
541 def _get_banner(self):
551 """ Gets a banner to display at the beginning of a session.
542 """ Gets a banner to display at the beginning of a session.
552 """
543 """
553 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
544 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
554 '"license" for more information.'
545 '"license" for more information.'
555 return banner % (sys.version, sys.platform)
546 return banner % (sys.version, sys.platform)
556
547
557 def _get_context(self, cursor=None):
548 def _get_context(self, cursor=None):
558 """ Gets the context for the specified cursor (or the current cursor
549 """ Gets the context for the specified cursor (or the current cursor
559 if none is specified).
550 if none is specified).
560 """
551 """
561 if cursor is None:
552 if cursor is None:
562 cursor = self._get_cursor()
553 cursor = self._get_cursor()
563 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
554 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
564 QtGui.QTextCursor.KeepAnchor)
555 QtGui.QTextCursor.KeepAnchor)
565 text = cursor.selection().toPlainText()
556 text = cursor.selection().toPlainText()
566 return self._completion_lexer.get_context(text)
557 return self._completion_lexer.get_context(text)
567
558
568 def _process_execute_abort(self, msg):
559 def _process_execute_abort(self, msg):
569 """ Process a reply for an aborted execution request.
560 """ Process a reply for an aborted execution request.
570 """
561 """
571 self._append_plain_text("ERROR: execution aborted\n")
562 self._append_plain_text("ERROR: execution aborted\n")
572
563
573 def _process_execute_error(self, msg):
564 def _process_execute_error(self, msg):
574 """ Process a reply for an execution request that resulted in an error.
565 """ Process a reply for an execution request that resulted in an error.
575 """
566 """
576 content = msg['content']
567 content = msg['content']
577 # If a SystemExit is passed along, this means exit() was called - also
568 # If a SystemExit is passed along, this means exit() was called - also
578 # all the ipython %exit magic syntax of '-k' to be used to keep
569 # all the ipython %exit magic syntax of '-k' to be used to keep
579 # the kernel running
570 # the kernel running
580 if content['ename']=='SystemExit':
571 if content['ename']=='SystemExit':
581 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
572 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
582 self._keep_kernel_on_exit = keepkernel
573 self._keep_kernel_on_exit = keepkernel
583 self.exit_requested.emit()
574 self.exit_requested.emit()
584 else:
575 else:
585 traceback = ''.join(content['traceback'])
576 traceback = ''.join(content['traceback'])
586 self._append_plain_text(traceback)
577 self._append_plain_text(traceback)
587
578
588 def _process_execute_ok(self, msg):
579 def _process_execute_ok(self, msg):
589 """ Process a reply for a successful execution equest.
580 """ Process a reply for a successful execution equest.
590 """
581 """
591 payload = msg['content']['payload']
582 payload = msg['content']['payload']
592 for item in payload:
583 for item in payload:
593 if not self._process_execute_payload(item):
584 if not self._process_execute_payload(item):
594 warning = 'Warning: received unknown payload of type %s'
585 warning = 'Warning: received unknown payload of type %s'
595 print(warning % repr(item['source']))
586 print(warning % repr(item['source']))
596
587
597 def _process_execute_payload(self, item):
588 def _process_execute_payload(self, item):
598 """ Process a single payload item from the list of payload items in an
589 """ Process a single payload item from the list of payload items in an
599 execution reply. Returns whether the payload was handled.
590 execution reply. Returns whether the payload was handled.
600 """
591 """
601 # The basic FrontendWidget doesn't handle payloads, as they are a
592 # The basic FrontendWidget doesn't handle payloads, as they are a
602 # mechanism for going beyond the standard Python interpreter model.
593 # mechanism for going beyond the standard Python interpreter model.
603 return False
594 return False
604
595
605 def _show_interpreter_prompt(self):
596 def _show_interpreter_prompt(self):
606 """ Shows a prompt for the interpreter.
597 """ Shows a prompt for the interpreter.
607 """
598 """
608 self._show_prompt('>>> ')
599 self._show_prompt('>>> ')
609
600
610 def _show_interpreter_prompt_for_reply(self, msg):
601 def _show_interpreter_prompt_for_reply(self, msg):
611 """ Shows a prompt for the interpreter given an 'execute_reply' message.
602 """ Shows a prompt for the interpreter given an 'execute_reply' message.
612 """
603 """
613 self._show_interpreter_prompt()
604 self._show_interpreter_prompt()
614
605
615 #------ Signal handlers ----------------------------------------------------
606 #------ Signal handlers ----------------------------------------------------
616
607
617 def _document_contents_change(self, position, removed, added):
608 def _document_contents_change(self, position, removed, added):
618 """ Called whenever the document's content changes. Display a call tip
609 """ Called whenever the document's content changes. Display a call tip
619 if appropriate.
610 if appropriate.
620 """
611 """
621 # Calculate where the cursor should be *after* the change:
612 # Calculate where the cursor should be *after* the change:
622 position += added
613 position += added
623
614
624 document = self._control.document()
615 document = self._control.document()
625 if position == self._get_cursor().position():
616 if position == self._get_cursor().position():
626 self._call_tip()
617 self._call_tip()
@@ -1,510 +1,511 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import os.path
12 import re
12 import re
13 from subprocess import Popen
13 from subprocess import Popen
14 import sys
14 import sys
15 from textwrap import dedent
15 from textwrap import dedent
16
16
17 # System library imports
17 # System library imports
18 from IPython.external.qt import QtCore, QtGui
18 from IPython.external.qt import QtCore, QtGui
19
19
20 # Local imports
20 # Local imports
21 from IPython.core.inputsplitter import IPythonInputSplitter, \
21 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 transform_ipy_prompt
22 transform_ipy_prompt
23 from IPython.core.usage import default_gui_banner
23 from IPython.core.usage import default_gui_banner
24 from IPython.utils.traitlets import Bool, Str, Unicode
24 from IPython.utils.traitlets import Bool, Str, Unicode
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26 import styles
26 import styles
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Constants
29 # Constants
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
33 # in between)
33 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
36 default_input_sep = '\n'
37 default_output_sep = ''
37 default_output_sep = ''
38 default_output_sep2 = ''
38 default_output_sep2 = ''
39
39
40 # Base path for most payload sources.
40 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42
42
43 if sys.platform.startswith('win'):
43 if sys.platform.startswith('win'):
44 default_editor = 'notepad'
44 default_editor = 'notepad'
45 else:
45 else:
46 default_editor = ''
46 default_editor = ''
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # IPythonWidget class
49 # IPythonWidget class
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class IPythonWidget(FrontendWidget):
52 class IPythonWidget(FrontendWidget):
53 """ A FrontendWidget for an IPython kernel.
53 """ A FrontendWidget for an IPython kernel.
54 """
54 """
55
55
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # settings.
58 # settings.
59 custom_edit = Bool(False)
59 custom_edit = Bool(False)
60 custom_edit_requested = QtCore.Signal(object, object)
60 custom_edit_requested = QtCore.Signal(object, object)
61
61
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename will
65 {filename} format specifier, it will be used. Otherwise, the filename
66 be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit magic
73 this parameter is not specified, the line number option to the %edit
74 will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
78 help="""
78 help="""
79 A CSS stylesheet. The stylesheet can contain classes for:
79 A CSS stylesheet. The stylesheet can contain classes for:
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 3. IPython: .error, .in-prompt, .out-prompt, etc
82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 """)
83 """)
84
84
85
85
86 syntax_style = Str(config=True,
86 syntax_style = Str(config=True,
87 help="""
87 help="""
88 If not empty, use this Pygments style for syntax highlighting. Otherwise,
88 If not empty, use this Pygments style for syntax highlighting.
89 the style sheet is queried for Pygments style information.
89 Otherwise, the style sheet is queried for Pygments style
90 information.
90 """)
91 """)
91
92
92 # Prompts.
93 # Prompts.
93 in_prompt = Str(default_in_prompt, config=True)
94 in_prompt = Str(default_in_prompt, config=True)
94 out_prompt = Str(default_out_prompt, config=True)
95 out_prompt = Str(default_out_prompt, config=True)
95 input_sep = Str(default_input_sep, config=True)
96 input_sep = Str(default_input_sep, config=True)
96 output_sep = Str(default_output_sep, config=True)
97 output_sep = Str(default_output_sep, config=True)
97 output_sep2 = Str(default_output_sep2, config=True)
98 output_sep2 = Str(default_output_sep2, config=True)
98
99
99 # FrontendWidget protected class variables.
100 # FrontendWidget protected class variables.
100 _input_splitter_class = IPythonInputSplitter
101 _input_splitter_class = IPythonInputSplitter
101
102
102 # IPythonWidget protected class variables.
103 # IPythonWidget protected class variables.
103 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_page = 'IPython.zmq.page.page'
108 _payload_source_page = 'IPython.zmq.page.page'
108
109
109 #---------------------------------------------------------------------------
110 #---------------------------------------------------------------------------
110 # 'object' interface
111 # 'object' interface
111 #---------------------------------------------------------------------------
112 #---------------------------------------------------------------------------
112
113
113 def __init__(self, *args, **kw):
114 def __init__(self, *args, **kw):
114 super(IPythonWidget, self).__init__(*args, **kw)
115 super(IPythonWidget, self).__init__(*args, **kw)
115
116
116 # IPythonWidget protected variables.
117 # IPythonWidget protected variables.
117 self._payload_handlers = {
118 self._payload_handlers = {
118 self._payload_source_edit : self._handle_payload_edit,
119 self._payload_source_edit : self._handle_payload_edit,
119 self._payload_source_exit : self._handle_payload_exit,
120 self._payload_source_exit : self._handle_payload_exit,
120 self._payload_source_page : self._handle_payload_page,
121 self._payload_source_page : self._handle_payload_page,
121 self._payload_source_next_input : self._handle_payload_next_input }
122 self._payload_source_next_input : self._handle_payload_next_input }
122 self._previous_prompt_obj = None
123 self._previous_prompt_obj = None
123 self._keep_kernel_on_exit = None
124 self._keep_kernel_on_exit = None
124
125
125 # Initialize widget styling.
126 # Initialize widget styling.
126 if self.style_sheet:
127 if self.style_sheet:
127 self._style_sheet_changed()
128 self._style_sheet_changed()
128 self._syntax_style_changed()
129 self._syntax_style_changed()
129 else:
130 else:
130 self.set_default_style()
131 self.set_default_style()
131
132
132 #---------------------------------------------------------------------------
133 #---------------------------------------------------------------------------
133 # 'BaseFrontendMixin' abstract interface
134 # 'BaseFrontendMixin' abstract interface
134 #---------------------------------------------------------------------------
135 #---------------------------------------------------------------------------
135
136
136 def _handle_complete_reply(self, rep):
137 def _handle_complete_reply(self, rep):
137 """ Reimplemented to support IPython's improved completion machinery.
138 """ Reimplemented to support IPython's improved completion machinery.
138 """
139 """
139 cursor = self._get_cursor()
140 cursor = self._get_cursor()
140 info = self._request_info.get('complete')
141 info = self._request_info.get('complete')
141 if info and info.id == rep['parent_header']['msg_id'] and \
142 if info and info.id == rep['parent_header']['msg_id'] and \
142 info.pos == cursor.position():
143 info.pos == cursor.position():
143 matches = rep['content']['matches']
144 matches = rep['content']['matches']
144 text = rep['content']['matched_text']
145 text = rep['content']['matched_text']
145 offset = len(text)
146 offset = len(text)
146
147
147 # Clean up matches with period and path separators if the matched
148 # Clean up matches with period and path separators if the matched
148 # text has not been transformed. This is done by truncating all
149 # text has not been transformed. This is done by truncating all
149 # but the last component and then suitably decreasing the offset
150 # but the last component and then suitably decreasing the offset
150 # between the current cursor position and the start of completion.
151 # between the current cursor position and the start of completion.
151 if len(matches) > 1 and matches[0][:offset] == text:
152 if len(matches) > 1 and matches[0][:offset] == text:
152 parts = re.split(r'[./\\]', text)
153 parts = re.split(r'[./\\]', text)
153 sep_count = len(parts) - 1
154 sep_count = len(parts) - 1
154 if sep_count:
155 if sep_count:
155 chop_length = sum(map(len, parts[:sep_count])) + sep_count
156 chop_length = sum(map(len, parts[:sep_count])) + sep_count
156 matches = [ match[chop_length:] for match in matches ]
157 matches = [ match[chop_length:] for match in matches ]
157 offset -= chop_length
158 offset -= chop_length
158
159
159 # Move the cursor to the start of the match and complete.
160 # Move the cursor to the start of the match and complete.
160 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
161 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
161 self._complete_with_items(cursor, matches)
162 self._complete_with_items(cursor, matches)
162
163
163 def _handle_execute_reply(self, msg):
164 def _handle_execute_reply(self, msg):
164 """ Reimplemented to support prompt requests.
165 """ Reimplemented to support prompt requests.
165 """
166 """
166 info = self._request_info.get('execute')
167 info = self._request_info.get('execute')
167 if info and info.id == msg['parent_header']['msg_id']:
168 if info and info.id == msg['parent_header']['msg_id']:
168 if info.kind == 'prompt':
169 if info.kind == 'prompt':
169 number = msg['content']['execution_count'] + 1
170 number = msg['content']['execution_count'] + 1
170 self._show_interpreter_prompt(number)
171 self._show_interpreter_prompt(number)
171 else:
172 else:
172 super(IPythonWidget, self)._handle_execute_reply(msg)
173 super(IPythonWidget, self)._handle_execute_reply(msg)
173
174
174 def _handle_history_reply(self, msg):
175 def _handle_history_reply(self, msg):
175 """ Implemented to handle history tail replies, which are only supported
176 """ Implemented to handle history tail replies, which are only supported
176 by the IPython kernel.
177 by the IPython kernel.
177 """
178 """
178 history_items = msg['content']['history']
179 history_items = msg['content']['history']
179 items = [ line.rstrip() for _, _, line in history_items ]
180 items = [ line.rstrip() for _, _, line in history_items ]
180 self._set_history(items)
181 self._set_history(items)
181
182
182 def _handle_pyout(self, msg):
183 def _handle_pyout(self, msg):
183 """ Reimplemented for IPython-style "display hook".
184 """ Reimplemented for IPython-style "display hook".
184 """
185 """
185 if not self._hidden and self._is_from_this_session(msg):
186 if not self._hidden and self._is_from_this_session(msg):
186 content = msg['content']
187 content = msg['content']
187 prompt_number = content['execution_count']
188 prompt_number = content['execution_count']
188 data = content['data']
189 data = content['data']
189 if data.has_key('text/html'):
190 if data.has_key('text/html'):
190 self._append_plain_text(self.output_sep)
191 self._append_plain_text(self.output_sep, True)
191 self._append_html(self._make_out_prompt(prompt_number))
192 self._append_html(self._make_out_prompt(prompt_number), True)
192 html = data['text/html']
193 html = data['text/html']
193 self._append_plain_text('\n')
194 self._append_plain_text('\n', True)
194 self._append_html(html + self.output_sep2)
195 self._append_html(html + self.output_sep2, True)
195 elif data.has_key('text/plain'):
196 elif data.has_key('text/plain'):
196 self._append_plain_text(self.output_sep)
197 self._append_plain_text(self.output_sep, True)
197 self._append_html(self._make_out_prompt(prompt_number))
198 self._append_html(self._make_out_prompt(prompt_number), True)
198 text = data['text/plain']
199 text = data['text/plain']
199 # If the repr is multiline, make sure we start on a new line,
200 # If the repr is multiline, make sure we start on a new line,
200 # so that its lines are aligned.
201 # so that its lines are aligned.
201 if "\n" in text and not self.output_sep.endswith("\n"):
202 if "\n" in text and not self.output_sep.endswith("\n"):
202 self._append_plain_text('\n')
203 self._append_plain_text('\n', True)
203 self._append_plain_text(text + self.output_sep2)
204 self._append_plain_text(text + self.output_sep2, True)
204
205
205 def _handle_display_data(self, msg):
206 def _handle_display_data(self, msg):
206 """ The base handler for the ``display_data`` message.
207 """ The base handler for the ``display_data`` message.
207 """
208 """
208 # For now, we don't display data from other frontends, but we
209 # For now, we don't display data from other frontends, but we
209 # eventually will as this allows all frontends to monitor the display
210 # eventually will as this allows all frontends to monitor the display
210 # data. But we need to figure out how to handle this in the GUI.
211 # data. But we need to figure out how to handle this in the GUI.
211 if not self._hidden and self._is_from_this_session(msg):
212 if not self._hidden and self._is_from_this_session(msg):
212 source = msg['content']['source']
213 source = msg['content']['source']
213 data = msg['content']['data']
214 data = msg['content']['data']
214 metadata = msg['content']['metadata']
215 metadata = msg['content']['metadata']
215 # In the regular IPythonWidget, we simply print the plain text
216 # In the regular IPythonWidget, we simply print the plain text
216 # representation.
217 # representation.
217 if data.has_key('text/html'):
218 if data.has_key('text/html'):
218 html = data['text/html']
219 html = data['text/html']
219 self._append_html(html)
220 self._append_html(html, True)
220 elif data.has_key('text/plain'):
221 elif data.has_key('text/plain'):
221 text = data['text/plain']
222 text = data['text/plain']
222 self._append_plain_text(text)
223 self._append_plain_text(text, True)
223 # This newline seems to be needed for text and html output.
224 # This newline seems to be needed for text and html output.
224 self._append_plain_text(u'\n')
225 self._append_plain_text(u'\n', True)
225
226
226 def _started_channels(self):
227 def _started_channels(self):
227 """ Reimplemented to make a history request.
228 """ Reimplemented to make a history request.
228 """
229 """
229 super(IPythonWidget, self)._started_channels()
230 super(IPythonWidget, self)._started_channels()
230 self.kernel_manager.shell_channel.history(hist_access_type='tail', n=1000)
231 self.kernel_manager.shell_channel.history(hist_access_type='tail',
231
232 n=1000)
232 #---------------------------------------------------------------------------
233 #---------------------------------------------------------------------------
233 # 'ConsoleWidget' public interface
234 # 'ConsoleWidget' public interface
234 #---------------------------------------------------------------------------
235 #---------------------------------------------------------------------------
235
236
236 def copy(self):
237 def copy(self):
237 """ Copy the currently selected text to the clipboard, removing prompts
238 """ Copy the currently selected text to the clipboard, removing prompts
238 if possible.
239 if possible.
239 """
240 """
240 text = self._control.textCursor().selection().toPlainText()
241 text = self._control.textCursor().selection().toPlainText()
241 if text:
242 if text:
242 lines = map(transform_ipy_prompt, text.splitlines())
243 lines = map(transform_ipy_prompt, text.splitlines())
243 text = '\n'.join(lines)
244 text = '\n'.join(lines)
244 QtGui.QApplication.clipboard().setText(text)
245 QtGui.QApplication.clipboard().setText(text)
245
246
246 #---------------------------------------------------------------------------
247 #---------------------------------------------------------------------------
247 # 'FrontendWidget' public interface
248 # 'FrontendWidget' public interface
248 #---------------------------------------------------------------------------
249 #---------------------------------------------------------------------------
249
250
250 def execute_file(self, path, hidden=False):
251 def execute_file(self, path, hidden=False):
251 """ Reimplemented to use the 'run' magic.
252 """ Reimplemented to use the 'run' magic.
252 """
253 """
253 # Use forward slashes on Windows to avoid escaping each separator.
254 # Use forward slashes on Windows to avoid escaping each separator.
254 if sys.platform == 'win32':
255 if sys.platform == 'win32':
255 path = os.path.normpath(path).replace('\\', '/')
256 path = os.path.normpath(path).replace('\\', '/')
256
257
257 self.execute('%%run %s' % path, hidden=hidden)
258 self.execute('%%run %s' % path, hidden=hidden)
258
259
259 #---------------------------------------------------------------------------
260 #---------------------------------------------------------------------------
260 # 'FrontendWidget' protected interface
261 # 'FrontendWidget' protected interface
261 #---------------------------------------------------------------------------
262 #---------------------------------------------------------------------------
262
263
263 def _complete(self):
264 def _complete(self):
264 """ Reimplemented to support IPython's improved completion machinery.
265 """ Reimplemented to support IPython's improved completion machinery.
265 """
266 """
266 # We let the kernel split the input line, so we *always* send an empty
267 # We let the kernel split the input line, so we *always* send an empty
267 # text field. Readline-based frontends do get a real text field which
268 # text field. Readline-based frontends do get a real text field which
268 # they can use.
269 # they can use.
269 text = ''
270 text = ''
270
271
271 # Send the completion request to the kernel
272 # Send the completion request to the kernel
272 msg_id = self.kernel_manager.shell_channel.complete(
273 msg_id = self.kernel_manager.shell_channel.complete(
273 text, # text
274 text, # text
274 self._get_input_buffer_cursor_line(), # line
275 self._get_input_buffer_cursor_line(), # line
275 self._get_input_buffer_cursor_column(), # cursor_pos
276 self._get_input_buffer_cursor_column(), # cursor_pos
276 self.input_buffer) # block
277 self.input_buffer) # block
277 pos = self._get_cursor().position()
278 pos = self._get_cursor().position()
278 info = self._CompletionRequest(msg_id, pos)
279 info = self._CompletionRequest(msg_id, pos)
279 self._request_info['complete'] = info
280 self._request_info['complete'] = info
280
281
281 def _get_banner(self):
282 def _get_banner(self):
282 """ Reimplemented to return IPython's default banner.
283 """ Reimplemented to return IPython's default banner.
283 """
284 """
284 return default_gui_banner
285 return default_gui_banner
285
286
286 def _process_execute_error(self, msg):
287 def _process_execute_error(self, msg):
287 """ Reimplemented for IPython-style traceback formatting.
288 """ Reimplemented for IPython-style traceback formatting.
288 """
289 """
289 content = msg['content']
290 content = msg['content']
290 traceback = '\n'.join(content['traceback']) + '\n'
291 traceback = '\n'.join(content['traceback']) + '\n'
291 if False:
292 if False:
292 # FIXME: For now, tracebacks come as plain text, so we can't use
293 # FIXME: For now, tracebacks come as plain text, so we can't use
293 # the html renderer yet. Once we refactor ultratb to produce
294 # the html renderer yet. Once we refactor ultratb to produce
294 # properly styled tracebacks, this branch should be the default
295 # properly styled tracebacks, this branch should be the default
295 traceback = traceback.replace(' ', '&nbsp;')
296 traceback = traceback.replace(' ', '&nbsp;')
296 traceback = traceback.replace('\n', '<br/>')
297 traceback = traceback.replace('\n', '<br/>')
297
298
298 ename = content['ename']
299 ename = content['ename']
299 ename_styled = '<span class="error">%s</span>' % ename
300 ename_styled = '<span class="error">%s</span>' % ename
300 traceback = traceback.replace(ename, ename_styled)
301 traceback = traceback.replace(ename, ename_styled)
301
302
302 self._append_html(traceback)
303 self._append_html(traceback)
303 else:
304 else:
304 # This is the fallback for now, using plain text with ansi escapes
305 # This is the fallback for now, using plain text with ansi escapes
305 self._append_plain_text(traceback)
306 self._append_plain_text(traceback)
306
307
307 def _process_execute_payload(self, item):
308 def _process_execute_payload(self, item):
308 """ Reimplemented to dispatch payloads to handler methods.
309 """ Reimplemented to dispatch payloads to handler methods.
309 """
310 """
310 handler = self._payload_handlers.get(item['source'])
311 handler = self._payload_handlers.get(item['source'])
311 if handler is None:
312 if handler is None:
312 # We have no handler for this type of payload, simply ignore it
313 # We have no handler for this type of payload, simply ignore it
313 return False
314 return False
314 else:
315 else:
315 handler(item)
316 handler(item)
316 return True
317 return True
317
318
318 def _show_interpreter_prompt(self, number=None):
319 def _show_interpreter_prompt(self, number=None):
319 """ Reimplemented for IPython-style prompts.
320 """ Reimplemented for IPython-style prompts.
320 """
321 """
321 # If a number was not specified, make a prompt number request.
322 # If a number was not specified, make a prompt number request.
322 if number is None:
323 if number is None:
323 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
324 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
324 info = self._ExecutionRequest(msg_id, 'prompt')
325 info = self._ExecutionRequest(msg_id, 'prompt')
325 self._request_info['execute'] = info
326 self._request_info['execute'] = info
326 return
327 return
327
328
328 # Show a new prompt and save information about it so that it can be
329 # Show a new prompt and save information about it so that it can be
329 # updated later if the prompt number turns out to be wrong.
330 # updated later if the prompt number turns out to be wrong.
330 self._prompt_sep = self.input_sep
331 self._prompt_sep = self.input_sep
331 self._show_prompt(self._make_in_prompt(number), html=True)
332 self._show_prompt(self._make_in_prompt(number), html=True)
332 block = self._control.document().lastBlock()
333 block = self._control.document().lastBlock()
333 length = len(self._prompt)
334 length = len(self._prompt)
334 self._previous_prompt_obj = self._PromptBlock(block, length, number)
335 self._previous_prompt_obj = self._PromptBlock(block, length, number)
335
336
336 # Update continuation prompt to reflect (possibly) new prompt length.
337 # Update continuation prompt to reflect (possibly) new prompt length.
337 self._set_continuation_prompt(
338 self._set_continuation_prompt(
338 self._make_continuation_prompt(self._prompt), html=True)
339 self._make_continuation_prompt(self._prompt), html=True)
339
340
340 def _show_interpreter_prompt_for_reply(self, msg):
341 def _show_interpreter_prompt_for_reply(self, msg):
341 """ Reimplemented for IPython-style prompts.
342 """ Reimplemented for IPython-style prompts.
342 """
343 """
343 # Update the old prompt number if necessary.
344 # Update the old prompt number if necessary.
344 content = msg['content']
345 content = msg['content']
345 previous_prompt_number = content['execution_count']
346 previous_prompt_number = content['execution_count']
346 if self._previous_prompt_obj and \
347 if self._previous_prompt_obj and \
347 self._previous_prompt_obj.number != previous_prompt_number:
348 self._previous_prompt_obj.number != previous_prompt_number:
348 block = self._previous_prompt_obj.block
349 block = self._previous_prompt_obj.block
349
350
350 # Make sure the prompt block has not been erased.
351 # Make sure the prompt block has not been erased.
351 if block.isValid() and block.text():
352 if block.isValid() and block.text():
352
353
353 # Remove the old prompt and insert a new prompt.
354 # Remove the old prompt and insert a new prompt.
354 cursor = QtGui.QTextCursor(block)
355 cursor = QtGui.QTextCursor(block)
355 cursor.movePosition(QtGui.QTextCursor.Right,
356 cursor.movePosition(QtGui.QTextCursor.Right,
356 QtGui.QTextCursor.KeepAnchor,
357 QtGui.QTextCursor.KeepAnchor,
357 self._previous_prompt_obj.length)
358 self._previous_prompt_obj.length)
358 prompt = self._make_in_prompt(previous_prompt_number)
359 prompt = self._make_in_prompt(previous_prompt_number)
359 self._prompt = self._insert_html_fetching_plain_text(
360 self._prompt = self._insert_html_fetching_plain_text(
360 cursor, prompt)
361 cursor, prompt)
361
362
362 # When the HTML is inserted, Qt blows away the syntax
363 # When the HTML is inserted, Qt blows away the syntax
363 # highlighting for the line, so we need to rehighlight it.
364 # highlighting for the line, so we need to rehighlight it.
364 self._highlighter.rehighlightBlock(cursor.block())
365 self._highlighter.rehighlightBlock(cursor.block())
365
366
366 self._previous_prompt_obj = None
367 self._previous_prompt_obj = None
367
368
368 # Show a new prompt with the kernel's estimated prompt number.
369 # Show a new prompt with the kernel's estimated prompt number.
369 self._show_interpreter_prompt(previous_prompt_number + 1)
370 self._show_interpreter_prompt(previous_prompt_number + 1)
370
371
371 #---------------------------------------------------------------------------
372 #---------------------------------------------------------------------------
372 # 'IPythonWidget' interface
373 # 'IPythonWidget' interface
373 #---------------------------------------------------------------------------
374 #---------------------------------------------------------------------------
374
375
375 def set_default_style(self, colors='lightbg'):
376 def set_default_style(self, colors='lightbg'):
376 """ Sets the widget style to the class defaults.
377 """ Sets the widget style to the class defaults.
377
378
378 Parameters:
379 Parameters:
379 -----------
380 -----------
380 colors : str, optional (default lightbg)
381 colors : str, optional (default lightbg)
381 Whether to use the default IPython light background or dark
382 Whether to use the default IPython light background or dark
382 background or B&W style.
383 background or B&W style.
383 """
384 """
384 colors = colors.lower()
385 colors = colors.lower()
385 if colors=='lightbg':
386 if colors=='lightbg':
386 self.style_sheet = styles.default_light_style_sheet
387 self.style_sheet = styles.default_light_style_sheet
387 self.syntax_style = styles.default_light_syntax_style
388 self.syntax_style = styles.default_light_syntax_style
388 elif colors=='linux':
389 elif colors=='linux':
389 self.style_sheet = styles.default_dark_style_sheet
390 self.style_sheet = styles.default_dark_style_sheet
390 self.syntax_style = styles.default_dark_syntax_style
391 self.syntax_style = styles.default_dark_syntax_style
391 elif colors=='nocolor':
392 elif colors=='nocolor':
392 self.style_sheet = styles.default_bw_style_sheet
393 self.style_sheet = styles.default_bw_style_sheet
393 self.syntax_style = styles.default_bw_syntax_style
394 self.syntax_style = styles.default_bw_syntax_style
394 else:
395 else:
395 raise KeyError("No such color scheme: %s"%colors)
396 raise KeyError("No such color scheme: %s"%colors)
396
397
397 #---------------------------------------------------------------------------
398 #---------------------------------------------------------------------------
398 # 'IPythonWidget' protected interface
399 # 'IPythonWidget' protected interface
399 #---------------------------------------------------------------------------
400 #---------------------------------------------------------------------------
400
401
401 def _edit(self, filename, line=None):
402 def _edit(self, filename, line=None):
402 """ Opens a Python script for editing.
403 """ Opens a Python script for editing.
403
404
404 Parameters:
405 Parameters:
405 -----------
406 -----------
406 filename : str
407 filename : str
407 A path to a local system file.
408 A path to a local system file.
408
409
409 line : int, optional
410 line : int, optional
410 A line of interest in the file.
411 A line of interest in the file.
411 """
412 """
412 if self.custom_edit:
413 if self.custom_edit:
413 self.custom_edit_requested.emit(filename, line)
414 self.custom_edit_requested.emit(filename, line)
414 elif not self.editor:
415 elif not self.editor:
415 self._append_plain_text('No default editor available.\n'
416 self._append_plain_text('No default editor available.\n'
416 'Specify a GUI text editor in the `IPythonWidget.editor` configurable\n'
417 'Specify a GUI text editor in the `IPythonWidget.editor` '
417 'to enable the %edit magic')
418 'configurable to enable the %edit magic')
418 else:
419 else:
419 try:
420 try:
420 filename = '"%s"' % filename
421 filename = '"%s"' % filename
421 if line and self.editor_line:
422 if line and self.editor_line:
422 command = self.editor_line.format(filename=filename,
423 command = self.editor_line.format(filename=filename,
423 line=line)
424 line=line)
424 else:
425 else:
425 try:
426 try:
426 command = self.editor.format()
427 command = self.editor.format()
427 except KeyError:
428 except KeyError:
428 command = self.editor.format(filename=filename)
429 command = self.editor.format(filename=filename)
429 else:
430 else:
430 command += ' ' + filename
431 command += ' ' + filename
431 except KeyError:
432 except KeyError:
432 self._append_plain_text('Invalid editor command.\n')
433 self._append_plain_text('Invalid editor command.\n')
433 else:
434 else:
434 try:
435 try:
435 Popen(command, shell=True)
436 Popen(command, shell=True)
436 except OSError:
437 except OSError:
437 msg = 'Opening editor with command "%s" failed.\n'
438 msg = 'Opening editor with command "%s" failed.\n'
438 self._append_plain_text(msg % command)
439 self._append_plain_text(msg % command)
439
440
440 def _make_in_prompt(self, number):
441 def _make_in_prompt(self, number):
441 """ Given a prompt number, returns an HTML In prompt.
442 """ Given a prompt number, returns an HTML In prompt.
442 """
443 """
443 body = self.in_prompt % number
444 body = self.in_prompt % number
444 return '<span class="in-prompt">%s</span>' % body
445 return '<span class="in-prompt">%s</span>' % body
445
446
446 def _make_continuation_prompt(self, prompt):
447 def _make_continuation_prompt(self, prompt):
447 """ Given a plain text version of an In prompt, returns an HTML
448 """ Given a plain text version of an In prompt, returns an HTML
448 continuation prompt.
449 continuation prompt.
449 """
450 """
450 end_chars = '...: '
451 end_chars = '...: '
451 space_count = len(prompt.lstrip('\n')) - len(end_chars)
452 space_count = len(prompt.lstrip('\n')) - len(end_chars)
452 body = '&nbsp;' * space_count + end_chars
453 body = '&nbsp;' * space_count + end_chars
453 return '<span class="in-prompt">%s</span>' % body
454 return '<span class="in-prompt">%s</span>' % body
454
455
455 def _make_out_prompt(self, number):
456 def _make_out_prompt(self, number):
456 """ Given a prompt number, returns an HTML Out prompt.
457 """ Given a prompt number, returns an HTML Out prompt.
457 """
458 """
458 body = self.out_prompt % number
459 body = self.out_prompt % number
459 return '<span class="out-prompt">%s</span>' % body
460 return '<span class="out-prompt">%s</span>' % body
460
461
461 #------ Payload handlers --------------------------------------------------
462 #------ Payload handlers --------------------------------------------------
462
463
463 # Payload handlers with a generic interface: each takes the opaque payload
464 # Payload handlers with a generic interface: each takes the opaque payload
464 # dict, unpacks it and calls the underlying functions with the necessary
465 # dict, unpacks it and calls the underlying functions with the necessary
465 # arguments.
466 # arguments.
466
467
467 def _handle_payload_edit(self, item):
468 def _handle_payload_edit(self, item):
468 self._edit(item['filename'], item['line_number'])
469 self._edit(item['filename'], item['line_number'])
469
470
470 def _handle_payload_exit(self, item):
471 def _handle_payload_exit(self, item):
471 self._keep_kernel_on_exit = item['keepkernel']
472 self._keep_kernel_on_exit = item['keepkernel']
472 self.exit_requested.emit()
473 self.exit_requested.emit()
473
474
474 def _handle_payload_next_input(self, item):
475 def _handle_payload_next_input(self, item):
475 self.input_buffer = dedent(item['text'].rstrip())
476 self.input_buffer = dedent(item['text'].rstrip())
476
477
477 def _handle_payload_page(self, item):
478 def _handle_payload_page(self, item):
478 # Since the plain text widget supports only a very small subset of HTML
479 # Since the plain text widget supports only a very small subset of HTML
479 # and we have no control over the HTML source, we only page HTML
480 # and we have no control over the HTML source, we only page HTML
480 # payloads in the rich text widget.
481 # payloads in the rich text widget.
481 if item['html'] and self.kind == 'rich':
482 if item['html'] and self.kind == 'rich':
482 self._page(item['html'], html=True)
483 self._page(item['html'], html=True)
483 else:
484 else:
484 self._page(item['text'], html=False)
485 self._page(item['text'], html=False)
485
486
486 #------ Trait change handlers --------------------------------------------
487 #------ Trait change handlers --------------------------------------------
487
488
488 def _style_sheet_changed(self):
489 def _style_sheet_changed(self):
489 """ Set the style sheets of the underlying widgets.
490 """ Set the style sheets of the underlying widgets.
490 """
491 """
491 self.setStyleSheet(self.style_sheet)
492 self.setStyleSheet(self.style_sheet)
492 self._control.document().setDefaultStyleSheet(self.style_sheet)
493 self._control.document().setDefaultStyleSheet(self.style_sheet)
493 if self._page_control:
494 if self._page_control:
494 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
495 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
495
496
496 bg_color = self._control.palette().window().color()
497 bg_color = self._control.palette().window().color()
497 self._ansi_processor.set_background_color(bg_color)
498 self._ansi_processor.set_background_color(bg_color)
498
499
499
500
500 def _syntax_style_changed(self):
501 def _syntax_style_changed(self):
501 """ Set the style for the syntax highlighter.
502 """ Set the style for the syntax highlighter.
502 """
503 """
503 if self._highlighter is None:
504 if self._highlighter is None:
504 # ignore premature calls
505 # ignore premature calls
505 return
506 return
506 if self.syntax_style:
507 if self.syntax_style:
507 self._highlighter.set_style(self.syntax_style)
508 self._highlighter.set_style(self.syntax_style)
508 else:
509 else:
509 self._highlighter.set_style_sheet(self.style_sheet)
510 self._highlighter.set_style_sheet(self.style_sheet)
510
511
@@ -1,249 +1,252 b''
1 # Standard libary imports.
1 # Standard libary imports.
2 from base64 import decodestring
2 from base64 import decodestring
3 import os
3 import os
4 import re
4 import re
5
5
6 # System libary imports.
6 # System libary imports.
7 from IPython.external.qt import QtCore, QtGui
7 from IPython.external.qt import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
11 from ipython_widget import IPythonWidget
11 from ipython_widget import IPythonWidget
12
12
13
13
14 class RichIPythonWidget(IPythonWidget):
14 class RichIPythonWidget(IPythonWidget):
15 """ An IPythonWidget that supports rich text, including lists, images, and
15 """ An IPythonWidget that supports rich text, including lists, images, and
16 tables. Note that raw performance will be reduced compared to the plain
16 tables. Note that raw performance will be reduced compared to the plain
17 text version.
17 text version.
18 """
18 """
19
19
20 # RichIPythonWidget protected class variables.
20 # RichIPythonWidget protected class variables.
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
22
22
23 #---------------------------------------------------------------------------
23 #---------------------------------------------------------------------------
24 # 'object' interface
24 # 'object' interface
25 #---------------------------------------------------------------------------
25 #---------------------------------------------------------------------------
26
26
27 def __init__(self, *args, **kw):
27 def __init__(self, *args, **kw):
28 """ Create a RichIPythonWidget.
28 """ Create a RichIPythonWidget.
29 """
29 """
30 kw['kind'] = 'rich'
30 kw['kind'] = 'rich'
31 super(RichIPythonWidget, self).__init__(*args, **kw)
31 super(RichIPythonWidget, self).__init__(*args, **kw)
32
32
33 # Configure the ConsoleWidget HTML exporter for our formats.
33 # Configure the ConsoleWidget HTML exporter for our formats.
34 self._html_exporter.image_tag = self._get_image_tag
34 self._html_exporter.image_tag = self._get_image_tag
35
35
36 # Dictionary for resolving document resource names to SVG data.
36 # Dictionary for resolving document resource names to SVG data.
37 self._name_to_svg_map = {}
37 self._name_to_svg_map = {}
38
38
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40 # 'ConsoleWidget' protected interface
40 # 'ConsoleWidget' protected interface
41 #---------------------------------------------------------------------------
41 #---------------------------------------------------------------------------
42
42
43 def _context_menu_make(self, pos):
43 def _context_menu_make(self, pos):
44 """ Reimplemented to return a custom context menu for images.
44 """ Reimplemented to return a custom context menu for images.
45 """
45 """
46 format = self._control.cursorForPosition(pos).charFormat()
46 format = self._control.cursorForPosition(pos).charFormat()
47 name = format.stringProperty(QtGui.QTextFormat.ImageName)
47 name = format.stringProperty(QtGui.QTextFormat.ImageName)
48 if name:
48 if name:
49 menu = QtGui.QMenu()
49 menu = QtGui.QMenu()
50
50
51 menu.addAction('Copy Image', lambda: self._copy_image(name))
51 menu.addAction('Copy Image', lambda: self._copy_image(name))
52 menu.addAction('Save Image As...', lambda: self._save_image(name))
52 menu.addAction('Save Image As...', lambda: self._save_image(name))
53 menu.addSeparator()
53 menu.addSeparator()
54
54
55 svg = self._name_to_svg_map.get(name, None)
55 svg = self._name_to_svg_map.get(name, None)
56 if svg is not None:
56 if svg is not None:
57 menu.addSeparator()
57 menu.addSeparator()
58 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
58 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
59 menu.addAction('Save SVG As...',
59 menu.addAction('Save SVG As...',
60 lambda: save_svg(svg, self._control))
60 lambda: save_svg(svg, self._control))
61 else:
61 else:
62 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
62 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
63 return menu
63 return menu
64
64
65 #---------------------------------------------------------------------------
65 #---------------------------------------------------------------------------
66 # 'BaseFrontendMixin' abstract interface
66 # 'BaseFrontendMixin' abstract interface
67 #---------------------------------------------------------------------------
67 #---------------------------------------------------------------------------
68
68
69 def _handle_pyout(self, msg):
69 def _handle_pyout(self, msg):
70 """ Overridden to handle rich data types, like SVG.
70 """ Overridden to handle rich data types, like SVG.
71 """
71 """
72 if not self._hidden and self._is_from_this_session(msg):
72 if not self._hidden and self._is_from_this_session(msg):
73 content = msg['content']
73 content = msg['content']
74 prompt_number = content['execution_count']
74 prompt_number = content['execution_count']
75 data = content['data']
75 data = content['data']
76 if data.has_key('image/svg+xml'):
76 if data.has_key('image/svg+xml'):
77 self._append_plain_text(self.output_sep)
77 self._append_plain_text(self.output_sep, True)
78 self._append_html(self._make_out_prompt(prompt_number))
78 self._append_html(self._make_out_prompt(prompt_number), True)
79 # TODO: try/except this call.
79 self._append_svg(data['image/svg+xml'], True)
80 self._append_svg(data['image/svg+xml'])
80 self._append_html(self.output_sep2, True)
81 self._append_html(self.output_sep2)
82 elif data.has_key('image/png'):
81 elif data.has_key('image/png'):
83 self._append_plain_text(self.output_sep)
82 self._append_plain_text(self.output_sep, True)
84 self._append_html(self._make_out_prompt(prompt_number))
83 self._append_html(self._make_out_prompt(prompt_number), True)
85 # This helps the output to look nice.
84 # This helps the output to look nice.
86 self._append_plain_text('\n')
85 self._append_plain_text('\n', True)
87 # TODO: try/except these calls
86 self._append_png(decodestring(data['image/png']), True)
88 png = decodestring(data['image/png'])
87 self._append_html(self.output_sep2, True)
89 self._append_png(png)
90 self._append_html(self.output_sep2)
91 else:
88 else:
92 # Default back to the plain text representation.
89 # Default back to the plain text representation.
93 return super(RichIPythonWidget, self)._handle_pyout(msg)
90 return super(RichIPythonWidget, self)._handle_pyout(msg)
94
91
95 def _handle_display_data(self, msg):
92 def _handle_display_data(self, msg):
96 """ Overridden to handle rich data types, like SVG.
93 """ Overridden to handle rich data types, like SVG.
97 """
94 """
98 if not self._hidden and self._is_from_this_session(msg):
95 if not self._hidden and self._is_from_this_session(msg):
99 source = msg['content']['source']
96 source = msg['content']['source']
100 data = msg['content']['data']
97 data = msg['content']['data']
101 metadata = msg['content']['metadata']
98 metadata = msg['content']['metadata']
102 # Try to use the svg or html representations.
99 # Try to use the svg or html representations.
103 # FIXME: Is this the right ordering of things to try?
100 # FIXME: Is this the right ordering of things to try?
104 if data.has_key('image/svg+xml'):
101 if data.has_key('image/svg+xml'):
105 svg = data['image/svg+xml']
102 svg = data['image/svg+xml']
106 # TODO: try/except this call.
103 self._append_svg(svg, True)
107 self._append_svg(svg)
108 elif data.has_key('image/png'):
104 elif data.has_key('image/png'):
109 # TODO: try/except these calls
110 # PNG data is base64 encoded as it passes over the network
105 # PNG data is base64 encoded as it passes over the network
111 # in a JSON structure so we decode it.
106 # in a JSON structure so we decode it.
112 png = decodestring(data['image/png'])
107 png = decodestring(data['image/png'])
113 self._append_png(png)
108 self._append_png(png, True)
114 else:
109 else:
115 # Default back to the plain text representation.
110 # Default back to the plain text representation.
116 return super(RichIPythonWidget, self)._handle_display_data(msg)
111 return super(RichIPythonWidget, self)._handle_display_data(msg)
117
112
118 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
119 # 'RichIPythonWidget' protected interface
114 # 'RichIPythonWidget' protected interface
120 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
121
116
122 def _append_svg(self, svg):
117 def _append_png(self, png, before_prompt=False):
123 """ Append raw svg data to the widget.
118 """ Append raw PNG data to the widget.
124 """
119 """
125 try:
120 self._append_custom(self._insert_png, png, before_prompt)
126 image = svg_to_image(svg)
127 except ValueError:
128 self._append_plain_text('Received invalid plot data.')
129 else:
130 format = self._add_image(image)
131 self._name_to_svg_map[format.name()] = svg
132 cursor = self._get_end_cursor()
133 cursor.insertBlock()
134 cursor.insertImage(format)
135 cursor.insertBlock()
136
121
137 def _append_png(self, png):
122 def _append_svg(self, svg, before_prompt=False):
138 """ Append raw svg data to the widget.
123 """ Append raw SVG data to the widget.
139 """
124 """
140 try:
125 self._append_custom(self._insert_svg, svg, before_prompt)
141 image = QtGui.QImage()
142 image.loadFromData(png, 'PNG')
143 except ValueError:
144 self._append_plain_text('Received invalid plot data.')
145 else:
146 format = self._add_image(image)
147 cursor = self._get_end_cursor()
148 cursor.insertBlock()
149 cursor.insertImage(format)
150 cursor.insertBlock()
151
126
152 def _add_image(self, image):
127 def _add_image(self, image):
153 """ Adds the specified QImage to the document and returns a
128 """ Adds the specified QImage to the document and returns a
154 QTextImageFormat that references it.
129 QTextImageFormat that references it.
155 """
130 """
156 document = self._control.document()
131 document = self._control.document()
157 name = str(image.cacheKey())
132 name = str(image.cacheKey())
158 document.addResource(QtGui.QTextDocument.ImageResource,
133 document.addResource(QtGui.QTextDocument.ImageResource,
159 QtCore.QUrl(name), image)
134 QtCore.QUrl(name), image)
160 format = QtGui.QTextImageFormat()
135 format = QtGui.QTextImageFormat()
161 format.setName(name)
136 format.setName(name)
162 return format
137 return format
163
138
164 def _copy_image(self, name):
139 def _copy_image(self, name):
165 """ Copies the ImageResource with 'name' to the clipboard.
140 """ Copies the ImageResource with 'name' to the clipboard.
166 """
141 """
167 image = self._get_image(name)
142 image = self._get_image(name)
168 QtGui.QApplication.clipboard().setImage(image)
143 QtGui.QApplication.clipboard().setImage(image)
169
144
170 def _get_image(self, name):
145 def _get_image(self, name):
171 """ Returns the QImage stored as the ImageResource with 'name'.
146 """ Returns the QImage stored as the ImageResource with 'name'.
172 """
147 """
173 document = self._control.document()
148 document = self._control.document()
174 image = document.resource(QtGui.QTextDocument.ImageResource,
149 image = document.resource(QtGui.QTextDocument.ImageResource,
175 QtCore.QUrl(name))
150 QtCore.QUrl(name))
176 return image
151 return image
177
152
178 def _get_image_tag(self, match, path = None, format = "png"):
153 def _get_image_tag(self, match, path = None, format = "png"):
179 """ Return (X)HTML mark-up for the image-tag given by match.
154 """ Return (X)HTML mark-up for the image-tag given by match.
180
155
181 Parameters
156 Parameters
182 ----------
157 ----------
183 match : re.SRE_Match
158 match : re.SRE_Match
184 A match to an HTML image tag as exported by Qt, with
159 A match to an HTML image tag as exported by Qt, with
185 match.group("Name") containing the matched image ID.
160 match.group("Name") containing the matched image ID.
186
161
187 path : string|None, optional [default None]
162 path : string|None, optional [default None]
188 If not None, specifies a path to which supporting files may be
163 If not None, specifies a path to which supporting files may be
189 written (e.g., for linked images). If None, all images are to be
164 written (e.g., for linked images). If None, all images are to be
190 included inline.
165 included inline.
191
166
192 format : "png"|"svg", optional [default "png"]
167 format : "png"|"svg", optional [default "png"]
193 Format for returned or referenced images.
168 Format for returned or referenced images.
194 """
169 """
195 if format == "png":
170 if format == "png":
196 try:
171 try:
197 image = self._get_image(match.group("name"))
172 image = self._get_image(match.group("name"))
198 except KeyError:
173 except KeyError:
199 return "<b>Couldn't find image %s</b>" % match.group("name")
174 return "<b>Couldn't find image %s</b>" % match.group("name")
200
175
201 if path is not None:
176 if path is not None:
202 if not os.path.exists(path):
177 if not os.path.exists(path):
203 os.mkdir(path)
178 os.mkdir(path)
204 relpath = os.path.basename(path)
179 relpath = os.path.basename(path)
205 if image.save("%s/qt_img%s.png" % (path,match.group("name")),
180 if image.save("%s/qt_img%s.png" % (path,match.group("name")),
206 "PNG"):
181 "PNG"):
207 return '<img src="%s/qt_img%s.png">' % (relpath,
182 return '<img src="%s/qt_img%s.png">' % (relpath,
208 match.group("name"))
183 match.group("name"))
209 else:
184 else:
210 return "<b>Couldn't save image!</b>"
185 return "<b>Couldn't save image!</b>"
211 else:
186 else:
212 ba = QtCore.QByteArray()
187 ba = QtCore.QByteArray()
213 buffer_ = QtCore.QBuffer(ba)
188 buffer_ = QtCore.QBuffer(ba)
214 buffer_.open(QtCore.QIODevice.WriteOnly)
189 buffer_.open(QtCore.QIODevice.WriteOnly)
215 image.save(buffer_, "PNG")
190 image.save(buffer_, "PNG")
216 buffer_.close()
191 buffer_.close()
217 return '<img src="data:image/png;base64,\n%s\n" />' % (
192 return '<img src="data:image/png;base64,\n%s\n" />' % (
218 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
193 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
219
194
220 elif format == "svg":
195 elif format == "svg":
221 try:
196 try:
222 svg = str(self._name_to_svg_map[match.group("name")])
197 svg = str(self._name_to_svg_map[match.group("name")])
223 except KeyError:
198 except KeyError:
224 return "<b>Couldn't find image %s</b>" % match.group("name")
199 return "<b>Couldn't find image %s</b>" % match.group("name")
225
200
226 # Not currently checking path, because it's tricky to find a
201 # Not currently checking path, because it's tricky to find a
227 # cross-browser way to embed external SVG images (e.g., via
202 # cross-browser way to embed external SVG images (e.g., via
228 # object or embed tags).
203 # object or embed tags).
229
204
230 # Chop stand-alone header from matplotlib SVG
205 # Chop stand-alone header from matplotlib SVG
231 offset = svg.find("<svg")
206 offset = svg.find("<svg")
232 assert(offset > -1)
207 assert(offset > -1)
233
208
234 return svg[offset:]
209 return svg[offset:]
235
210
236 else:
211 else:
237 return '<b>Unrecognized image format</b>'
212 return '<b>Unrecognized image format</b>'
238
213
214 def _insert_png(self, cursor, png):
215 """ Insert raw PNG data into the widget.
216 """
217 try:
218 image = QtGui.QImage()
219 image.loadFromData(png, 'PNG')
220 except ValueError:
221 self._insert_plain_text(cursor, 'Received invalid PNG data.')
222 else:
223 format = self._add_image(image)
224 cursor.insertBlock()
225 cursor.insertImage(format)
226 cursor.insertBlock()
227
228 def _insert_svg(self, cursor, svg):
229 """ Insert raw SVG data into the widet.
230 """
231 try:
232 image = svg_to_image(svg)
233 except ValueError:
234 self._insert_plain_text(cursor, 'Received invalid SVG data.')
235 else:
236 format = self._add_image(image)
237 self._name_to_svg_map[format.name()] = svg
238 cursor.insertBlock()
239 cursor.insertImage(format)
240 cursor.insertBlock()
241
239 def _save_image(self, name, format='PNG'):
242 def _save_image(self, name, format='PNG'):
240 """ Shows a save dialog for the ImageResource with 'name'.
243 """ Shows a save dialog for the ImageResource with 'name'.
241 """
244 """
242 dialog = QtGui.QFileDialog(self._control, 'Save Image')
245 dialog = QtGui.QFileDialog(self._control, 'Save Image')
243 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
246 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
244 dialog.setDefaultSuffix(format.lower())
247 dialog.setDefaultSuffix(format.lower())
245 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
248 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
246 if dialog.exec_():
249 if dialog.exec_():
247 filename = dialog.selectedFiles()[0]
250 filename = dialog.selectedFiles()[0]
248 image = self._get_image(name)
251 image = self._get_image(name)
249 image.save(filename, format)
252 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now