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