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