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