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