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