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