##// END OF EJS Templates
QtConsole now uses newapp
MinRK -
Show More
@@ -1,1766 +1,1772 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 Configurable
19 from IPython.config.configurable import Configurable
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.traitlets import Bool, Enum, Int
22 from IPython.utils.traitlets import Bool, Enum, Int
23 from ansi_code_processor import QtAnsiCodeProcessor
23 from ansi_code_processor import QtAnsiCodeProcessor
24 from completion_widget import CompletionWidget
24 from completion_widget import CompletionWidget
25 from kill_ring import QtKillRing
25 from kill_ring import QtKillRing
26
26
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 # Functions
28 # Functions
29 #-----------------------------------------------------------------------------
29 #-----------------------------------------------------------------------------
30
30
31 def is_letter_or_number(char):
31 def is_letter_or_number(char):
32 """ Returns whether the specified unicode character is a letter or a number.
32 """ Returns whether the specified unicode character is a letter or a number.
33 """
33 """
34 cat = category(char)
34 cat = category(char)
35 return cat.startswith('L') or cat.startswith('N')
35 return cat.startswith('L') or cat.startswith('N')
36
36
37 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
38 # Classes
38 # Classes
39 #-----------------------------------------------------------------------------
39 #-----------------------------------------------------------------------------
40
40
41 class ConsoleWidget(Configurable, QtGui.QWidget):
41 class ConsoleWidget(Configurable, QtGui.QWidget):
42 """ An abstract base class for console-type widgets. This class has
42 """ An abstract base class for console-type widgets. This class has
43 functionality for:
43 functionality for:
44
44
45 * Maintaining a prompt and editing region
45 * Maintaining a prompt and editing region
46 * Providing the traditional Unix-style console keyboard shortcuts
46 * Providing the traditional Unix-style console keyboard shortcuts
47 * Performing tab completion
47 * Performing tab completion
48 * Paging text
48 * Paging text
49 * Handling ANSI escape codes
49 * Handling ANSI escape codes
50
50
51 ConsoleWidget also provides a number of utility methods that will be
51 ConsoleWidget also provides a number of utility methods that will be
52 convenient to implementors of a console-style widget.
52 convenient to implementors of a console-style widget.
53 """
53 """
54 __metaclass__ = MetaQObjectHasTraits
54 __metaclass__ = MetaQObjectHasTraits
55
55
56 #------ Configuration ------------------------------------------------------
56 #------ Configuration ------------------------------------------------------
57
57
58 # Whether to process ANSI escape codes.
58 ansi_codes = Bool(True, config=True,
59 ansi_codes = Bool(True, config=True)
59 help="Whether to process ANSI escape codes."
60
60 )
61 # The maximum number of lines of text before truncation. Specifying a
61 buffer_size = Int(500, config=True,
62 # non-positive number disables text truncation (not recommended).
62 help="""
63 buffer_size = Int(500, config=True)
63 The maximum number of lines of text before truncation. Specifying a
64
64 non-positive number disables text truncation (not recommended).
65 # Whether to use a list widget or plain text output for tab completion.
65 """
66 gui_completion = Bool(False, config=True)
66 )
67
67 gui_completion = Bool(False, config=True,
68 # The type of underlying text widget to use. Valid values are 'plain', which
68 help="Use a list widget instead of plain text output for tab completion."
69 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
69 )
70 # NOTE: this value can only be specified during initialization.
70 # NOTE: this value can only be specified during initialization.
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
71 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
72
72 help="""
73 # The type of paging to use. Valid values are:
73 The type of underlying text widget to use. Valid values are 'plain', which
74 # 'inside' : The widget pages like a traditional terminal.
74 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
75 # 'hsplit' : When paging is requested, the widget is split
75 """
76 # horizontally. The top pane contains the console, and the
76 )
77 # bottom pane contains the paged text.
78 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
79 # 'custom' : No action is taken by the widget beyond emitting a
80 # 'custom_page_requested(str)' signal.
81 # 'none' : The text is written directly to the console.
82 # NOTE: this value can only be specified during initialization.
77 # NOTE: this value can only be specified during initialization.
83 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
78 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
84 default_value='inside', config=True)
79 default_value='inside', config=True,
80 help="""
81 The type of paging to use. Valid values are:
82 'inside' : The widget pages like a traditional terminal.
83 'hsplit' : When paging is requested, the widget is split
84 : horizontally. The top pane contains the console, and the
85 : bottom pane contains the paged text.
86 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
87 'custom' : No action is taken by the widget beyond emitting a
88 : 'custom_page_requested(str)' signal.
89 'none' : The text is written directly to the console.
90 """)
85
91
86 # Whether to override ShortcutEvents for the keybindings defined by this
92 # Whether to override ShortcutEvents for the keybindings defined by this
87 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
93 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
88 # priority (when it has focus) over, e.g., window-level menu shortcuts.
94 # priority (when it has focus) over, e.g., window-level menu shortcuts.
89 override_shortcuts = Bool(False)
95 override_shortcuts = Bool(False)
90
96
91 #------ Signals ------------------------------------------------------------
97 #------ Signals ------------------------------------------------------------
92
98
93 # Signals that indicate ConsoleWidget state.
99 # Signals that indicate ConsoleWidget state.
94 copy_available = QtCore.Signal(bool)
100 copy_available = QtCore.Signal(bool)
95 redo_available = QtCore.Signal(bool)
101 redo_available = QtCore.Signal(bool)
96 undo_available = QtCore.Signal(bool)
102 undo_available = QtCore.Signal(bool)
97
103
98 # Signal emitted when paging is needed and the paging style has been
104 # Signal emitted when paging is needed and the paging style has been
99 # specified as 'custom'.
105 # specified as 'custom'.
100 custom_page_requested = QtCore.Signal(object)
106 custom_page_requested = QtCore.Signal(object)
101
107
102 # Signal emitted when the font is changed.
108 # Signal emitted when the font is changed.
103 font_changed = QtCore.Signal(QtGui.QFont)
109 font_changed = QtCore.Signal(QtGui.QFont)
104
110
105 #------ Protected class variables ------------------------------------------
111 #------ Protected class variables ------------------------------------------
106
112
107 # When the control key is down, these keys are mapped.
113 # When the control key is down, these keys are mapped.
108 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
114 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
109 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
115 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
110 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
116 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
111 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
117 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
112 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
118 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
113 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
119 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
114 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
120 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
115 if not sys.platform == 'darwin':
121 if not sys.platform == 'darwin':
116 # On OS X, Ctrl-E already does the right thing, whereas End moves the
122 # On OS X, Ctrl-E already does the right thing, whereas End moves the
117 # cursor to the bottom of the buffer.
123 # cursor to the bottom of the buffer.
118 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
124 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
119
125
120 # The shortcuts defined by this widget. We need to keep track of these to
126 # The shortcuts defined by this widget. We need to keep track of these to
121 # support 'override_shortcuts' above.
127 # support 'override_shortcuts' above.
122 _shortcuts = set(_ctrl_down_remap.keys() +
128 _shortcuts = set(_ctrl_down_remap.keys() +
123 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
129 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
124 QtCore.Qt.Key_V ])
130 QtCore.Qt.Key_V ])
125
131
126 #---------------------------------------------------------------------------
132 #---------------------------------------------------------------------------
127 # 'QObject' interface
133 # 'QObject' interface
128 #---------------------------------------------------------------------------
134 #---------------------------------------------------------------------------
129
135
130 def __init__(self, parent=None, **kw):
136 def __init__(self, parent=None, **kw):
131 """ Create a ConsoleWidget.
137 """ Create a ConsoleWidget.
132
138
133 Parameters:
139 Parameters:
134 -----------
140 -----------
135 parent : QWidget, optional [default None]
141 parent : QWidget, optional [default None]
136 The parent for this widget.
142 The parent for this widget.
137 """
143 """
138 QtGui.QWidget.__init__(self, parent)
144 QtGui.QWidget.__init__(self, parent)
139 Configurable.__init__(self, **kw)
145 Configurable.__init__(self, **kw)
140
146
141 # Create the layout and underlying text widget.
147 # Create the layout and underlying text widget.
142 layout = QtGui.QStackedLayout(self)
148 layout = QtGui.QStackedLayout(self)
143 layout.setContentsMargins(0, 0, 0, 0)
149 layout.setContentsMargins(0, 0, 0, 0)
144 self._control = self._create_control()
150 self._control = self._create_control()
145 self._page_control = None
151 self._page_control = None
146 self._splitter = None
152 self._splitter = None
147 if self.paging in ('hsplit', 'vsplit'):
153 if self.paging in ('hsplit', 'vsplit'):
148 self._splitter = QtGui.QSplitter()
154 self._splitter = QtGui.QSplitter()
149 if self.paging == 'hsplit':
155 if self.paging == 'hsplit':
150 self._splitter.setOrientation(QtCore.Qt.Horizontal)
156 self._splitter.setOrientation(QtCore.Qt.Horizontal)
151 else:
157 else:
152 self._splitter.setOrientation(QtCore.Qt.Vertical)
158 self._splitter.setOrientation(QtCore.Qt.Vertical)
153 self._splitter.addWidget(self._control)
159 self._splitter.addWidget(self._control)
154 layout.addWidget(self._splitter)
160 layout.addWidget(self._splitter)
155 else:
161 else:
156 layout.addWidget(self._control)
162 layout.addWidget(self._control)
157
163
158 # Create the paging widget, if necessary.
164 # Create the paging widget, if necessary.
159 if self.paging in ('inside', 'hsplit', 'vsplit'):
165 if self.paging in ('inside', 'hsplit', 'vsplit'):
160 self._page_control = self._create_page_control()
166 self._page_control = self._create_page_control()
161 if self._splitter:
167 if self._splitter:
162 self._page_control.hide()
168 self._page_control.hide()
163 self._splitter.addWidget(self._page_control)
169 self._splitter.addWidget(self._page_control)
164 else:
170 else:
165 layout.addWidget(self._page_control)
171 layout.addWidget(self._page_control)
166
172
167 # Initialize protected variables. Some variables contain useful state
173 # Initialize protected variables. Some variables contain useful state
168 # information for subclasses; they should be considered read-only.
174 # information for subclasses; they should be considered read-only.
169 self._ansi_processor = QtAnsiCodeProcessor()
175 self._ansi_processor = QtAnsiCodeProcessor()
170 self._completion_widget = CompletionWidget(self._control)
176 self._completion_widget = CompletionWidget(self._control)
171 self._continuation_prompt = '> '
177 self._continuation_prompt = '> '
172 self._continuation_prompt_html = None
178 self._continuation_prompt_html = None
173 self._executing = False
179 self._executing = False
174 self._filter_drag = False
180 self._filter_drag = False
175 self._filter_resize = False
181 self._filter_resize = False
176 self._html_exporter = HtmlExporter(self._control)
182 self._html_exporter = HtmlExporter(self._control)
177 self._input_buffer_executing = ''
183 self._input_buffer_executing = ''
178 self._input_buffer_pending = ''
184 self._input_buffer_pending = ''
179 self._kill_ring = QtKillRing(self._control)
185 self._kill_ring = QtKillRing(self._control)
180 self._prompt = ''
186 self._prompt = ''
181 self._prompt_html = None
187 self._prompt_html = None
182 self._prompt_pos = 0
188 self._prompt_pos = 0
183 self._prompt_sep = ''
189 self._prompt_sep = ''
184 self._reading = False
190 self._reading = False
185 self._reading_callback = None
191 self._reading_callback = None
186 self._tab_width = 8
192 self._tab_width = 8
187 self._text_completing_pos = 0
193 self._text_completing_pos = 0
188
194
189 # Set a monospaced font.
195 # Set a monospaced font.
190 self.reset_font()
196 self.reset_font()
191
197
192 # Configure actions.
198 # Configure actions.
193 action = QtGui.QAction('Print', None)
199 action = QtGui.QAction('Print', None)
194 action.setEnabled(True)
200 action.setEnabled(True)
195 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
201 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
196 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
202 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
197 # Only override the default if there is a collision.
203 # Only override the default if there is a collision.
198 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
204 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
199 printkey = "Ctrl+Shift+P"
205 printkey = "Ctrl+Shift+P"
200 action.setShortcut(printkey)
206 action.setShortcut(printkey)
201 action.triggered.connect(self.print_)
207 action.triggered.connect(self.print_)
202 self.addAction(action)
208 self.addAction(action)
203 self._print_action = action
209 self._print_action = action
204
210
205 action = QtGui.QAction('Save as HTML/XML', None)
211 action = QtGui.QAction('Save as HTML/XML', None)
206 action.setShortcut(QtGui.QKeySequence.Save)
212 action.setShortcut(QtGui.QKeySequence.Save)
207 action.triggered.connect(self.export_html)
213 action.triggered.connect(self.export_html)
208 self.addAction(action)
214 self.addAction(action)
209 self._export_action = action
215 self._export_action = action
210
216
211 action = QtGui.QAction('Select All', None)
217 action = QtGui.QAction('Select All', None)
212 action.setEnabled(True)
218 action.setEnabled(True)
213 action.setShortcut(QtGui.QKeySequence.SelectAll)
219 action.setShortcut(QtGui.QKeySequence.SelectAll)
214 action.triggered.connect(self.select_all)
220 action.triggered.connect(self.select_all)
215 self.addAction(action)
221 self.addAction(action)
216 self._select_all_action = action
222 self._select_all_action = action
217
223
218 def eventFilter(self, obj, event):
224 def eventFilter(self, obj, event):
219 """ Reimplemented to ensure a console-like behavior in the underlying
225 """ Reimplemented to ensure a console-like behavior in the underlying
220 text widgets.
226 text widgets.
221 """
227 """
222 etype = event.type()
228 etype = event.type()
223 if etype == QtCore.QEvent.KeyPress:
229 if etype == QtCore.QEvent.KeyPress:
224
230
225 # Re-map keys for all filtered widgets.
231 # Re-map keys for all filtered widgets.
226 key = event.key()
232 key = event.key()
227 if self._control_key_down(event.modifiers()) and \
233 if self._control_key_down(event.modifiers()) and \
228 key in self._ctrl_down_remap:
234 key in self._ctrl_down_remap:
229 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
235 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
230 self._ctrl_down_remap[key],
236 self._ctrl_down_remap[key],
231 QtCore.Qt.NoModifier)
237 QtCore.Qt.NoModifier)
232 QtGui.qApp.sendEvent(obj, new_event)
238 QtGui.qApp.sendEvent(obj, new_event)
233 return True
239 return True
234
240
235 elif obj == self._control:
241 elif obj == self._control:
236 return self._event_filter_console_keypress(event)
242 return self._event_filter_console_keypress(event)
237
243
238 elif obj == self._page_control:
244 elif obj == self._page_control:
239 return self._event_filter_page_keypress(event)
245 return self._event_filter_page_keypress(event)
240
246
241 # Make middle-click paste safe.
247 # Make middle-click paste safe.
242 elif etype == QtCore.QEvent.MouseButtonRelease and \
248 elif etype == QtCore.QEvent.MouseButtonRelease and \
243 event.button() == QtCore.Qt.MidButton and \
249 event.button() == QtCore.Qt.MidButton and \
244 obj == self._control.viewport():
250 obj == self._control.viewport():
245 cursor = self._control.cursorForPosition(event.pos())
251 cursor = self._control.cursorForPosition(event.pos())
246 self._control.setTextCursor(cursor)
252 self._control.setTextCursor(cursor)
247 self.paste(QtGui.QClipboard.Selection)
253 self.paste(QtGui.QClipboard.Selection)
248 return True
254 return True
249
255
250 # Manually adjust the scrollbars *after* a resize event is dispatched.
256 # Manually adjust the scrollbars *after* a resize event is dispatched.
251 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
257 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
252 self._filter_resize = True
258 self._filter_resize = True
253 QtGui.qApp.sendEvent(obj, event)
259 QtGui.qApp.sendEvent(obj, event)
254 self._adjust_scrollbars()
260 self._adjust_scrollbars()
255 self._filter_resize = False
261 self._filter_resize = False
256 return True
262 return True
257
263
258 # Override shortcuts for all filtered widgets.
264 # Override shortcuts for all filtered widgets.
259 elif etype == QtCore.QEvent.ShortcutOverride and \
265 elif etype == QtCore.QEvent.ShortcutOverride and \
260 self.override_shortcuts and \
266 self.override_shortcuts and \
261 self._control_key_down(event.modifiers()) and \
267 self._control_key_down(event.modifiers()) and \
262 event.key() in self._shortcuts:
268 event.key() in self._shortcuts:
263 event.accept()
269 event.accept()
264
270
265 # Ensure that drags are safe. The problem is that the drag starting
271 # Ensure that drags are safe. The problem is that the drag starting
266 # logic, which determines whether the drag is a Copy or Move, is locked
272 # logic, which determines whether the drag is a Copy or Move, is locked
267 # down in QTextControl. If the widget is editable, which it must be if
273 # down in QTextControl. If the widget is editable, which it must be if
268 # we're not executing, the drag will be a Move. The following hack
274 # we're not executing, the drag will be a Move. The following hack
269 # prevents QTextControl from deleting the text by clearing the selection
275 # prevents QTextControl from deleting the text by clearing the selection
270 # when a drag leave event originating from this widget is dispatched.
276 # when a drag leave event originating from this widget is dispatched.
271 # The fact that we have to clear the user's selection is unfortunate,
277 # The fact that we have to clear the user's selection is unfortunate,
272 # but the alternative--trying to prevent Qt from using its hardwired
278 # but the alternative--trying to prevent Qt from using its hardwired
273 # drag logic and writing our own--is worse.
279 # drag logic and writing our own--is worse.
274 elif etype == QtCore.QEvent.DragEnter and \
280 elif etype == QtCore.QEvent.DragEnter and \
275 obj == self._control.viewport() and \
281 obj == self._control.viewport() and \
276 event.source() == self._control.viewport():
282 event.source() == self._control.viewport():
277 self._filter_drag = True
283 self._filter_drag = True
278 elif etype == QtCore.QEvent.DragLeave and \
284 elif etype == QtCore.QEvent.DragLeave and \
279 obj == self._control.viewport() and \
285 obj == self._control.viewport() and \
280 self._filter_drag:
286 self._filter_drag:
281 cursor = self._control.textCursor()
287 cursor = self._control.textCursor()
282 cursor.clearSelection()
288 cursor.clearSelection()
283 self._control.setTextCursor(cursor)
289 self._control.setTextCursor(cursor)
284 self._filter_drag = False
290 self._filter_drag = False
285
291
286 # Ensure that drops are safe.
292 # Ensure that drops are safe.
287 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
293 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
288 cursor = self._control.cursorForPosition(event.pos())
294 cursor = self._control.cursorForPosition(event.pos())
289 if self._in_buffer(cursor.position()):
295 if self._in_buffer(cursor.position()):
290 text = event.mimeData().text()
296 text = event.mimeData().text()
291 self._insert_plain_text_into_buffer(cursor, text)
297 self._insert_plain_text_into_buffer(cursor, text)
292
298
293 # Qt is expecting to get something here--drag and drop occurs in its
299 # Qt is expecting to get something here--drag and drop occurs in its
294 # own event loop. Send a DragLeave event to end it.
300 # own event loop. Send a DragLeave event to end it.
295 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
301 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
296 return True
302 return True
297
303
298 return super(ConsoleWidget, self).eventFilter(obj, event)
304 return super(ConsoleWidget, self).eventFilter(obj, event)
299
305
300 #---------------------------------------------------------------------------
306 #---------------------------------------------------------------------------
301 # 'QWidget' interface
307 # 'QWidget' interface
302 #---------------------------------------------------------------------------
308 #---------------------------------------------------------------------------
303
309
304 def sizeHint(self):
310 def sizeHint(self):
305 """ Reimplemented to suggest a size that is 80 characters wide and
311 """ Reimplemented to suggest a size that is 80 characters wide and
306 25 lines high.
312 25 lines high.
307 """
313 """
308 font_metrics = QtGui.QFontMetrics(self.font)
314 font_metrics = QtGui.QFontMetrics(self.font)
309 margin = (self._control.frameWidth() +
315 margin = (self._control.frameWidth() +
310 self._control.document().documentMargin()) * 2
316 self._control.document().documentMargin()) * 2
311 style = self.style()
317 style = self.style()
312 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
318 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
313
319
314 # Note 1: Despite my best efforts to take the various margins into
320 # Note 1: Despite my best efforts to take the various margins into
315 # account, the width is still coming out a bit too small, so we include
321 # account, the width is still coming out a bit too small, so we include
316 # a fudge factor of one character here.
322 # a fudge factor of one character here.
317 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
323 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
318 # to a Qt bug on certain Mac OS systems where it returns 0.
324 # to a Qt bug on certain Mac OS systems where it returns 0.
319 width = font_metrics.width(' ') * 81 + margin
325 width = font_metrics.width(' ') * 81 + margin
320 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
326 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
321 if self.paging == 'hsplit':
327 if self.paging == 'hsplit':
322 width = width * 2 + splitwidth
328 width = width * 2 + splitwidth
323
329
324 height = font_metrics.height() * 25 + margin
330 height = font_metrics.height() * 25 + margin
325 if self.paging == 'vsplit':
331 if self.paging == 'vsplit':
326 height = height * 2 + splitwidth
332 height = height * 2 + splitwidth
327
333
328 return QtCore.QSize(width, height)
334 return QtCore.QSize(width, height)
329
335
330 #---------------------------------------------------------------------------
336 #---------------------------------------------------------------------------
331 # 'ConsoleWidget' public interface
337 # 'ConsoleWidget' public interface
332 #---------------------------------------------------------------------------
338 #---------------------------------------------------------------------------
333
339
334 def can_copy(self):
340 def can_copy(self):
335 """ Returns whether text can be copied to the clipboard.
341 """ Returns whether text can be copied to the clipboard.
336 """
342 """
337 return self._control.textCursor().hasSelection()
343 return self._control.textCursor().hasSelection()
338
344
339 def can_cut(self):
345 def can_cut(self):
340 """ Returns whether text can be cut to the clipboard.
346 """ Returns whether text can be cut to the clipboard.
341 """
347 """
342 cursor = self._control.textCursor()
348 cursor = self._control.textCursor()
343 return (cursor.hasSelection() and
349 return (cursor.hasSelection() and
344 self._in_buffer(cursor.anchor()) and
350 self._in_buffer(cursor.anchor()) and
345 self._in_buffer(cursor.position()))
351 self._in_buffer(cursor.position()))
346
352
347 def can_paste(self):
353 def can_paste(self):
348 """ Returns whether text can be pasted from the clipboard.
354 """ Returns whether text can be pasted from the clipboard.
349 """
355 """
350 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
356 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
351 return bool(QtGui.QApplication.clipboard().text())
357 return bool(QtGui.QApplication.clipboard().text())
352 return False
358 return False
353
359
354 def clear(self, keep_input=True):
360 def clear(self, keep_input=True):
355 """ Clear the console.
361 """ Clear the console.
356
362
357 Parameters:
363 Parameters:
358 -----------
364 -----------
359 keep_input : bool, optional (default True)
365 keep_input : bool, optional (default True)
360 If set, restores the old input buffer if a new prompt is written.
366 If set, restores the old input buffer if a new prompt is written.
361 """
367 """
362 if self._executing:
368 if self._executing:
363 self._control.clear()
369 self._control.clear()
364 else:
370 else:
365 if keep_input:
371 if keep_input:
366 input_buffer = self.input_buffer
372 input_buffer = self.input_buffer
367 self._control.clear()
373 self._control.clear()
368 self._show_prompt()
374 self._show_prompt()
369 if keep_input:
375 if keep_input:
370 self.input_buffer = input_buffer
376 self.input_buffer = input_buffer
371
377
372 def copy(self):
378 def copy(self):
373 """ Copy the currently selected text to the clipboard.
379 """ Copy the currently selected text to the clipboard.
374 """
380 """
375 self._control.copy()
381 self._control.copy()
376
382
377 def cut(self):
383 def cut(self):
378 """ Copy the currently selected text to the clipboard and delete it
384 """ Copy the currently selected text to the clipboard and delete it
379 if it's inside the input buffer.
385 if it's inside the input buffer.
380 """
386 """
381 self.copy()
387 self.copy()
382 if self.can_cut():
388 if self.can_cut():
383 self._control.textCursor().removeSelectedText()
389 self._control.textCursor().removeSelectedText()
384
390
385 def execute(self, source=None, hidden=False, interactive=False):
391 def execute(self, source=None, hidden=False, interactive=False):
386 """ Executes source or the input buffer, possibly prompting for more
392 """ Executes source or the input buffer, possibly prompting for more
387 input.
393 input.
388
394
389 Parameters:
395 Parameters:
390 -----------
396 -----------
391 source : str, optional
397 source : str, optional
392
398
393 The source to execute. If not specified, the input buffer will be
399 The source to execute. If not specified, the input buffer will be
394 used. If specified and 'hidden' is False, the input buffer will be
400 used. If specified and 'hidden' is False, the input buffer will be
395 replaced with the source before execution.
401 replaced with the source before execution.
396
402
397 hidden : bool, optional (default False)
403 hidden : bool, optional (default False)
398
404
399 If set, no output will be shown and the prompt will not be modified.
405 If set, no output will be shown and the prompt will not be modified.
400 In other words, it will be completely invisible to the user that
406 In other words, it will be completely invisible to the user that
401 an execution has occurred.
407 an execution has occurred.
402
408
403 interactive : bool, optional (default False)
409 interactive : bool, optional (default False)
404
410
405 Whether the console is to treat the source as having been manually
411 Whether the console is to treat the source as having been manually
406 entered by the user. The effect of this parameter depends on the
412 entered by the user. The effect of this parameter depends on the
407 subclass implementation.
413 subclass implementation.
408
414
409 Raises:
415 Raises:
410 -------
416 -------
411 RuntimeError
417 RuntimeError
412 If incomplete input is given and 'hidden' is True. In this case,
418 If incomplete input is given and 'hidden' is True. In this case,
413 it is not possible to prompt for more input.
419 it is not possible to prompt for more input.
414
420
415 Returns:
421 Returns:
416 --------
422 --------
417 A boolean indicating whether the source was executed.
423 A boolean indicating whether the source was executed.
418 """
424 """
419 # WARNING: The order in which things happen here is very particular, in
425 # WARNING: The order in which things happen here is very particular, in
420 # large part because our syntax highlighting is fragile. If you change
426 # large part because our syntax highlighting is fragile. If you change
421 # something, test carefully!
427 # something, test carefully!
422
428
423 # Decide what to execute.
429 # Decide what to execute.
424 if source is None:
430 if source is None:
425 source = self.input_buffer
431 source = self.input_buffer
426 if not hidden:
432 if not hidden:
427 # A newline is appended later, but it should be considered part
433 # A newline is appended later, but it should be considered part
428 # of the input buffer.
434 # of the input buffer.
429 source += '\n'
435 source += '\n'
430 elif not hidden:
436 elif not hidden:
431 self.input_buffer = source
437 self.input_buffer = source
432
438
433 # Execute the source or show a continuation prompt if it is incomplete.
439 # Execute the source or show a continuation prompt if it is incomplete.
434 complete = self._is_complete(source, interactive)
440 complete = self._is_complete(source, interactive)
435 if hidden:
441 if hidden:
436 if complete:
442 if complete:
437 self._execute(source, hidden)
443 self._execute(source, hidden)
438 else:
444 else:
439 error = 'Incomplete noninteractive input: "%s"'
445 error = 'Incomplete noninteractive input: "%s"'
440 raise RuntimeError(error % source)
446 raise RuntimeError(error % source)
441 else:
447 else:
442 if complete:
448 if complete:
443 self._append_plain_text('\n')
449 self._append_plain_text('\n')
444 self._input_buffer_executing = self.input_buffer
450 self._input_buffer_executing = self.input_buffer
445 self._executing = True
451 self._executing = True
446 self._prompt_finished()
452 self._prompt_finished()
447
453
448 # The maximum block count is only in effect during execution.
454 # The maximum block count is only in effect during execution.
449 # This ensures that _prompt_pos does not become invalid due to
455 # This ensures that _prompt_pos does not become invalid due to
450 # text truncation.
456 # text truncation.
451 self._control.document().setMaximumBlockCount(self.buffer_size)
457 self._control.document().setMaximumBlockCount(self.buffer_size)
452
458
453 # Setting a positive maximum block count will automatically
459 # Setting a positive maximum block count will automatically
454 # disable the undo/redo history, but just to be safe:
460 # disable the undo/redo history, but just to be safe:
455 self._control.setUndoRedoEnabled(False)
461 self._control.setUndoRedoEnabled(False)
456
462
457 # Perform actual execution.
463 # Perform actual execution.
458 self._execute(source, hidden)
464 self._execute(source, hidden)
459
465
460 else:
466 else:
461 # Do this inside an edit block so continuation prompts are
467 # Do this inside an edit block so continuation prompts are
462 # removed seamlessly via undo/redo.
468 # removed seamlessly via undo/redo.
463 cursor = self._get_end_cursor()
469 cursor = self._get_end_cursor()
464 cursor.beginEditBlock()
470 cursor.beginEditBlock()
465 cursor.insertText('\n')
471 cursor.insertText('\n')
466 self._insert_continuation_prompt(cursor)
472 self._insert_continuation_prompt(cursor)
467 cursor.endEditBlock()
473 cursor.endEditBlock()
468
474
469 # Do not do this inside the edit block. It works as expected
475 # Do not do this inside the edit block. It works as expected
470 # when using a QPlainTextEdit control, but does not have an
476 # when using a QPlainTextEdit control, but does not have an
471 # effect when using a QTextEdit. I believe this is a Qt bug.
477 # effect when using a QTextEdit. I believe this is a Qt bug.
472 self._control.moveCursor(QtGui.QTextCursor.End)
478 self._control.moveCursor(QtGui.QTextCursor.End)
473
479
474 return complete
480 return complete
475
481
476 def export_html(self):
482 def export_html(self):
477 """ Shows a dialog to export HTML/XML in various formats.
483 """ Shows a dialog to export HTML/XML in various formats.
478 """
484 """
479 self._html_exporter.export()
485 self._html_exporter.export()
480
486
481 def _get_input_buffer(self):
487 def _get_input_buffer(self):
482 """ The text that the user has entered entered at the current prompt.
488 """ The text that the user has entered entered at the current prompt.
483
489
484 If the console is currently executing, the text that is executing will
490 If the console is currently executing, the text that is executing will
485 always be returned.
491 always be returned.
486 """
492 """
487 # If we're executing, the input buffer may not even exist anymore due to
493 # If we're executing, the input buffer may not even exist anymore due to
488 # the limit imposed by 'buffer_size'. Therefore, we store it.
494 # the limit imposed by 'buffer_size'. Therefore, we store it.
489 if self._executing:
495 if self._executing:
490 return self._input_buffer_executing
496 return self._input_buffer_executing
491
497
492 cursor = self._get_end_cursor()
498 cursor = self._get_end_cursor()
493 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
499 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
494 input_buffer = cursor.selection().toPlainText()
500 input_buffer = cursor.selection().toPlainText()
495
501
496 # Strip out continuation prompts.
502 # Strip out continuation prompts.
497 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
503 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
498
504
499 def _set_input_buffer(self, string):
505 def _set_input_buffer(self, string):
500 """ Sets the text in the input buffer.
506 """ Sets the text in the input buffer.
501
507
502 If the console is currently executing, this call has no *immediate*
508 If the console is currently executing, this call has no *immediate*
503 effect. When the execution is finished, the input buffer will be updated
509 effect. When the execution is finished, the input buffer will be updated
504 appropriately.
510 appropriately.
505 """
511 """
506 # If we're executing, store the text for later.
512 # If we're executing, store the text for later.
507 if self._executing:
513 if self._executing:
508 self._input_buffer_pending = string
514 self._input_buffer_pending = string
509 return
515 return
510
516
511 # Remove old text.
517 # Remove old text.
512 cursor = self._get_end_cursor()
518 cursor = self._get_end_cursor()
513 cursor.beginEditBlock()
519 cursor.beginEditBlock()
514 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
520 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
515 cursor.removeSelectedText()
521 cursor.removeSelectedText()
516
522
517 # Insert new text with continuation prompts.
523 # Insert new text with continuation prompts.
518 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
524 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
519 cursor.endEditBlock()
525 cursor.endEditBlock()
520 self._control.moveCursor(QtGui.QTextCursor.End)
526 self._control.moveCursor(QtGui.QTextCursor.End)
521
527
522 input_buffer = property(_get_input_buffer, _set_input_buffer)
528 input_buffer = property(_get_input_buffer, _set_input_buffer)
523
529
524 def _get_font(self):
530 def _get_font(self):
525 """ The base font being used by the ConsoleWidget.
531 """ The base font being used by the ConsoleWidget.
526 """
532 """
527 return self._control.document().defaultFont()
533 return self._control.document().defaultFont()
528
534
529 def _set_font(self, font):
535 def _set_font(self, font):
530 """ Sets the base font for the ConsoleWidget to the specified QFont.
536 """ Sets the base font for the ConsoleWidget to the specified QFont.
531 """
537 """
532 font_metrics = QtGui.QFontMetrics(font)
538 font_metrics = QtGui.QFontMetrics(font)
533 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
539 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
534
540
535 self._completion_widget.setFont(font)
541 self._completion_widget.setFont(font)
536 self._control.document().setDefaultFont(font)
542 self._control.document().setDefaultFont(font)
537 if self._page_control:
543 if self._page_control:
538 self._page_control.document().setDefaultFont(font)
544 self._page_control.document().setDefaultFont(font)
539
545
540 self.font_changed.emit(font)
546 self.font_changed.emit(font)
541
547
542 font = property(_get_font, _set_font)
548 font = property(_get_font, _set_font)
543
549
544 def paste(self, mode=QtGui.QClipboard.Clipboard):
550 def paste(self, mode=QtGui.QClipboard.Clipboard):
545 """ Paste the contents of the clipboard into the input region.
551 """ Paste the contents of the clipboard into the input region.
546
552
547 Parameters:
553 Parameters:
548 -----------
554 -----------
549 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
555 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
550
556
551 Controls which part of the system clipboard is used. This can be
557 Controls which part of the system clipboard is used. This can be
552 used to access the selection clipboard in X11 and the Find buffer
558 used to access the selection clipboard in X11 and the Find buffer
553 in Mac OS. By default, the regular clipboard is used.
559 in Mac OS. By default, the regular clipboard is used.
554 """
560 """
555 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
561 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
556 # Make sure the paste is safe.
562 # Make sure the paste is safe.
557 self._keep_cursor_in_buffer()
563 self._keep_cursor_in_buffer()
558 cursor = self._control.textCursor()
564 cursor = self._control.textCursor()
559
565
560 # Remove any trailing newline, which confuses the GUI and forces the
566 # Remove any trailing newline, which confuses the GUI and forces the
561 # user to backspace.
567 # user to backspace.
562 text = QtGui.QApplication.clipboard().text(mode).rstrip()
568 text = QtGui.QApplication.clipboard().text(mode).rstrip()
563 self._insert_plain_text_into_buffer(cursor, dedent(text))
569 self._insert_plain_text_into_buffer(cursor, dedent(text))
564
570
565 def print_(self, printer = None):
571 def print_(self, printer = None):
566 """ Print the contents of the ConsoleWidget to the specified QPrinter.
572 """ Print the contents of the ConsoleWidget to the specified QPrinter.
567 """
573 """
568 if (not printer):
574 if (not printer):
569 printer = QtGui.QPrinter()
575 printer = QtGui.QPrinter()
570 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
576 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
571 return
577 return
572 self._control.print_(printer)
578 self._control.print_(printer)
573
579
574 def prompt_to_top(self):
580 def prompt_to_top(self):
575 """ Moves the prompt to the top of the viewport.
581 """ Moves the prompt to the top of the viewport.
576 """
582 """
577 if not self._executing:
583 if not self._executing:
578 prompt_cursor = self._get_prompt_cursor()
584 prompt_cursor = self._get_prompt_cursor()
579 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
585 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
580 self._set_cursor(prompt_cursor)
586 self._set_cursor(prompt_cursor)
581 self._set_top_cursor(prompt_cursor)
587 self._set_top_cursor(prompt_cursor)
582
588
583 def redo(self):
589 def redo(self):
584 """ Redo the last operation. If there is no operation to redo, nothing
590 """ Redo the last operation. If there is no operation to redo, nothing
585 happens.
591 happens.
586 """
592 """
587 self._control.redo()
593 self._control.redo()
588
594
589 def reset_font(self):
595 def reset_font(self):
590 """ Sets the font to the default fixed-width font for this platform.
596 """ Sets the font to the default fixed-width font for this platform.
591 """
597 """
592 if sys.platform == 'win32':
598 if sys.platform == 'win32':
593 # Consolas ships with Vista/Win7, fallback to Courier if needed
599 # Consolas ships with Vista/Win7, fallback to Courier if needed
594 family, fallback = 'Consolas', 'Courier'
600 family, fallback = 'Consolas', 'Courier'
595 elif sys.platform == 'darwin':
601 elif sys.platform == 'darwin':
596 # OSX always has Monaco, no need for a fallback
602 # OSX always has Monaco, no need for a fallback
597 family, fallback = 'Monaco', None
603 family, fallback = 'Monaco', None
598 else:
604 else:
599 # FIXME: remove Consolas as a default on Linux once our font
605 # FIXME: remove Consolas as a default on Linux once our font
600 # selections are configurable by the user.
606 # selections are configurable by the user.
601 family, fallback = 'Consolas', 'Monospace'
607 family, fallback = 'Consolas', 'Monospace'
602 font = get_font(family, fallback)
608 font = get_font(family, fallback)
603 font.setPointSize(QtGui.qApp.font().pointSize())
609 font.setPointSize(QtGui.qApp.font().pointSize())
604 font.setStyleHint(QtGui.QFont.TypeWriter)
610 font.setStyleHint(QtGui.QFont.TypeWriter)
605 self._set_font(font)
611 self._set_font(font)
606
612
607 def change_font_size(self, delta):
613 def change_font_size(self, delta):
608 """Change the font size by the specified amount (in points).
614 """Change the font size by the specified amount (in points).
609 """
615 """
610 font = self.font
616 font = self.font
611 size = max(font.pointSize() + delta, 1) # minimum 1 point
617 size = max(font.pointSize() + delta, 1) # minimum 1 point
612 font.setPointSize(size)
618 font.setPointSize(size)
613 self._set_font(font)
619 self._set_font(font)
614
620
615 def select_all(self):
621 def select_all(self):
616 """ Selects all the text in the buffer.
622 """ Selects all the text in the buffer.
617 """
623 """
618 self._control.selectAll()
624 self._control.selectAll()
619
625
620 def _get_tab_width(self):
626 def _get_tab_width(self):
621 """ The width (in terms of space characters) for tab characters.
627 """ The width (in terms of space characters) for tab characters.
622 """
628 """
623 return self._tab_width
629 return self._tab_width
624
630
625 def _set_tab_width(self, tab_width):
631 def _set_tab_width(self, tab_width):
626 """ Sets the width (in terms of space characters) for tab characters.
632 """ Sets the width (in terms of space characters) for tab characters.
627 """
633 """
628 font_metrics = QtGui.QFontMetrics(self.font)
634 font_metrics = QtGui.QFontMetrics(self.font)
629 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
635 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
630
636
631 self._tab_width = tab_width
637 self._tab_width = tab_width
632
638
633 tab_width = property(_get_tab_width, _set_tab_width)
639 tab_width = property(_get_tab_width, _set_tab_width)
634
640
635 def undo(self):
641 def undo(self):
636 """ Undo the last operation. If there is no operation to undo, nothing
642 """ Undo the last operation. If there is no operation to undo, nothing
637 happens.
643 happens.
638 """
644 """
639 self._control.undo()
645 self._control.undo()
640
646
641 #---------------------------------------------------------------------------
647 #---------------------------------------------------------------------------
642 # 'ConsoleWidget' abstract interface
648 # 'ConsoleWidget' abstract interface
643 #---------------------------------------------------------------------------
649 #---------------------------------------------------------------------------
644
650
645 def _is_complete(self, source, interactive):
651 def _is_complete(self, source, interactive):
646 """ Returns whether 'source' can be executed. When triggered by an
652 """ Returns whether 'source' can be executed. When triggered by an
647 Enter/Return key press, 'interactive' is True; otherwise, it is
653 Enter/Return key press, 'interactive' is True; otherwise, it is
648 False.
654 False.
649 """
655 """
650 raise NotImplementedError
656 raise NotImplementedError
651
657
652 def _execute(self, source, hidden):
658 def _execute(self, source, hidden):
653 """ Execute 'source'. If 'hidden', do not show any output.
659 """ Execute 'source'. If 'hidden', do not show any output.
654 """
660 """
655 raise NotImplementedError
661 raise NotImplementedError
656
662
657 def _prompt_started_hook(self):
663 def _prompt_started_hook(self):
658 """ Called immediately after a new prompt is displayed.
664 """ Called immediately after a new prompt is displayed.
659 """
665 """
660 pass
666 pass
661
667
662 def _prompt_finished_hook(self):
668 def _prompt_finished_hook(self):
663 """ Called immediately after a prompt is finished, i.e. when some input
669 """ Called immediately after a prompt is finished, i.e. when some input
664 will be processed and a new prompt displayed.
670 will be processed and a new prompt displayed.
665 """
671 """
666 pass
672 pass
667
673
668 def _up_pressed(self, shift_modifier):
674 def _up_pressed(self, shift_modifier):
669 """ Called when the up key is pressed. Returns whether to continue
675 """ Called when the up key is pressed. Returns whether to continue
670 processing the event.
676 processing the event.
671 """
677 """
672 return True
678 return True
673
679
674 def _down_pressed(self, shift_modifier):
680 def _down_pressed(self, shift_modifier):
675 """ Called when the down key is pressed. Returns whether to continue
681 """ Called when the down key is pressed. Returns whether to continue
676 processing the event.
682 processing the event.
677 """
683 """
678 return True
684 return True
679
685
680 def _tab_pressed(self):
686 def _tab_pressed(self):
681 """ Called when the tab key is pressed. Returns whether to continue
687 """ Called when the tab key is pressed. Returns whether to continue
682 processing the event.
688 processing the event.
683 """
689 """
684 return False
690 return False
685
691
686 #--------------------------------------------------------------------------
692 #--------------------------------------------------------------------------
687 # 'ConsoleWidget' protected interface
693 # 'ConsoleWidget' protected interface
688 #--------------------------------------------------------------------------
694 #--------------------------------------------------------------------------
689
695
690 def _append_html(self, html):
696 def _append_html(self, html):
691 """ Appends html at the end of the console buffer.
697 """ Appends html at the end of the console buffer.
692 """
698 """
693 cursor = self._get_end_cursor()
699 cursor = self._get_end_cursor()
694 self._insert_html(cursor, html)
700 self._insert_html(cursor, html)
695
701
696 def _append_html_fetching_plain_text(self, html):
702 def _append_html_fetching_plain_text(self, html):
697 """ Appends 'html', then returns the plain text version of it.
703 """ Appends 'html', then returns the plain text version of it.
698 """
704 """
699 cursor = self._get_end_cursor()
705 cursor = self._get_end_cursor()
700 return self._insert_html_fetching_plain_text(cursor, html)
706 return self._insert_html_fetching_plain_text(cursor, html)
701
707
702 def _append_plain_text(self, text):
708 def _append_plain_text(self, text):
703 """ Appends plain text at the end of the console buffer, processing
709 """ Appends plain text at the end of the console buffer, processing
704 ANSI codes if enabled.
710 ANSI codes if enabled.
705 """
711 """
706 cursor = self._get_end_cursor()
712 cursor = self._get_end_cursor()
707 self._insert_plain_text(cursor, text)
713 self._insert_plain_text(cursor, text)
708
714
709 def _append_plain_text_keeping_prompt(self, text):
715 def _append_plain_text_keeping_prompt(self, text):
710 """ Writes 'text' after the current prompt, then restores the old prompt
716 """ Writes 'text' after the current prompt, then restores the old prompt
711 with its old input buffer.
717 with its old input buffer.
712 """
718 """
713 input_buffer = self.input_buffer
719 input_buffer = self.input_buffer
714 self._append_plain_text('\n')
720 self._append_plain_text('\n')
715 self._prompt_finished()
721 self._prompt_finished()
716
722
717 self._append_plain_text(text)
723 self._append_plain_text(text)
718 self._show_prompt()
724 self._show_prompt()
719 self.input_buffer = input_buffer
725 self.input_buffer = input_buffer
720
726
721 def _cancel_text_completion(self):
727 def _cancel_text_completion(self):
722 """ If text completion is progress, cancel it.
728 """ If text completion is progress, cancel it.
723 """
729 """
724 if self._text_completing_pos:
730 if self._text_completing_pos:
725 self._clear_temporary_buffer()
731 self._clear_temporary_buffer()
726 self._text_completing_pos = 0
732 self._text_completing_pos = 0
727
733
728 def _clear_temporary_buffer(self):
734 def _clear_temporary_buffer(self):
729 """ Clears the "temporary text" buffer, i.e. all the text following
735 """ Clears the "temporary text" buffer, i.e. all the text following
730 the prompt region.
736 the prompt region.
731 """
737 """
732 # Select and remove all text below the input buffer.
738 # Select and remove all text below the input buffer.
733 cursor = self._get_prompt_cursor()
739 cursor = self._get_prompt_cursor()
734 prompt = self._continuation_prompt.lstrip()
740 prompt = self._continuation_prompt.lstrip()
735 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
741 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
736 temp_cursor = QtGui.QTextCursor(cursor)
742 temp_cursor = QtGui.QTextCursor(cursor)
737 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
743 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
738 text = temp_cursor.selection().toPlainText().lstrip()
744 text = temp_cursor.selection().toPlainText().lstrip()
739 if not text.startswith(prompt):
745 if not text.startswith(prompt):
740 break
746 break
741 else:
747 else:
742 # We've reached the end of the input buffer and no text follows.
748 # We've reached the end of the input buffer and no text follows.
743 return
749 return
744 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
750 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
745 cursor.movePosition(QtGui.QTextCursor.End,
751 cursor.movePosition(QtGui.QTextCursor.End,
746 QtGui.QTextCursor.KeepAnchor)
752 QtGui.QTextCursor.KeepAnchor)
747 cursor.removeSelectedText()
753 cursor.removeSelectedText()
748
754
749 # After doing this, we have no choice but to clear the undo/redo
755 # After doing this, we have no choice but to clear the undo/redo
750 # history. Otherwise, the text is not "temporary" at all, because it
756 # history. Otherwise, the text is not "temporary" at all, because it
751 # can be recalled with undo/redo. Unfortunately, Qt does not expose
757 # can be recalled with undo/redo. Unfortunately, Qt does not expose
752 # fine-grained control to the undo/redo system.
758 # fine-grained control to the undo/redo system.
753 if self._control.isUndoRedoEnabled():
759 if self._control.isUndoRedoEnabled():
754 self._control.setUndoRedoEnabled(False)
760 self._control.setUndoRedoEnabled(False)
755 self._control.setUndoRedoEnabled(True)
761 self._control.setUndoRedoEnabled(True)
756
762
757 def _complete_with_items(self, cursor, items):
763 def _complete_with_items(self, cursor, items):
758 """ Performs completion with 'items' at the specified cursor location.
764 """ Performs completion with 'items' at the specified cursor location.
759 """
765 """
760 self._cancel_text_completion()
766 self._cancel_text_completion()
761
767
762 if len(items) == 1:
768 if len(items) == 1:
763 cursor.setPosition(self._control.textCursor().position(),
769 cursor.setPosition(self._control.textCursor().position(),
764 QtGui.QTextCursor.KeepAnchor)
770 QtGui.QTextCursor.KeepAnchor)
765 cursor.insertText(items[0])
771 cursor.insertText(items[0])
766
772
767 elif len(items) > 1:
773 elif len(items) > 1:
768 current_pos = self._control.textCursor().position()
774 current_pos = self._control.textCursor().position()
769 prefix = commonprefix(items)
775 prefix = commonprefix(items)
770 if prefix:
776 if prefix:
771 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
777 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
772 cursor.insertText(prefix)
778 cursor.insertText(prefix)
773 current_pos = cursor.position()
779 current_pos = cursor.position()
774
780
775 if self.gui_completion:
781 if self.gui_completion:
776 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
782 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
777 self._completion_widget.show_items(cursor, items)
783 self._completion_widget.show_items(cursor, items)
778 else:
784 else:
779 cursor.beginEditBlock()
785 cursor.beginEditBlock()
780 self._append_plain_text('\n')
786 self._append_plain_text('\n')
781 self._page(self._format_as_columns(items))
787 self._page(self._format_as_columns(items))
782 cursor.endEditBlock()
788 cursor.endEditBlock()
783
789
784 cursor.setPosition(current_pos)
790 cursor.setPosition(current_pos)
785 self._control.moveCursor(QtGui.QTextCursor.End)
791 self._control.moveCursor(QtGui.QTextCursor.End)
786 self._control.setTextCursor(cursor)
792 self._control.setTextCursor(cursor)
787 self._text_completing_pos = current_pos
793 self._text_completing_pos = current_pos
788
794
789 def _context_menu_make(self, pos):
795 def _context_menu_make(self, pos):
790 """ Creates a context menu for the given QPoint (in widget coordinates).
796 """ Creates a context menu for the given QPoint (in widget coordinates).
791 """
797 """
792 menu = QtGui.QMenu(self)
798 menu = QtGui.QMenu(self)
793
799
794 cut_action = menu.addAction('Cut', self.cut)
800 cut_action = menu.addAction('Cut', self.cut)
795 cut_action.setEnabled(self.can_cut())
801 cut_action.setEnabled(self.can_cut())
796 cut_action.setShortcut(QtGui.QKeySequence.Cut)
802 cut_action.setShortcut(QtGui.QKeySequence.Cut)
797
803
798 copy_action = menu.addAction('Copy', self.copy)
804 copy_action = menu.addAction('Copy', self.copy)
799 copy_action.setEnabled(self.can_copy())
805 copy_action.setEnabled(self.can_copy())
800 copy_action.setShortcut(QtGui.QKeySequence.Copy)
806 copy_action.setShortcut(QtGui.QKeySequence.Copy)
801
807
802 paste_action = menu.addAction('Paste', self.paste)
808 paste_action = menu.addAction('Paste', self.paste)
803 paste_action.setEnabled(self.can_paste())
809 paste_action.setEnabled(self.can_paste())
804 paste_action.setShortcut(QtGui.QKeySequence.Paste)
810 paste_action.setShortcut(QtGui.QKeySequence.Paste)
805
811
806 menu.addSeparator()
812 menu.addSeparator()
807 menu.addAction(self._select_all_action)
813 menu.addAction(self._select_all_action)
808
814
809 menu.addSeparator()
815 menu.addSeparator()
810 menu.addAction(self._export_action)
816 menu.addAction(self._export_action)
811 menu.addAction(self._print_action)
817 menu.addAction(self._print_action)
812
818
813 return menu
819 return menu
814
820
815 def _control_key_down(self, modifiers, include_command=False):
821 def _control_key_down(self, modifiers, include_command=False):
816 """ Given a KeyboardModifiers flags object, return whether the Control
822 """ Given a KeyboardModifiers flags object, return whether the Control
817 key is down.
823 key is down.
818
824
819 Parameters:
825 Parameters:
820 -----------
826 -----------
821 include_command : bool, optional (default True)
827 include_command : bool, optional (default True)
822 Whether to treat the Command key as a (mutually exclusive) synonym
828 Whether to treat the Command key as a (mutually exclusive) synonym
823 for Control when in Mac OS.
829 for Control when in Mac OS.
824 """
830 """
825 # Note that on Mac OS, ControlModifier corresponds to the Command key
831 # Note that on Mac OS, ControlModifier corresponds to the Command key
826 # while MetaModifier corresponds to the Control key.
832 # while MetaModifier corresponds to the Control key.
827 if sys.platform == 'darwin':
833 if sys.platform == 'darwin':
828 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
834 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
829 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
835 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
830 else:
836 else:
831 return bool(modifiers & QtCore.Qt.ControlModifier)
837 return bool(modifiers & QtCore.Qt.ControlModifier)
832
838
833 def _create_control(self):
839 def _create_control(self):
834 """ Creates and connects the underlying text widget.
840 """ Creates and connects the underlying text widget.
835 """
841 """
836 # Create the underlying control.
842 # Create the underlying control.
837 if self.kind == 'plain':
843 if self.kind == 'plain':
838 control = QtGui.QPlainTextEdit()
844 control = QtGui.QPlainTextEdit()
839 elif self.kind == 'rich':
845 elif self.kind == 'rich':
840 control = QtGui.QTextEdit()
846 control = QtGui.QTextEdit()
841 control.setAcceptRichText(False)
847 control.setAcceptRichText(False)
842
848
843 # Install event filters. The filter on the viewport is needed for
849 # Install event filters. The filter on the viewport is needed for
844 # mouse events and drag events.
850 # mouse events and drag events.
845 control.installEventFilter(self)
851 control.installEventFilter(self)
846 control.viewport().installEventFilter(self)
852 control.viewport().installEventFilter(self)
847
853
848 # Connect signals.
854 # Connect signals.
849 control.cursorPositionChanged.connect(self._cursor_position_changed)
855 control.cursorPositionChanged.connect(self._cursor_position_changed)
850 control.customContextMenuRequested.connect(
856 control.customContextMenuRequested.connect(
851 self._custom_context_menu_requested)
857 self._custom_context_menu_requested)
852 control.copyAvailable.connect(self.copy_available)
858 control.copyAvailable.connect(self.copy_available)
853 control.redoAvailable.connect(self.redo_available)
859 control.redoAvailable.connect(self.redo_available)
854 control.undoAvailable.connect(self.undo_available)
860 control.undoAvailable.connect(self.undo_available)
855
861
856 # Hijack the document size change signal to prevent Qt from adjusting
862 # Hijack the document size change signal to prevent Qt from adjusting
857 # the viewport's scrollbar. We are relying on an implementation detail
863 # the viewport's scrollbar. We are relying on an implementation detail
858 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
864 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
859 # this functionality we cannot create a nice terminal interface.
865 # this functionality we cannot create a nice terminal interface.
860 layout = control.document().documentLayout()
866 layout = control.document().documentLayout()
861 layout.documentSizeChanged.disconnect()
867 layout.documentSizeChanged.disconnect()
862 layout.documentSizeChanged.connect(self._adjust_scrollbars)
868 layout.documentSizeChanged.connect(self._adjust_scrollbars)
863
869
864 # Configure the control.
870 # Configure the control.
865 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
871 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
866 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
872 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
867 control.setReadOnly(True)
873 control.setReadOnly(True)
868 control.setUndoRedoEnabled(False)
874 control.setUndoRedoEnabled(False)
869 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
875 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
870 return control
876 return control
871
877
872 def _create_page_control(self):
878 def _create_page_control(self):
873 """ Creates and connects the underlying paging widget.
879 """ Creates and connects the underlying paging widget.
874 """
880 """
875 if self.kind == 'plain':
881 if self.kind == 'plain':
876 control = QtGui.QPlainTextEdit()
882 control = QtGui.QPlainTextEdit()
877 elif self.kind == 'rich':
883 elif self.kind == 'rich':
878 control = QtGui.QTextEdit()
884 control = QtGui.QTextEdit()
879 control.installEventFilter(self)
885 control.installEventFilter(self)
880 control.setReadOnly(True)
886 control.setReadOnly(True)
881 control.setUndoRedoEnabled(False)
887 control.setUndoRedoEnabled(False)
882 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
888 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
883 return control
889 return control
884
890
885 def _event_filter_console_keypress(self, event):
891 def _event_filter_console_keypress(self, event):
886 """ Filter key events for the underlying text widget to create a
892 """ Filter key events for the underlying text widget to create a
887 console-like interface.
893 console-like interface.
888 """
894 """
889 intercepted = False
895 intercepted = False
890 cursor = self._control.textCursor()
896 cursor = self._control.textCursor()
891 position = cursor.position()
897 position = cursor.position()
892 key = event.key()
898 key = event.key()
893 ctrl_down = self._control_key_down(event.modifiers())
899 ctrl_down = self._control_key_down(event.modifiers())
894 alt_down = event.modifiers() & QtCore.Qt.AltModifier
900 alt_down = event.modifiers() & QtCore.Qt.AltModifier
895 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
901 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
896
902
897 #------ Special sequences ----------------------------------------------
903 #------ Special sequences ----------------------------------------------
898
904
899 if event.matches(QtGui.QKeySequence.Copy):
905 if event.matches(QtGui.QKeySequence.Copy):
900 self.copy()
906 self.copy()
901 intercepted = True
907 intercepted = True
902
908
903 elif event.matches(QtGui.QKeySequence.Cut):
909 elif event.matches(QtGui.QKeySequence.Cut):
904 self.cut()
910 self.cut()
905 intercepted = True
911 intercepted = True
906
912
907 elif event.matches(QtGui.QKeySequence.Paste):
913 elif event.matches(QtGui.QKeySequence.Paste):
908 self.paste()
914 self.paste()
909 intercepted = True
915 intercepted = True
910
916
911 #------ Special modifier logic -----------------------------------------
917 #------ Special modifier logic -----------------------------------------
912
918
913 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
919 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
914 intercepted = True
920 intercepted = True
915
921
916 # Special handling when tab completing in text mode.
922 # Special handling when tab completing in text mode.
917 self._cancel_text_completion()
923 self._cancel_text_completion()
918
924
919 if self._in_buffer(position):
925 if self._in_buffer(position):
920 if self._reading:
926 if self._reading:
921 self._append_plain_text('\n')
927 self._append_plain_text('\n')
922 self._reading = False
928 self._reading = False
923 if self._reading_callback:
929 if self._reading_callback:
924 self._reading_callback()
930 self._reading_callback()
925
931
926 # If the input buffer is a single line or there is only
932 # If the input buffer is a single line or there is only
927 # whitespace after the cursor, execute. Otherwise, split the
933 # whitespace after the cursor, execute. Otherwise, split the
928 # line with a continuation prompt.
934 # line with a continuation prompt.
929 elif not self._executing:
935 elif not self._executing:
930 cursor.movePosition(QtGui.QTextCursor.End,
936 cursor.movePosition(QtGui.QTextCursor.End,
931 QtGui.QTextCursor.KeepAnchor)
937 QtGui.QTextCursor.KeepAnchor)
932 at_end = len(cursor.selectedText().strip()) == 0
938 at_end = len(cursor.selectedText().strip()) == 0
933 single_line = (self._get_end_cursor().blockNumber() ==
939 single_line = (self._get_end_cursor().blockNumber() ==
934 self._get_prompt_cursor().blockNumber())
940 self._get_prompt_cursor().blockNumber())
935 if (at_end or shift_down or single_line) and not ctrl_down:
941 if (at_end or shift_down or single_line) and not ctrl_down:
936 self.execute(interactive = not shift_down)
942 self.execute(interactive = not shift_down)
937 else:
943 else:
938 # Do this inside an edit block for clean undo/redo.
944 # Do this inside an edit block for clean undo/redo.
939 cursor.beginEditBlock()
945 cursor.beginEditBlock()
940 cursor.setPosition(position)
946 cursor.setPosition(position)
941 cursor.insertText('\n')
947 cursor.insertText('\n')
942 self._insert_continuation_prompt(cursor)
948 self._insert_continuation_prompt(cursor)
943 cursor.endEditBlock()
949 cursor.endEditBlock()
944
950
945 # Ensure that the whole input buffer is visible.
951 # Ensure that the whole input buffer is visible.
946 # FIXME: This will not be usable if the input buffer is
952 # FIXME: This will not be usable if the input buffer is
947 # taller than the console widget.
953 # taller than the console widget.
948 self._control.moveCursor(QtGui.QTextCursor.End)
954 self._control.moveCursor(QtGui.QTextCursor.End)
949 self._control.setTextCursor(cursor)
955 self._control.setTextCursor(cursor)
950
956
951 #------ Control/Cmd modifier -------------------------------------------
957 #------ Control/Cmd modifier -------------------------------------------
952
958
953 elif ctrl_down:
959 elif ctrl_down:
954 if key == QtCore.Qt.Key_G:
960 if key == QtCore.Qt.Key_G:
955 self._keyboard_quit()
961 self._keyboard_quit()
956 intercepted = True
962 intercepted = True
957
963
958 elif key == QtCore.Qt.Key_K:
964 elif key == QtCore.Qt.Key_K:
959 if self._in_buffer(position):
965 if self._in_buffer(position):
960 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
966 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
961 QtGui.QTextCursor.KeepAnchor)
967 QtGui.QTextCursor.KeepAnchor)
962 if not cursor.hasSelection():
968 if not cursor.hasSelection():
963 # Line deletion (remove continuation prompt)
969 # Line deletion (remove continuation prompt)
964 cursor.movePosition(QtGui.QTextCursor.NextBlock,
970 cursor.movePosition(QtGui.QTextCursor.NextBlock,
965 QtGui.QTextCursor.KeepAnchor)
971 QtGui.QTextCursor.KeepAnchor)
966 cursor.movePosition(QtGui.QTextCursor.Right,
972 cursor.movePosition(QtGui.QTextCursor.Right,
967 QtGui.QTextCursor.KeepAnchor,
973 QtGui.QTextCursor.KeepAnchor,
968 len(self._continuation_prompt))
974 len(self._continuation_prompt))
969 self._kill_ring.kill_cursor(cursor)
975 self._kill_ring.kill_cursor(cursor)
970 intercepted = True
976 intercepted = True
971
977
972 elif key == QtCore.Qt.Key_L:
978 elif key == QtCore.Qt.Key_L:
973 self.prompt_to_top()
979 self.prompt_to_top()
974 intercepted = True
980 intercepted = True
975
981
976 elif key == QtCore.Qt.Key_O:
982 elif key == QtCore.Qt.Key_O:
977 if self._page_control and self._page_control.isVisible():
983 if self._page_control and self._page_control.isVisible():
978 self._page_control.setFocus()
984 self._page_control.setFocus()
979 intercepted = True
985 intercepted = True
980
986
981 elif key == QtCore.Qt.Key_U:
987 elif key == QtCore.Qt.Key_U:
982 if self._in_buffer(position):
988 if self._in_buffer(position):
983 start_line = cursor.blockNumber()
989 start_line = cursor.blockNumber()
984 if start_line == self._get_prompt_cursor().blockNumber():
990 if start_line == self._get_prompt_cursor().blockNumber():
985 offset = len(self._prompt)
991 offset = len(self._prompt)
986 else:
992 else:
987 offset = len(self._continuation_prompt)
993 offset = len(self._continuation_prompt)
988 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
994 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
989 QtGui.QTextCursor.KeepAnchor)
995 QtGui.QTextCursor.KeepAnchor)
990 cursor.movePosition(QtGui.QTextCursor.Right,
996 cursor.movePosition(QtGui.QTextCursor.Right,
991 QtGui.QTextCursor.KeepAnchor, offset)
997 QtGui.QTextCursor.KeepAnchor, offset)
992 self._kill_ring.kill_cursor(cursor)
998 self._kill_ring.kill_cursor(cursor)
993 intercepted = True
999 intercepted = True
994
1000
995 elif key == QtCore.Qt.Key_Y:
1001 elif key == QtCore.Qt.Key_Y:
996 self._keep_cursor_in_buffer()
1002 self._keep_cursor_in_buffer()
997 self._kill_ring.yank()
1003 self._kill_ring.yank()
998 intercepted = True
1004 intercepted = True
999
1005
1000 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1006 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1001 intercepted = True
1007 intercepted = True
1002
1008
1003 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1009 elif key in (QtCore.Qt.Key_Plus, QtCore.Qt.Key_Equal):
1004 self.change_font_size(1)
1010 self.change_font_size(1)
1005 intercepted = True
1011 intercepted = True
1006
1012
1007 elif key == QtCore.Qt.Key_Minus:
1013 elif key == QtCore.Qt.Key_Minus:
1008 self.change_font_size(-1)
1014 self.change_font_size(-1)
1009 intercepted = True
1015 intercepted = True
1010
1016
1011 elif key == QtCore.Qt.Key_0:
1017 elif key == QtCore.Qt.Key_0:
1012 self.reset_font()
1018 self.reset_font()
1013 intercepted = True
1019 intercepted = True
1014
1020
1015 #------ Alt modifier ---------------------------------------------------
1021 #------ Alt modifier ---------------------------------------------------
1016
1022
1017 elif alt_down:
1023 elif alt_down:
1018 if key == QtCore.Qt.Key_B:
1024 if key == QtCore.Qt.Key_B:
1019 self._set_cursor(self._get_word_start_cursor(position))
1025 self._set_cursor(self._get_word_start_cursor(position))
1020 intercepted = True
1026 intercepted = True
1021
1027
1022 elif key == QtCore.Qt.Key_F:
1028 elif key == QtCore.Qt.Key_F:
1023 self._set_cursor(self._get_word_end_cursor(position))
1029 self._set_cursor(self._get_word_end_cursor(position))
1024 intercepted = True
1030 intercepted = True
1025
1031
1026 elif key == QtCore.Qt.Key_Y:
1032 elif key == QtCore.Qt.Key_Y:
1027 self._kill_ring.rotate()
1033 self._kill_ring.rotate()
1028 intercepted = True
1034 intercepted = True
1029
1035
1030 elif key == QtCore.Qt.Key_Backspace:
1036 elif key == QtCore.Qt.Key_Backspace:
1031 cursor = self._get_word_start_cursor(position)
1037 cursor = self._get_word_start_cursor(position)
1032 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1038 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1033 self._kill_ring.kill_cursor(cursor)
1039 self._kill_ring.kill_cursor(cursor)
1034 intercepted = True
1040 intercepted = True
1035
1041
1036 elif key == QtCore.Qt.Key_D:
1042 elif key == QtCore.Qt.Key_D:
1037 cursor = self._get_word_end_cursor(position)
1043 cursor = self._get_word_end_cursor(position)
1038 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1044 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1039 self._kill_ring.kill_cursor(cursor)
1045 self._kill_ring.kill_cursor(cursor)
1040 intercepted = True
1046 intercepted = True
1041
1047
1042 elif key == QtCore.Qt.Key_Delete:
1048 elif key == QtCore.Qt.Key_Delete:
1043 intercepted = True
1049 intercepted = True
1044
1050
1045 elif key == QtCore.Qt.Key_Greater:
1051 elif key == QtCore.Qt.Key_Greater:
1046 self._control.moveCursor(QtGui.QTextCursor.End)
1052 self._control.moveCursor(QtGui.QTextCursor.End)
1047 intercepted = True
1053 intercepted = True
1048
1054
1049 elif key == QtCore.Qt.Key_Less:
1055 elif key == QtCore.Qt.Key_Less:
1050 self._control.setTextCursor(self._get_prompt_cursor())
1056 self._control.setTextCursor(self._get_prompt_cursor())
1051 intercepted = True
1057 intercepted = True
1052
1058
1053 #------ No modifiers ---------------------------------------------------
1059 #------ No modifiers ---------------------------------------------------
1054
1060
1055 else:
1061 else:
1056 if shift_down:
1062 if shift_down:
1057 anchormode = QtGui.QTextCursor.KeepAnchor
1063 anchormode = QtGui.QTextCursor.KeepAnchor
1058 else:
1064 else:
1059 anchormode = QtGui.QTextCursor.MoveAnchor
1065 anchormode = QtGui.QTextCursor.MoveAnchor
1060
1066
1061 if key == QtCore.Qt.Key_Escape:
1067 if key == QtCore.Qt.Key_Escape:
1062 self._keyboard_quit()
1068 self._keyboard_quit()
1063 intercepted = True
1069 intercepted = True
1064
1070
1065 elif key == QtCore.Qt.Key_Up:
1071 elif key == QtCore.Qt.Key_Up:
1066 if self._reading or not self._up_pressed(shift_down):
1072 if self._reading or not self._up_pressed(shift_down):
1067 intercepted = True
1073 intercepted = True
1068 else:
1074 else:
1069 prompt_line = self._get_prompt_cursor().blockNumber()
1075 prompt_line = self._get_prompt_cursor().blockNumber()
1070 intercepted = cursor.blockNumber() <= prompt_line
1076 intercepted = cursor.blockNumber() <= prompt_line
1071
1077
1072 elif key == QtCore.Qt.Key_Down:
1078 elif key == QtCore.Qt.Key_Down:
1073 if self._reading or not self._down_pressed(shift_down):
1079 if self._reading or not self._down_pressed(shift_down):
1074 intercepted = True
1080 intercepted = True
1075 else:
1081 else:
1076 end_line = self._get_end_cursor().blockNumber()
1082 end_line = self._get_end_cursor().blockNumber()
1077 intercepted = cursor.blockNumber() == end_line
1083 intercepted = cursor.blockNumber() == end_line
1078
1084
1079 elif key == QtCore.Qt.Key_Tab:
1085 elif key == QtCore.Qt.Key_Tab:
1080 if not self._reading:
1086 if not self._reading:
1081 intercepted = not self._tab_pressed()
1087 intercepted = not self._tab_pressed()
1082
1088
1083 elif key == QtCore.Qt.Key_Left:
1089 elif key == QtCore.Qt.Key_Left:
1084
1090
1085 # Move to the previous line
1091 # Move to the previous line
1086 line, col = cursor.blockNumber(), cursor.columnNumber()
1092 line, col = cursor.blockNumber(), cursor.columnNumber()
1087 if line > self._get_prompt_cursor().blockNumber() and \
1093 if line > self._get_prompt_cursor().blockNumber() and \
1088 col == len(self._continuation_prompt):
1094 col == len(self._continuation_prompt):
1089 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1095 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1090 mode=anchormode)
1096 mode=anchormode)
1091 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1097 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1092 mode=anchormode)
1098 mode=anchormode)
1093 intercepted = True
1099 intercepted = True
1094
1100
1095 # Regular left movement
1101 # Regular left movement
1096 else:
1102 else:
1097 intercepted = not self._in_buffer(position - 1)
1103 intercepted = not self._in_buffer(position - 1)
1098
1104
1099 elif key == QtCore.Qt.Key_Right:
1105 elif key == QtCore.Qt.Key_Right:
1100 original_block_number = cursor.blockNumber()
1106 original_block_number = cursor.blockNumber()
1101 cursor.movePosition(QtGui.QTextCursor.Right,
1107 cursor.movePosition(QtGui.QTextCursor.Right,
1102 mode=anchormode)
1108 mode=anchormode)
1103 if cursor.blockNumber() != original_block_number:
1109 if cursor.blockNumber() != original_block_number:
1104 cursor.movePosition(QtGui.QTextCursor.Right,
1110 cursor.movePosition(QtGui.QTextCursor.Right,
1105 n=len(self._continuation_prompt),
1111 n=len(self._continuation_prompt),
1106 mode=anchormode)
1112 mode=anchormode)
1107 self._set_cursor(cursor)
1113 self._set_cursor(cursor)
1108 intercepted = True
1114 intercepted = True
1109
1115
1110 elif key == QtCore.Qt.Key_Home:
1116 elif key == QtCore.Qt.Key_Home:
1111 start_line = cursor.blockNumber()
1117 start_line = cursor.blockNumber()
1112 if start_line == self._get_prompt_cursor().blockNumber():
1118 if start_line == self._get_prompt_cursor().blockNumber():
1113 start_pos = self._prompt_pos
1119 start_pos = self._prompt_pos
1114 else:
1120 else:
1115 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1121 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1116 QtGui.QTextCursor.KeepAnchor)
1122 QtGui.QTextCursor.KeepAnchor)
1117 start_pos = cursor.position()
1123 start_pos = cursor.position()
1118 start_pos += len(self._continuation_prompt)
1124 start_pos += len(self._continuation_prompt)
1119 cursor.setPosition(position)
1125 cursor.setPosition(position)
1120 if shift_down and self._in_buffer(position):
1126 if shift_down and self._in_buffer(position):
1121 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1127 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1122 else:
1128 else:
1123 cursor.setPosition(start_pos)
1129 cursor.setPosition(start_pos)
1124 self._set_cursor(cursor)
1130 self._set_cursor(cursor)
1125 intercepted = True
1131 intercepted = True
1126
1132
1127 elif key == QtCore.Qt.Key_Backspace:
1133 elif key == QtCore.Qt.Key_Backspace:
1128
1134
1129 # Line deletion (remove continuation prompt)
1135 # Line deletion (remove continuation prompt)
1130 line, col = cursor.blockNumber(), cursor.columnNumber()
1136 line, col = cursor.blockNumber(), cursor.columnNumber()
1131 if not self._reading and \
1137 if not self._reading and \
1132 col == len(self._continuation_prompt) and \
1138 col == len(self._continuation_prompt) and \
1133 line > self._get_prompt_cursor().blockNumber():
1139 line > self._get_prompt_cursor().blockNumber():
1134 cursor.beginEditBlock()
1140 cursor.beginEditBlock()
1135 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1141 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1136 QtGui.QTextCursor.KeepAnchor)
1142 QtGui.QTextCursor.KeepAnchor)
1137 cursor.removeSelectedText()
1143 cursor.removeSelectedText()
1138 cursor.deletePreviousChar()
1144 cursor.deletePreviousChar()
1139 cursor.endEditBlock()
1145 cursor.endEditBlock()
1140 intercepted = True
1146 intercepted = True
1141
1147
1142 # Regular backwards deletion
1148 # Regular backwards deletion
1143 else:
1149 else:
1144 anchor = cursor.anchor()
1150 anchor = cursor.anchor()
1145 if anchor == position:
1151 if anchor == position:
1146 intercepted = not self._in_buffer(position - 1)
1152 intercepted = not self._in_buffer(position - 1)
1147 else:
1153 else:
1148 intercepted = not self._in_buffer(min(anchor, position))
1154 intercepted = not self._in_buffer(min(anchor, position))
1149
1155
1150 elif key == QtCore.Qt.Key_Delete:
1156 elif key == QtCore.Qt.Key_Delete:
1151
1157
1152 # Line deletion (remove continuation prompt)
1158 # Line deletion (remove continuation prompt)
1153 if not self._reading and self._in_buffer(position) and \
1159 if not self._reading and self._in_buffer(position) and \
1154 cursor.atBlockEnd() and not cursor.hasSelection():
1160 cursor.atBlockEnd() and not cursor.hasSelection():
1155 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1161 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1156 QtGui.QTextCursor.KeepAnchor)
1162 QtGui.QTextCursor.KeepAnchor)
1157 cursor.movePosition(QtGui.QTextCursor.Right,
1163 cursor.movePosition(QtGui.QTextCursor.Right,
1158 QtGui.QTextCursor.KeepAnchor,
1164 QtGui.QTextCursor.KeepAnchor,
1159 len(self._continuation_prompt))
1165 len(self._continuation_prompt))
1160 cursor.removeSelectedText()
1166 cursor.removeSelectedText()
1161 intercepted = True
1167 intercepted = True
1162
1168
1163 # Regular forwards deletion:
1169 # Regular forwards deletion:
1164 else:
1170 else:
1165 anchor = cursor.anchor()
1171 anchor = cursor.anchor()
1166 intercepted = (not self._in_buffer(anchor) or
1172 intercepted = (not self._in_buffer(anchor) or
1167 not self._in_buffer(position))
1173 not self._in_buffer(position))
1168
1174
1169 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1175 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1170 # using the keyboard in any part of the buffer.
1176 # using the keyboard in any part of the buffer.
1171 if not self._control_key_down(event.modifiers(), include_command=True):
1177 if not self._control_key_down(event.modifiers(), include_command=True):
1172 self._keep_cursor_in_buffer()
1178 self._keep_cursor_in_buffer()
1173
1179
1174 return intercepted
1180 return intercepted
1175
1181
1176 def _event_filter_page_keypress(self, event):
1182 def _event_filter_page_keypress(self, event):
1177 """ Filter key events for the paging widget to create console-like
1183 """ Filter key events for the paging widget to create console-like
1178 interface.
1184 interface.
1179 """
1185 """
1180 key = event.key()
1186 key = event.key()
1181 ctrl_down = self._control_key_down(event.modifiers())
1187 ctrl_down = self._control_key_down(event.modifiers())
1182 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1188 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1183
1189
1184 if ctrl_down:
1190 if ctrl_down:
1185 if key == QtCore.Qt.Key_O:
1191 if key == QtCore.Qt.Key_O:
1186 self._control.setFocus()
1192 self._control.setFocus()
1187 intercept = True
1193 intercept = True
1188
1194
1189 elif alt_down:
1195 elif alt_down:
1190 if key == QtCore.Qt.Key_Greater:
1196 if key == QtCore.Qt.Key_Greater:
1191 self._page_control.moveCursor(QtGui.QTextCursor.End)
1197 self._page_control.moveCursor(QtGui.QTextCursor.End)
1192 intercepted = True
1198 intercepted = True
1193
1199
1194 elif key == QtCore.Qt.Key_Less:
1200 elif key == QtCore.Qt.Key_Less:
1195 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1201 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1196 intercepted = True
1202 intercepted = True
1197
1203
1198 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1204 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1199 if self._splitter:
1205 if self._splitter:
1200 self._page_control.hide()
1206 self._page_control.hide()
1201 else:
1207 else:
1202 self.layout().setCurrentWidget(self._control)
1208 self.layout().setCurrentWidget(self._control)
1203 return True
1209 return True
1204
1210
1205 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1211 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1206 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1212 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1207 QtCore.Qt.Key_PageDown,
1213 QtCore.Qt.Key_PageDown,
1208 QtCore.Qt.NoModifier)
1214 QtCore.Qt.NoModifier)
1209 QtGui.qApp.sendEvent(self._page_control, new_event)
1215 QtGui.qApp.sendEvent(self._page_control, new_event)
1210 return True
1216 return True
1211
1217
1212 elif key == QtCore.Qt.Key_Backspace:
1218 elif key == QtCore.Qt.Key_Backspace:
1213 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1219 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1214 QtCore.Qt.Key_PageUp,
1220 QtCore.Qt.Key_PageUp,
1215 QtCore.Qt.NoModifier)
1221 QtCore.Qt.NoModifier)
1216 QtGui.qApp.sendEvent(self._page_control, new_event)
1222 QtGui.qApp.sendEvent(self._page_control, new_event)
1217 return True
1223 return True
1218
1224
1219 return False
1225 return False
1220
1226
1221 def _format_as_columns(self, items, separator=' '):
1227 def _format_as_columns(self, items, separator=' '):
1222 """ Transform a list of strings into a single string with columns.
1228 """ Transform a list of strings into a single string with columns.
1223
1229
1224 Parameters
1230 Parameters
1225 ----------
1231 ----------
1226 items : sequence of strings
1232 items : sequence of strings
1227 The strings to process.
1233 The strings to process.
1228
1234
1229 separator : str, optional [default is two spaces]
1235 separator : str, optional [default is two spaces]
1230 The string that separates columns.
1236 The string that separates columns.
1231
1237
1232 Returns
1238 Returns
1233 -------
1239 -------
1234 The formatted string.
1240 The formatted string.
1235 """
1241 """
1236 # Note: this code is adapted from columnize 0.3.2.
1242 # Note: this code is adapted from columnize 0.3.2.
1237 # See http://code.google.com/p/pycolumnize/
1243 # See http://code.google.com/p/pycolumnize/
1238
1244
1239 # Calculate the number of characters available.
1245 # Calculate the number of characters available.
1240 width = self._control.viewport().width()
1246 width = self._control.viewport().width()
1241 char_width = QtGui.QFontMetrics(self.font).width(' ')
1247 char_width = QtGui.QFontMetrics(self.font).width(' ')
1242 displaywidth = max(10, (width / char_width) - 1)
1248 displaywidth = max(10, (width / char_width) - 1)
1243
1249
1244 # Some degenerate cases.
1250 # Some degenerate cases.
1245 size = len(items)
1251 size = len(items)
1246 if size == 0:
1252 if size == 0:
1247 return '\n'
1253 return '\n'
1248 elif size == 1:
1254 elif size == 1:
1249 return '%s\n' % items[0]
1255 return '%s\n' % items[0]
1250
1256
1251 # Try every row count from 1 upwards
1257 # Try every row count from 1 upwards
1252 array_index = lambda nrows, row, col: nrows*col + row
1258 array_index = lambda nrows, row, col: nrows*col + row
1253 for nrows in range(1, size):
1259 for nrows in range(1, size):
1254 ncols = (size + nrows - 1) // nrows
1260 ncols = (size + nrows - 1) // nrows
1255 colwidths = []
1261 colwidths = []
1256 totwidth = -len(separator)
1262 totwidth = -len(separator)
1257 for col in range(ncols):
1263 for col in range(ncols):
1258 # Get max column width for this column
1264 # Get max column width for this column
1259 colwidth = 0
1265 colwidth = 0
1260 for row in range(nrows):
1266 for row in range(nrows):
1261 i = array_index(nrows, row, col)
1267 i = array_index(nrows, row, col)
1262 if i >= size: break
1268 if i >= size: break
1263 x = items[i]
1269 x = items[i]
1264 colwidth = max(colwidth, len(x))
1270 colwidth = max(colwidth, len(x))
1265 colwidths.append(colwidth)
1271 colwidths.append(colwidth)
1266 totwidth += colwidth + len(separator)
1272 totwidth += colwidth + len(separator)
1267 if totwidth > displaywidth:
1273 if totwidth > displaywidth:
1268 break
1274 break
1269 if totwidth <= displaywidth:
1275 if totwidth <= displaywidth:
1270 break
1276 break
1271
1277
1272 # The smallest number of rows computed and the max widths for each
1278 # The smallest number of rows computed and the max widths for each
1273 # column has been obtained. Now we just have to format each of the rows.
1279 # column has been obtained. Now we just have to format each of the rows.
1274 string = ''
1280 string = ''
1275 for row in range(nrows):
1281 for row in range(nrows):
1276 texts = []
1282 texts = []
1277 for col in range(ncols):
1283 for col in range(ncols):
1278 i = row + nrows*col
1284 i = row + nrows*col
1279 if i >= size:
1285 if i >= size:
1280 texts.append('')
1286 texts.append('')
1281 else:
1287 else:
1282 texts.append(items[i])
1288 texts.append(items[i])
1283 while texts and not texts[-1]:
1289 while texts and not texts[-1]:
1284 del texts[-1]
1290 del texts[-1]
1285 for col in range(len(texts)):
1291 for col in range(len(texts)):
1286 texts[col] = texts[col].ljust(colwidths[col])
1292 texts[col] = texts[col].ljust(colwidths[col])
1287 string += '%s\n' % separator.join(texts)
1293 string += '%s\n' % separator.join(texts)
1288 return string
1294 return string
1289
1295
1290 def _get_block_plain_text(self, block):
1296 def _get_block_plain_text(self, block):
1291 """ Given a QTextBlock, return its unformatted text.
1297 """ Given a QTextBlock, return its unformatted text.
1292 """
1298 """
1293 cursor = QtGui.QTextCursor(block)
1299 cursor = QtGui.QTextCursor(block)
1294 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1300 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1295 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1301 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1296 QtGui.QTextCursor.KeepAnchor)
1302 QtGui.QTextCursor.KeepAnchor)
1297 return cursor.selection().toPlainText()
1303 return cursor.selection().toPlainText()
1298
1304
1299 def _get_cursor(self):
1305 def _get_cursor(self):
1300 """ Convenience method that returns a cursor for the current position.
1306 """ Convenience method that returns a cursor for the current position.
1301 """
1307 """
1302 return self._control.textCursor()
1308 return self._control.textCursor()
1303
1309
1304 def _get_end_cursor(self):
1310 def _get_end_cursor(self):
1305 """ Convenience method that returns a cursor for the last character.
1311 """ Convenience method that returns a cursor for the last character.
1306 """
1312 """
1307 cursor = self._control.textCursor()
1313 cursor = self._control.textCursor()
1308 cursor.movePosition(QtGui.QTextCursor.End)
1314 cursor.movePosition(QtGui.QTextCursor.End)
1309 return cursor
1315 return cursor
1310
1316
1311 def _get_input_buffer_cursor_column(self):
1317 def _get_input_buffer_cursor_column(self):
1312 """ Returns the column of the cursor in the input buffer, excluding the
1318 """ Returns the column of the cursor in the input buffer, excluding the
1313 contribution by the prompt, or -1 if there is no such column.
1319 contribution by the prompt, or -1 if there is no such column.
1314 """
1320 """
1315 prompt = self._get_input_buffer_cursor_prompt()
1321 prompt = self._get_input_buffer_cursor_prompt()
1316 if prompt is None:
1322 if prompt is None:
1317 return -1
1323 return -1
1318 else:
1324 else:
1319 cursor = self._control.textCursor()
1325 cursor = self._control.textCursor()
1320 return cursor.columnNumber() - len(prompt)
1326 return cursor.columnNumber() - len(prompt)
1321
1327
1322 def _get_input_buffer_cursor_line(self):
1328 def _get_input_buffer_cursor_line(self):
1323 """ Returns the text of the line of the input buffer that contains the
1329 """ Returns the text of the line of the input buffer that contains the
1324 cursor, or None if there is no such line.
1330 cursor, or None if there is no such line.
1325 """
1331 """
1326 prompt = self._get_input_buffer_cursor_prompt()
1332 prompt = self._get_input_buffer_cursor_prompt()
1327 if prompt is None:
1333 if prompt is None:
1328 return None
1334 return None
1329 else:
1335 else:
1330 cursor = self._control.textCursor()
1336 cursor = self._control.textCursor()
1331 text = self._get_block_plain_text(cursor.block())
1337 text = self._get_block_plain_text(cursor.block())
1332 return text[len(prompt):]
1338 return text[len(prompt):]
1333
1339
1334 def _get_input_buffer_cursor_prompt(self):
1340 def _get_input_buffer_cursor_prompt(self):
1335 """ Returns the (plain text) prompt for line of the input buffer that
1341 """ Returns the (plain text) prompt for line of the input buffer that
1336 contains the cursor, or None if there is no such line.
1342 contains the cursor, or None if there is no such line.
1337 """
1343 """
1338 if self._executing:
1344 if self._executing:
1339 return None
1345 return None
1340 cursor = self._control.textCursor()
1346 cursor = self._control.textCursor()
1341 if cursor.position() >= self._prompt_pos:
1347 if cursor.position() >= self._prompt_pos:
1342 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1348 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1343 return self._prompt
1349 return self._prompt
1344 else:
1350 else:
1345 return self._continuation_prompt
1351 return self._continuation_prompt
1346 else:
1352 else:
1347 return None
1353 return None
1348
1354
1349 def _get_prompt_cursor(self):
1355 def _get_prompt_cursor(self):
1350 """ Convenience method that returns a cursor for the prompt position.
1356 """ Convenience method that returns a cursor for the prompt position.
1351 """
1357 """
1352 cursor = self._control.textCursor()
1358 cursor = self._control.textCursor()
1353 cursor.setPosition(self._prompt_pos)
1359 cursor.setPosition(self._prompt_pos)
1354 return cursor
1360 return cursor
1355
1361
1356 def _get_selection_cursor(self, start, end):
1362 def _get_selection_cursor(self, start, end):
1357 """ Convenience method that returns a cursor with text selected between
1363 """ Convenience method that returns a cursor with text selected between
1358 the positions 'start' and 'end'.
1364 the positions 'start' and 'end'.
1359 """
1365 """
1360 cursor = self._control.textCursor()
1366 cursor = self._control.textCursor()
1361 cursor.setPosition(start)
1367 cursor.setPosition(start)
1362 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1368 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1363 return cursor
1369 return cursor
1364
1370
1365 def _get_word_start_cursor(self, position):
1371 def _get_word_start_cursor(self, position):
1366 """ Find the start of the word to the left the given position. If a
1372 """ Find the start of the word to the left the given position. If a
1367 sequence of non-word characters precedes the first word, skip over
1373 sequence of non-word characters precedes the first word, skip over
1368 them. (This emulates the behavior of bash, emacs, etc.)
1374 them. (This emulates the behavior of bash, emacs, etc.)
1369 """
1375 """
1370 document = self._control.document()
1376 document = self._control.document()
1371 position -= 1
1377 position -= 1
1372 while position >= self._prompt_pos and \
1378 while position >= self._prompt_pos and \
1373 not is_letter_or_number(document.characterAt(position)):
1379 not is_letter_or_number(document.characterAt(position)):
1374 position -= 1
1380 position -= 1
1375 while position >= self._prompt_pos and \
1381 while position >= self._prompt_pos and \
1376 is_letter_or_number(document.characterAt(position)):
1382 is_letter_or_number(document.characterAt(position)):
1377 position -= 1
1383 position -= 1
1378 cursor = self._control.textCursor()
1384 cursor = self._control.textCursor()
1379 cursor.setPosition(position + 1)
1385 cursor.setPosition(position + 1)
1380 return cursor
1386 return cursor
1381
1387
1382 def _get_word_end_cursor(self, position):
1388 def _get_word_end_cursor(self, position):
1383 """ Find the end of the word to the right the given position. If a
1389 """ Find the end of the word to the right the given position. If a
1384 sequence of non-word characters precedes the first word, skip over
1390 sequence of non-word characters precedes the first word, skip over
1385 them. (This emulates the behavior of bash, emacs, etc.)
1391 them. (This emulates the behavior of bash, emacs, etc.)
1386 """
1392 """
1387 document = self._control.document()
1393 document = self._control.document()
1388 end = self._get_end_cursor().position()
1394 end = self._get_end_cursor().position()
1389 while position < end and \
1395 while position < end and \
1390 not is_letter_or_number(document.characterAt(position)):
1396 not is_letter_or_number(document.characterAt(position)):
1391 position += 1
1397 position += 1
1392 while position < end and \
1398 while position < end and \
1393 is_letter_or_number(document.characterAt(position)):
1399 is_letter_or_number(document.characterAt(position)):
1394 position += 1
1400 position += 1
1395 cursor = self._control.textCursor()
1401 cursor = self._control.textCursor()
1396 cursor.setPosition(position)
1402 cursor.setPosition(position)
1397 return cursor
1403 return cursor
1398
1404
1399 def _insert_continuation_prompt(self, cursor):
1405 def _insert_continuation_prompt(self, cursor):
1400 """ Inserts new continuation prompt using the specified cursor.
1406 """ Inserts new continuation prompt using the specified cursor.
1401 """
1407 """
1402 if self._continuation_prompt_html is None:
1408 if self._continuation_prompt_html is None:
1403 self._insert_plain_text(cursor, self._continuation_prompt)
1409 self._insert_plain_text(cursor, self._continuation_prompt)
1404 else:
1410 else:
1405 self._continuation_prompt = self._insert_html_fetching_plain_text(
1411 self._continuation_prompt = self._insert_html_fetching_plain_text(
1406 cursor, self._continuation_prompt_html)
1412 cursor, self._continuation_prompt_html)
1407
1413
1408 def _insert_html(self, cursor, html):
1414 def _insert_html(self, cursor, html):
1409 """ Inserts HTML using the specified cursor in such a way that future
1415 """ Inserts HTML using the specified cursor in such a way that future
1410 formatting is unaffected.
1416 formatting is unaffected.
1411 """
1417 """
1412 cursor.beginEditBlock()
1418 cursor.beginEditBlock()
1413 cursor.insertHtml(html)
1419 cursor.insertHtml(html)
1414
1420
1415 # After inserting HTML, the text document "remembers" it's in "html
1421 # After inserting HTML, the text document "remembers" it's in "html
1416 # mode", which means that subsequent calls adding plain text will result
1422 # mode", which means that subsequent calls adding plain text will result
1417 # in unwanted formatting, lost tab characters, etc. The following code
1423 # in unwanted formatting, lost tab characters, etc. The following code
1418 # hacks around this behavior, which I consider to be a bug in Qt, by
1424 # hacks around this behavior, which I consider to be a bug in Qt, by
1419 # (crudely) resetting the document's style state.
1425 # (crudely) resetting the document's style state.
1420 cursor.movePosition(QtGui.QTextCursor.Left,
1426 cursor.movePosition(QtGui.QTextCursor.Left,
1421 QtGui.QTextCursor.KeepAnchor)
1427 QtGui.QTextCursor.KeepAnchor)
1422 if cursor.selection().toPlainText() == ' ':
1428 if cursor.selection().toPlainText() == ' ':
1423 cursor.removeSelectedText()
1429 cursor.removeSelectedText()
1424 else:
1430 else:
1425 cursor.movePosition(QtGui.QTextCursor.Right)
1431 cursor.movePosition(QtGui.QTextCursor.Right)
1426 cursor.insertText(' ', QtGui.QTextCharFormat())
1432 cursor.insertText(' ', QtGui.QTextCharFormat())
1427 cursor.endEditBlock()
1433 cursor.endEditBlock()
1428
1434
1429 def _insert_html_fetching_plain_text(self, cursor, html):
1435 def _insert_html_fetching_plain_text(self, cursor, html):
1430 """ Inserts HTML using the specified cursor, then returns its plain text
1436 """ Inserts HTML using the specified cursor, then returns its plain text
1431 version.
1437 version.
1432 """
1438 """
1433 cursor.beginEditBlock()
1439 cursor.beginEditBlock()
1434 cursor.removeSelectedText()
1440 cursor.removeSelectedText()
1435
1441
1436 start = cursor.position()
1442 start = cursor.position()
1437 self._insert_html(cursor, html)
1443 self._insert_html(cursor, html)
1438 end = cursor.position()
1444 end = cursor.position()
1439 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1445 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1440 text = cursor.selection().toPlainText()
1446 text = cursor.selection().toPlainText()
1441
1447
1442 cursor.setPosition(end)
1448 cursor.setPosition(end)
1443 cursor.endEditBlock()
1449 cursor.endEditBlock()
1444 return text
1450 return text
1445
1451
1446 def _insert_plain_text(self, cursor, text):
1452 def _insert_plain_text(self, cursor, text):
1447 """ Inserts plain text using the specified cursor, processing ANSI codes
1453 """ Inserts plain text using the specified cursor, processing ANSI codes
1448 if enabled.
1454 if enabled.
1449 """
1455 """
1450 cursor.beginEditBlock()
1456 cursor.beginEditBlock()
1451 if self.ansi_codes:
1457 if self.ansi_codes:
1452 for substring in self._ansi_processor.split_string(text):
1458 for substring in self._ansi_processor.split_string(text):
1453 for act in self._ansi_processor.actions:
1459 for act in self._ansi_processor.actions:
1454
1460
1455 # Unlike real terminal emulators, we don't distinguish
1461 # Unlike real terminal emulators, we don't distinguish
1456 # between the screen and the scrollback buffer. A screen
1462 # between the screen and the scrollback buffer. A screen
1457 # erase request clears everything.
1463 # erase request clears everything.
1458 if act.action == 'erase' and act.area == 'screen':
1464 if act.action == 'erase' and act.area == 'screen':
1459 cursor.select(QtGui.QTextCursor.Document)
1465 cursor.select(QtGui.QTextCursor.Document)
1460 cursor.removeSelectedText()
1466 cursor.removeSelectedText()
1461
1467
1462 # Simulate a form feed by scrolling just past the last line.
1468 # Simulate a form feed by scrolling just past the last line.
1463 elif act.action == 'scroll' and act.unit == 'page':
1469 elif act.action == 'scroll' and act.unit == 'page':
1464 cursor.insertText('\n')
1470 cursor.insertText('\n')
1465 cursor.endEditBlock()
1471 cursor.endEditBlock()
1466 self._set_top_cursor(cursor)
1472 self._set_top_cursor(cursor)
1467 cursor.joinPreviousEditBlock()
1473 cursor.joinPreviousEditBlock()
1468 cursor.deletePreviousChar()
1474 cursor.deletePreviousChar()
1469
1475
1470 format = self._ansi_processor.get_format()
1476 format = self._ansi_processor.get_format()
1471 cursor.insertText(substring, format)
1477 cursor.insertText(substring, format)
1472 else:
1478 else:
1473 cursor.insertText(text)
1479 cursor.insertText(text)
1474 cursor.endEditBlock()
1480 cursor.endEditBlock()
1475
1481
1476 def _insert_plain_text_into_buffer(self, cursor, text):
1482 def _insert_plain_text_into_buffer(self, cursor, text):
1477 """ Inserts text into the input buffer using the specified cursor (which
1483 """ Inserts text into the input buffer using the specified cursor (which
1478 must be in the input buffer), ensuring that continuation prompts are
1484 must be in the input buffer), ensuring that continuation prompts are
1479 inserted as necessary.
1485 inserted as necessary.
1480 """
1486 """
1481 lines = text.splitlines(True)
1487 lines = text.splitlines(True)
1482 if lines:
1488 if lines:
1483 cursor.beginEditBlock()
1489 cursor.beginEditBlock()
1484 cursor.insertText(lines[0])
1490 cursor.insertText(lines[0])
1485 for line in lines[1:]:
1491 for line in lines[1:]:
1486 if self._continuation_prompt_html is None:
1492 if self._continuation_prompt_html is None:
1487 cursor.insertText(self._continuation_prompt)
1493 cursor.insertText(self._continuation_prompt)
1488 else:
1494 else:
1489 self._continuation_prompt = \
1495 self._continuation_prompt = \
1490 self._insert_html_fetching_plain_text(
1496 self._insert_html_fetching_plain_text(
1491 cursor, self._continuation_prompt_html)
1497 cursor, self._continuation_prompt_html)
1492 cursor.insertText(line)
1498 cursor.insertText(line)
1493 cursor.endEditBlock()
1499 cursor.endEditBlock()
1494
1500
1495 def _in_buffer(self, position=None):
1501 def _in_buffer(self, position=None):
1496 """ Returns whether the current cursor (or, if specified, a position) is
1502 """ Returns whether the current cursor (or, if specified, a position) is
1497 inside the editing region.
1503 inside the editing region.
1498 """
1504 """
1499 cursor = self._control.textCursor()
1505 cursor = self._control.textCursor()
1500 if position is None:
1506 if position is None:
1501 position = cursor.position()
1507 position = cursor.position()
1502 else:
1508 else:
1503 cursor.setPosition(position)
1509 cursor.setPosition(position)
1504 line = cursor.blockNumber()
1510 line = cursor.blockNumber()
1505 prompt_line = self._get_prompt_cursor().blockNumber()
1511 prompt_line = self._get_prompt_cursor().blockNumber()
1506 if line == prompt_line:
1512 if line == prompt_line:
1507 return position >= self._prompt_pos
1513 return position >= self._prompt_pos
1508 elif line > prompt_line:
1514 elif line > prompt_line:
1509 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1515 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1510 prompt_pos = cursor.position() + len(self._continuation_prompt)
1516 prompt_pos = cursor.position() + len(self._continuation_prompt)
1511 return position >= prompt_pos
1517 return position >= prompt_pos
1512 return False
1518 return False
1513
1519
1514 def _keep_cursor_in_buffer(self):
1520 def _keep_cursor_in_buffer(self):
1515 """ Ensures that the cursor is inside the editing region. Returns
1521 """ Ensures that the cursor is inside the editing region. Returns
1516 whether the cursor was moved.
1522 whether the cursor was moved.
1517 """
1523 """
1518 moved = not self._in_buffer()
1524 moved = not self._in_buffer()
1519 if moved:
1525 if moved:
1520 cursor = self._control.textCursor()
1526 cursor = self._control.textCursor()
1521 cursor.movePosition(QtGui.QTextCursor.End)
1527 cursor.movePosition(QtGui.QTextCursor.End)
1522 self._control.setTextCursor(cursor)
1528 self._control.setTextCursor(cursor)
1523 return moved
1529 return moved
1524
1530
1525 def _keyboard_quit(self):
1531 def _keyboard_quit(self):
1526 """ Cancels the current editing task ala Ctrl-G in Emacs.
1532 """ Cancels the current editing task ala Ctrl-G in Emacs.
1527 """
1533 """
1528 if self._text_completing_pos:
1534 if self._text_completing_pos:
1529 self._cancel_text_completion()
1535 self._cancel_text_completion()
1530 else:
1536 else:
1531 self.input_buffer = ''
1537 self.input_buffer = ''
1532
1538
1533 def _page(self, text, html=False):
1539 def _page(self, text, html=False):
1534 """ Displays text using the pager if it exceeds the height of the
1540 """ Displays text using the pager if it exceeds the height of the
1535 viewport.
1541 viewport.
1536
1542
1537 Parameters:
1543 Parameters:
1538 -----------
1544 -----------
1539 html : bool, optional (default False)
1545 html : bool, optional (default False)
1540 If set, the text will be interpreted as HTML instead of plain text.
1546 If set, the text will be interpreted as HTML instead of plain text.
1541 """
1547 """
1542 line_height = QtGui.QFontMetrics(self.font).height()
1548 line_height = QtGui.QFontMetrics(self.font).height()
1543 minlines = self._control.viewport().height() / line_height
1549 minlines = self._control.viewport().height() / line_height
1544 if self.paging != 'none' and \
1550 if self.paging != 'none' and \
1545 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1551 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1546 if self.paging == 'custom':
1552 if self.paging == 'custom':
1547 self.custom_page_requested.emit(text)
1553 self.custom_page_requested.emit(text)
1548 else:
1554 else:
1549 self._page_control.clear()
1555 self._page_control.clear()
1550 cursor = self._page_control.textCursor()
1556 cursor = self._page_control.textCursor()
1551 if html:
1557 if html:
1552 self._insert_html(cursor, text)
1558 self._insert_html(cursor, text)
1553 else:
1559 else:
1554 self._insert_plain_text(cursor, text)
1560 self._insert_plain_text(cursor, text)
1555 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1561 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1556
1562
1557 self._page_control.viewport().resize(self._control.size())
1563 self._page_control.viewport().resize(self._control.size())
1558 if self._splitter:
1564 if self._splitter:
1559 self._page_control.show()
1565 self._page_control.show()
1560 self._page_control.setFocus()
1566 self._page_control.setFocus()
1561 else:
1567 else:
1562 self.layout().setCurrentWidget(self._page_control)
1568 self.layout().setCurrentWidget(self._page_control)
1563 elif html:
1569 elif html:
1564 self._append_plain_html(text)
1570 self._append_plain_html(text)
1565 else:
1571 else:
1566 self._append_plain_text(text)
1572 self._append_plain_text(text)
1567
1573
1568 def _prompt_finished(self):
1574 def _prompt_finished(self):
1569 """ Called immediately after a prompt is finished, i.e. when some input
1575 """ Called immediately after a prompt is finished, i.e. when some input
1570 will be processed and a new prompt displayed.
1576 will be processed and a new prompt displayed.
1571 """
1577 """
1572 self._control.setReadOnly(True)
1578 self._control.setReadOnly(True)
1573 self._prompt_finished_hook()
1579 self._prompt_finished_hook()
1574
1580
1575 def _prompt_started(self):
1581 def _prompt_started(self):
1576 """ Called immediately after a new prompt is displayed.
1582 """ Called immediately after a new prompt is displayed.
1577 """
1583 """
1578 # Temporarily disable the maximum block count to permit undo/redo and
1584 # Temporarily disable the maximum block count to permit undo/redo and
1579 # to ensure that the prompt position does not change due to truncation.
1585 # to ensure that the prompt position does not change due to truncation.
1580 self._control.document().setMaximumBlockCount(0)
1586 self._control.document().setMaximumBlockCount(0)
1581 self._control.setUndoRedoEnabled(True)
1587 self._control.setUndoRedoEnabled(True)
1582
1588
1583 # Work around bug in QPlainTextEdit: input method is not re-enabled
1589 # Work around bug in QPlainTextEdit: input method is not re-enabled
1584 # when read-only is disabled.
1590 # when read-only is disabled.
1585 self._control.setReadOnly(False)
1591 self._control.setReadOnly(False)
1586 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1592 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1587
1593
1588 self._executing = False
1594 self._executing = False
1589 self._prompt_started_hook()
1595 self._prompt_started_hook()
1590
1596
1591 # If the input buffer has changed while executing, load it.
1597 # If the input buffer has changed while executing, load it.
1592 if self._input_buffer_pending:
1598 if self._input_buffer_pending:
1593 self.input_buffer = self._input_buffer_pending
1599 self.input_buffer = self._input_buffer_pending
1594 self._input_buffer_pending = ''
1600 self._input_buffer_pending = ''
1595
1601
1596 self._control.moveCursor(QtGui.QTextCursor.End)
1602 self._control.moveCursor(QtGui.QTextCursor.End)
1597
1603
1598 def _readline(self, prompt='', callback=None):
1604 def _readline(self, prompt='', callback=None):
1599 """ Reads one line of input from the user.
1605 """ Reads one line of input from the user.
1600
1606
1601 Parameters
1607 Parameters
1602 ----------
1608 ----------
1603 prompt : str, optional
1609 prompt : str, optional
1604 The prompt to print before reading the line.
1610 The prompt to print before reading the line.
1605
1611
1606 callback : callable, optional
1612 callback : callable, optional
1607 A callback to execute with the read line. If not specified, input is
1613 A callback to execute with the read line. If not specified, input is
1608 read *synchronously* and this method does not return until it has
1614 read *synchronously* and this method does not return until it has
1609 been read.
1615 been read.
1610
1616
1611 Returns
1617 Returns
1612 -------
1618 -------
1613 If a callback is specified, returns nothing. Otherwise, returns the
1619 If a callback is specified, returns nothing. Otherwise, returns the
1614 input string with the trailing newline stripped.
1620 input string with the trailing newline stripped.
1615 """
1621 """
1616 if self._reading:
1622 if self._reading:
1617 raise RuntimeError('Cannot read a line. Widget is already reading.')
1623 raise RuntimeError('Cannot read a line. Widget is already reading.')
1618
1624
1619 if not callback and not self.isVisible():
1625 if not callback and not self.isVisible():
1620 # If the user cannot see the widget, this function cannot return.
1626 # If the user cannot see the widget, this function cannot return.
1621 raise RuntimeError('Cannot synchronously read a line if the widget '
1627 raise RuntimeError('Cannot synchronously read a line if the widget '
1622 'is not visible!')
1628 'is not visible!')
1623
1629
1624 self._reading = True
1630 self._reading = True
1625 self._show_prompt(prompt, newline=False)
1631 self._show_prompt(prompt, newline=False)
1626
1632
1627 if callback is None:
1633 if callback is None:
1628 self._reading_callback = None
1634 self._reading_callback = None
1629 while self._reading:
1635 while self._reading:
1630 QtCore.QCoreApplication.processEvents()
1636 QtCore.QCoreApplication.processEvents()
1631 return self.input_buffer.rstrip('\n')
1637 return self.input_buffer.rstrip('\n')
1632
1638
1633 else:
1639 else:
1634 self._reading_callback = lambda: \
1640 self._reading_callback = lambda: \
1635 callback(self.input_buffer.rstrip('\n'))
1641 callback(self.input_buffer.rstrip('\n'))
1636
1642
1637 def _set_continuation_prompt(self, prompt, html=False):
1643 def _set_continuation_prompt(self, prompt, html=False):
1638 """ Sets the continuation prompt.
1644 """ Sets the continuation prompt.
1639
1645
1640 Parameters
1646 Parameters
1641 ----------
1647 ----------
1642 prompt : str
1648 prompt : str
1643 The prompt to show when more input is needed.
1649 The prompt to show when more input is needed.
1644
1650
1645 html : bool, optional (default False)
1651 html : bool, optional (default False)
1646 If set, the prompt will be inserted as formatted HTML. Otherwise,
1652 If set, the prompt will be inserted as formatted HTML. Otherwise,
1647 the prompt will be treated as plain text, though ANSI color codes
1653 the prompt will be treated as plain text, though ANSI color codes
1648 will be handled.
1654 will be handled.
1649 """
1655 """
1650 if html:
1656 if html:
1651 self._continuation_prompt_html = prompt
1657 self._continuation_prompt_html = prompt
1652 else:
1658 else:
1653 self._continuation_prompt = prompt
1659 self._continuation_prompt = prompt
1654 self._continuation_prompt_html = None
1660 self._continuation_prompt_html = None
1655
1661
1656 def _set_cursor(self, cursor):
1662 def _set_cursor(self, cursor):
1657 """ Convenience method to set the current cursor.
1663 """ Convenience method to set the current cursor.
1658 """
1664 """
1659 self._control.setTextCursor(cursor)
1665 self._control.setTextCursor(cursor)
1660
1666
1661 def _set_top_cursor(self, cursor):
1667 def _set_top_cursor(self, cursor):
1662 """ Scrolls the viewport so that the specified cursor is at the top.
1668 """ Scrolls the viewport so that the specified cursor is at the top.
1663 """
1669 """
1664 scrollbar = self._control.verticalScrollBar()
1670 scrollbar = self._control.verticalScrollBar()
1665 scrollbar.setValue(scrollbar.maximum())
1671 scrollbar.setValue(scrollbar.maximum())
1666 original_cursor = self._control.textCursor()
1672 original_cursor = self._control.textCursor()
1667 self._control.setTextCursor(cursor)
1673 self._control.setTextCursor(cursor)
1668 self._control.ensureCursorVisible()
1674 self._control.ensureCursorVisible()
1669 self._control.setTextCursor(original_cursor)
1675 self._control.setTextCursor(original_cursor)
1670
1676
1671 def _show_prompt(self, prompt=None, html=False, newline=True):
1677 def _show_prompt(self, prompt=None, html=False, newline=True):
1672 """ Writes a new prompt at the end of the buffer.
1678 """ Writes a new prompt at the end of the buffer.
1673
1679
1674 Parameters
1680 Parameters
1675 ----------
1681 ----------
1676 prompt : str, optional
1682 prompt : str, optional
1677 The prompt to show. If not specified, the previous prompt is used.
1683 The prompt to show. If not specified, the previous prompt is used.
1678
1684
1679 html : bool, optional (default False)
1685 html : bool, optional (default False)
1680 Only relevant when a prompt is specified. If set, the prompt will
1686 Only relevant when a prompt is specified. If set, the prompt will
1681 be inserted as formatted HTML. Otherwise, the prompt will be treated
1687 be inserted as formatted HTML. Otherwise, the prompt will be treated
1682 as plain text, though ANSI color codes will be handled.
1688 as plain text, though ANSI color codes will be handled.
1683
1689
1684 newline : bool, optional (default True)
1690 newline : bool, optional (default True)
1685 If set, a new line will be written before showing the prompt if
1691 If set, a new line will be written before showing the prompt if
1686 there is not already a newline at the end of the buffer.
1692 there is not already a newline at the end of the buffer.
1687 """
1693 """
1688 # Insert a preliminary newline, if necessary.
1694 # Insert a preliminary newline, if necessary.
1689 if newline:
1695 if newline:
1690 cursor = self._get_end_cursor()
1696 cursor = self._get_end_cursor()
1691 if cursor.position() > 0:
1697 if cursor.position() > 0:
1692 cursor.movePosition(QtGui.QTextCursor.Left,
1698 cursor.movePosition(QtGui.QTextCursor.Left,
1693 QtGui.QTextCursor.KeepAnchor)
1699 QtGui.QTextCursor.KeepAnchor)
1694 if cursor.selection().toPlainText() != '\n':
1700 if cursor.selection().toPlainText() != '\n':
1695 self._append_plain_text('\n')
1701 self._append_plain_text('\n')
1696
1702
1697 # Write the prompt.
1703 # Write the prompt.
1698 self._append_plain_text(self._prompt_sep)
1704 self._append_plain_text(self._prompt_sep)
1699 if prompt is None:
1705 if prompt is None:
1700 if self._prompt_html is None:
1706 if self._prompt_html is None:
1701 self._append_plain_text(self._prompt)
1707 self._append_plain_text(self._prompt)
1702 else:
1708 else:
1703 self._append_html(self._prompt_html)
1709 self._append_html(self._prompt_html)
1704 else:
1710 else:
1705 if html:
1711 if html:
1706 self._prompt = self._append_html_fetching_plain_text(prompt)
1712 self._prompt = self._append_html_fetching_plain_text(prompt)
1707 self._prompt_html = prompt
1713 self._prompt_html = prompt
1708 else:
1714 else:
1709 self._append_plain_text(prompt)
1715 self._append_plain_text(prompt)
1710 self._prompt = prompt
1716 self._prompt = prompt
1711 self._prompt_html = None
1717 self._prompt_html = None
1712
1718
1713 self._prompt_pos = self._get_end_cursor().position()
1719 self._prompt_pos = self._get_end_cursor().position()
1714 self._prompt_started()
1720 self._prompt_started()
1715
1721
1716 #------ Signal handlers ----------------------------------------------------
1722 #------ Signal handlers ----------------------------------------------------
1717
1723
1718 def _adjust_scrollbars(self):
1724 def _adjust_scrollbars(self):
1719 """ Expands the vertical scrollbar beyond the range set by Qt.
1725 """ Expands the vertical scrollbar beyond the range set by Qt.
1720 """
1726 """
1721 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1727 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1722 # and qtextedit.cpp.
1728 # and qtextedit.cpp.
1723 document = self._control.document()
1729 document = self._control.document()
1724 scrollbar = self._control.verticalScrollBar()
1730 scrollbar = self._control.verticalScrollBar()
1725 viewport_height = self._control.viewport().height()
1731 viewport_height = self._control.viewport().height()
1726 if isinstance(self._control, QtGui.QPlainTextEdit):
1732 if isinstance(self._control, QtGui.QPlainTextEdit):
1727 maximum = max(0, document.lineCount() - 1)
1733 maximum = max(0, document.lineCount() - 1)
1728 step = viewport_height / self._control.fontMetrics().lineSpacing()
1734 step = viewport_height / self._control.fontMetrics().lineSpacing()
1729 else:
1735 else:
1730 # QTextEdit does not do line-based layout and blocks will not in
1736 # QTextEdit does not do line-based layout and blocks will not in
1731 # general have the same height. Therefore it does not make sense to
1737 # general have the same height. Therefore it does not make sense to
1732 # attempt to scroll in line height increments.
1738 # attempt to scroll in line height increments.
1733 maximum = document.size().height()
1739 maximum = document.size().height()
1734 step = viewport_height
1740 step = viewport_height
1735 diff = maximum - scrollbar.maximum()
1741 diff = maximum - scrollbar.maximum()
1736 scrollbar.setRange(0, maximum)
1742 scrollbar.setRange(0, maximum)
1737 scrollbar.setPageStep(step)
1743 scrollbar.setPageStep(step)
1738
1744
1739 # Compensate for undesirable scrolling that occurs automatically due to
1745 # Compensate for undesirable scrolling that occurs automatically due to
1740 # maximumBlockCount() text truncation.
1746 # maximumBlockCount() text truncation.
1741 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1747 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1742 scrollbar.setValue(scrollbar.value() + diff)
1748 scrollbar.setValue(scrollbar.value() + diff)
1743
1749
1744 def _cursor_position_changed(self):
1750 def _cursor_position_changed(self):
1745 """ Clears the temporary buffer based on the cursor position.
1751 """ Clears the temporary buffer based on the cursor position.
1746 """
1752 """
1747 if self._text_completing_pos:
1753 if self._text_completing_pos:
1748 document = self._control.document()
1754 document = self._control.document()
1749 if self._text_completing_pos < document.characterCount():
1755 if self._text_completing_pos < document.characterCount():
1750 cursor = self._control.textCursor()
1756 cursor = self._control.textCursor()
1751 pos = cursor.position()
1757 pos = cursor.position()
1752 text_cursor = self._control.textCursor()
1758 text_cursor = self._control.textCursor()
1753 text_cursor.setPosition(self._text_completing_pos)
1759 text_cursor.setPosition(self._text_completing_pos)
1754 if pos < self._text_completing_pos or \
1760 if pos < self._text_completing_pos or \
1755 cursor.blockNumber() > text_cursor.blockNumber():
1761 cursor.blockNumber() > text_cursor.blockNumber():
1756 self._clear_temporary_buffer()
1762 self._clear_temporary_buffer()
1757 self._text_completing_pos = 0
1763 self._text_completing_pos = 0
1758 else:
1764 else:
1759 self._clear_temporary_buffer()
1765 self._clear_temporary_buffer()
1760 self._text_completing_pos = 0
1766 self._text_completing_pos = 0
1761
1767
1762 def _custom_context_menu_requested(self, pos):
1768 def _custom_context_menu_requested(self, pos):
1763 """ Shows a context menu at the given QPoint (in widget coordinates).
1769 """ Shows a context menu at the given QPoint (in widget coordinates).
1764 """
1770 """
1765 menu = self._context_menu_make(pos)
1771 menu = self._context_menu_make(pos)
1766 menu.exec_(self._control.mapToGlobal(pos))
1772 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,625 +1,626 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external.qt import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11
11
12 # Local imports
12 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
14 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool
16 from IPython.utils.traitlets import Bool, Instance
17 from bracket_matcher import BracketMatcher
17 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
19 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
20 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
21 from pygments_highlighter import PygmentsHighlighter
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 prompts.
26 prompts.
27 """
27 """
28
28
29 def __init__(self, frontend):
29 def __init__(self, frontend):
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 self._current_offset = 0
31 self._current_offset = 0
32 self._frontend = frontend
32 self._frontend = frontend
33 self.highlighting_on = False
33 self.highlighting_on = False
34
34
35 def highlightBlock(self, string):
35 def highlightBlock(self, string):
36 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """
37 """
38 if not self.highlighting_on:
38 if not self.highlighting_on:
39 return
39 return
40
40
41 # The input to this function is a unicode string that may contain
41 # The input to this function is a unicode string that may contain
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # the string as plain text so we can compare it.
43 # the string as plain text so we can compare it.
44 current_block = self.currentBlock()
44 current_block = self.currentBlock()
45 string = self._frontend._get_block_plain_text(current_block)
45 string = self._frontend._get_block_plain_text(current_block)
46
46
47 # Decide whether to check for the regular or continuation prompt.
47 # Decide whether to check for the regular or continuation prompt.
48 if current_block.contains(self._frontend._prompt_pos):
48 if current_block.contains(self._frontend._prompt_pos):
49 prompt = self._frontend._prompt
49 prompt = self._frontend._prompt
50 else:
50 else:
51 prompt = self._frontend._continuation_prompt
51 prompt = self._frontend._continuation_prompt
52
52
53 # Don't highlight the part of the string that contains the prompt.
53 # Don't highlight the part of the string that contains the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 string = string[len(prompt):]
56 string = string[len(prompt):]
57 else:
57 else:
58 self._current_offset = 0
58 self._current_offset = 0
59
59
60 PygmentsHighlighter.highlightBlock(self, string)
60 PygmentsHighlighter.highlightBlock(self, string)
61
61
62 def rehighlightBlock(self, block):
62 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
63 """ Reimplemented to temporarily enable highlighting if disabled.
64 """
64 """
65 old = self.highlighting_on
65 old = self.highlighting_on
66 self.highlighting_on = True
66 self.highlighting_on = True
67 super(FrontendHighlighter, self).rehighlightBlock(block)
67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 self.highlighting_on = old
68 self.highlighting_on = old
69
69
70 def setFormat(self, start, count, format):
70 def setFormat(self, start, count, format):
71 """ Reimplemented to highlight selectively.
71 """ Reimplemented to highlight selectively.
72 """
72 """
73 start += self._current_offset
73 start += self._current_offset
74 PygmentsHighlighter.setFormat(self, start, count, format)
74 PygmentsHighlighter.setFormat(self, start, count, format)
75
75
76
76
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 """ A Qt frontend for a generic Python kernel.
78 """ A Qt frontend for a generic Python kernel.
79 """
79 """
80
80
81 # An option and corresponding signal for overriding the default kernel
81 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
82 # interrupt behavior.
83 custom_interrupt = Bool(False)
83 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.Signal()
84 custom_interrupt_requested = QtCore.Signal()
85
85
86 # An option and corresponding signals for overriding the default kernel
86 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
87 # restart behavior.
88 custom_restart = Bool(False)
88 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.Signal(float)
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
90 custom_restart_requested = QtCore.Signal()
91
91
92 # Emitted when a user visible 'execute_request' has been submitted to the
92 # Emitted when a user visible 'execute_request' has been submitted to the
93 # kernel from the FrontendWidget. Contains the code to be executed.
93 # kernel from the FrontendWidget. Contains the code to be executed.
94 executing = QtCore.Signal(object)
94 executing = QtCore.Signal(object)
95
95
96 # Emitted when a user-visible 'execute_reply' has been received from the
96 # Emitted when a user-visible 'execute_reply' has been received from the
97 # kernel and processed by the FrontendWidget. Contains the response message.
97 # kernel and processed by the FrontendWidget. Contains the response message.
98 executed = QtCore.Signal(object)
98 executed = QtCore.Signal(object)
99
99
100 # Emitted when an exit request has been received from the kernel.
100 # Emitted when an exit request has been received from the kernel.
101 exit_requested = QtCore.Signal()
101 exit_requested = QtCore.Signal()
102
102
103 # Protected class variables.
103 # Protected class variables.
104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
104 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
105 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
106 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
107 _input_splitter_class = InputSplitter
107 _input_splitter_class = InputSplitter
108 _local_kernel = False
108 _local_kernel = False
109 _highlighter = Instance(FrontendHighlighter)
109
110
110 #---------------------------------------------------------------------------
111 #---------------------------------------------------------------------------
111 # 'object' interface
112 # 'object' interface
112 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
113
114
114 def __init__(self, *args, **kw):
115 def __init__(self, *args, **kw):
115 super(FrontendWidget, self).__init__(*args, **kw)
116 super(FrontendWidget, self).__init__(*args, **kw)
116
117
117 # FrontendWidget protected variables.
118 # FrontendWidget protected variables.
118 self._bracket_matcher = BracketMatcher(self._control)
119 self._bracket_matcher = BracketMatcher(self._control)
119 self._call_tip_widget = CallTipWidget(self._control)
120 self._call_tip_widget = CallTipWidget(self._control)
120 self._completion_lexer = CompletionLexer(PythonLexer())
121 self._completion_lexer = CompletionLexer(PythonLexer())
121 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
122 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
122 self._hidden = False
123 self._hidden = False
123 self._highlighter = FrontendHighlighter(self)
124 self._highlighter = FrontendHighlighter(self)
124 self._input_splitter = self._input_splitter_class(input_mode='cell')
125 self._input_splitter = self._input_splitter_class(input_mode='cell')
125 self._kernel_manager = None
126 self._kernel_manager = None
126 self._request_info = {}
127 self._request_info = {}
127
128
128 # Configure the ConsoleWidget.
129 # Configure the ConsoleWidget.
129 self.tab_width = 4
130 self.tab_width = 4
130 self._set_continuation_prompt('... ')
131 self._set_continuation_prompt('... ')
131
132
132 # Configure the CallTipWidget.
133 # Configure the CallTipWidget.
133 self._call_tip_widget.setFont(self.font)
134 self._call_tip_widget.setFont(self.font)
134 self.font_changed.connect(self._call_tip_widget.setFont)
135 self.font_changed.connect(self._call_tip_widget.setFont)
135
136
136 # Configure actions.
137 # Configure actions.
137 action = self._copy_raw_action
138 action = self._copy_raw_action
138 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
139 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
139 action.setEnabled(False)
140 action.setEnabled(False)
140 action.setShortcut(QtGui.QKeySequence(key))
141 action.setShortcut(QtGui.QKeySequence(key))
141 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
142 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
142 action.triggered.connect(self.copy_raw)
143 action.triggered.connect(self.copy_raw)
143 self.copy_available.connect(action.setEnabled)
144 self.copy_available.connect(action.setEnabled)
144 self.addAction(action)
145 self.addAction(action)
145
146
146 # Connect signal handlers.
147 # Connect signal handlers.
147 document = self._control.document()
148 document = self._control.document()
148 document.contentsChange.connect(self._document_contents_change)
149 document.contentsChange.connect(self._document_contents_change)
149
150
150 # Set flag for whether we are connected via localhost.
151 # Set flag for whether we are connected via localhost.
151 self._local_kernel = kw.get('local_kernel',
152 self._local_kernel = kw.get('local_kernel',
152 FrontendWidget._local_kernel)
153 FrontendWidget._local_kernel)
153
154
154 #---------------------------------------------------------------------------
155 #---------------------------------------------------------------------------
155 # 'ConsoleWidget' public interface
156 # 'ConsoleWidget' public interface
156 #---------------------------------------------------------------------------
157 #---------------------------------------------------------------------------
157
158
158 def copy(self):
159 def copy(self):
159 """ Copy the currently selected text to the clipboard, removing prompts.
160 """ Copy the currently selected text to the clipboard, removing prompts.
160 """
161 """
161 text = self._control.textCursor().selection().toPlainText()
162 text = self._control.textCursor().selection().toPlainText()
162 if text:
163 if text:
163 lines = map(transform_classic_prompt, text.splitlines())
164 lines = map(transform_classic_prompt, text.splitlines())
164 text = '\n'.join(lines)
165 text = '\n'.join(lines)
165 QtGui.QApplication.clipboard().setText(text)
166 QtGui.QApplication.clipboard().setText(text)
166
167
167 #---------------------------------------------------------------------------
168 #---------------------------------------------------------------------------
168 # 'ConsoleWidget' abstract interface
169 # 'ConsoleWidget' abstract interface
169 #---------------------------------------------------------------------------
170 #---------------------------------------------------------------------------
170
171
171 def _is_complete(self, source, interactive):
172 def _is_complete(self, source, interactive):
172 """ Returns whether 'source' can be completely processed and a new
173 """ Returns whether 'source' can be completely processed and a new
173 prompt created. When triggered by an Enter/Return key press,
174 prompt created. When triggered by an Enter/Return key press,
174 'interactive' is True; otherwise, it is False.
175 'interactive' is True; otherwise, it is False.
175 """
176 """
176 complete = self._input_splitter.push(source)
177 complete = self._input_splitter.push(source)
177 if interactive:
178 if interactive:
178 complete = not self._input_splitter.push_accepts_more()
179 complete = not self._input_splitter.push_accepts_more()
179 return complete
180 return complete
180
181
181 def _execute(self, source, hidden):
182 def _execute(self, source, hidden):
182 """ Execute 'source'. If 'hidden', do not show any output.
183 """ Execute 'source'. If 'hidden', do not show any output.
183
184
184 See parent class :meth:`execute` docstring for full details.
185 See parent class :meth:`execute` docstring for full details.
185 """
186 """
186 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
187 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
187 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
188 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
188 self._hidden = hidden
189 self._hidden = hidden
189 if not hidden:
190 if not hidden:
190 self.executing.emit(source)
191 self.executing.emit(source)
191
192
192 def _prompt_started_hook(self):
193 def _prompt_started_hook(self):
193 """ Called immediately after a new prompt is displayed.
194 """ Called immediately after a new prompt is displayed.
194 """
195 """
195 if not self._reading:
196 if not self._reading:
196 self._highlighter.highlighting_on = True
197 self._highlighter.highlighting_on = True
197
198
198 def _prompt_finished_hook(self):
199 def _prompt_finished_hook(self):
199 """ Called immediately after a prompt is finished, i.e. when some input
200 """ Called immediately after a prompt is finished, i.e. when some input
200 will be processed and a new prompt displayed.
201 will be processed and a new prompt displayed.
201 """
202 """
202 # Flush all state from the input splitter so the next round of
203 # Flush all state from the input splitter so the next round of
203 # reading input starts with a clean buffer.
204 # reading input starts with a clean buffer.
204 self._input_splitter.reset()
205 self._input_splitter.reset()
205
206
206 if not self._reading:
207 if not self._reading:
207 self._highlighter.highlighting_on = False
208 self._highlighter.highlighting_on = False
208
209
209 def _tab_pressed(self):
210 def _tab_pressed(self):
210 """ Called when the tab key is pressed. Returns whether to continue
211 """ Called when the tab key is pressed. Returns whether to continue
211 processing the event.
212 processing the event.
212 """
213 """
213 # Perform tab completion if:
214 # Perform tab completion if:
214 # 1) The cursor is in the input buffer.
215 # 1) The cursor is in the input buffer.
215 # 2) There is a non-whitespace character before the cursor.
216 # 2) There is a non-whitespace character before the cursor.
216 text = self._get_input_buffer_cursor_line()
217 text = self._get_input_buffer_cursor_line()
217 if text is None:
218 if text is None:
218 return False
219 return False
219 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
220 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
220 if complete:
221 if complete:
221 self._complete()
222 self._complete()
222 return not complete
223 return not complete
223
224
224 #---------------------------------------------------------------------------
225 #---------------------------------------------------------------------------
225 # 'ConsoleWidget' protected interface
226 # 'ConsoleWidget' protected interface
226 #---------------------------------------------------------------------------
227 #---------------------------------------------------------------------------
227
228
228 def _context_menu_make(self, pos):
229 def _context_menu_make(self, pos):
229 """ Reimplemented to add an action for raw copy.
230 """ Reimplemented to add an action for raw copy.
230 """
231 """
231 menu = super(FrontendWidget, self)._context_menu_make(pos)
232 menu = super(FrontendWidget, self)._context_menu_make(pos)
232 for before_action in menu.actions():
233 for before_action in menu.actions():
233 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
234 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
234 QtGui.QKeySequence.ExactMatch:
235 QtGui.QKeySequence.ExactMatch:
235 menu.insertAction(before_action, self._copy_raw_action)
236 menu.insertAction(before_action, self._copy_raw_action)
236 break
237 break
237 return menu
238 return menu
238
239
239 def _event_filter_console_keypress(self, event):
240 def _event_filter_console_keypress(self, event):
240 """ Reimplemented for execution interruption and smart backspace.
241 """ Reimplemented for execution interruption and smart backspace.
241 """
242 """
242 key = event.key()
243 key = event.key()
243 if self._control_key_down(event.modifiers(), include_command=False):
244 if self._control_key_down(event.modifiers(), include_command=False):
244
245
245 if key == QtCore.Qt.Key_C and self._executing:
246 if key == QtCore.Qt.Key_C and self._executing:
246 self.interrupt_kernel()
247 self.interrupt_kernel()
247 return True
248 return True
248
249
249 elif key == QtCore.Qt.Key_Period:
250 elif key == QtCore.Qt.Key_Period:
250 message = 'Are you sure you want to restart the kernel?'
251 message = 'Are you sure you want to restart the kernel?'
251 self.restart_kernel(message, now=False)
252 self.restart_kernel(message, now=False)
252 return True
253 return True
253
254
254 elif not event.modifiers() & QtCore.Qt.AltModifier:
255 elif not event.modifiers() & QtCore.Qt.AltModifier:
255
256
256 # Smart backspace: remove four characters in one backspace if:
257 # Smart backspace: remove four characters in one backspace if:
257 # 1) everything left of the cursor is whitespace
258 # 1) everything left of the cursor is whitespace
258 # 2) the four characters immediately left of the cursor are spaces
259 # 2) the four characters immediately left of the cursor are spaces
259 if key == QtCore.Qt.Key_Backspace:
260 if key == QtCore.Qt.Key_Backspace:
260 col = self._get_input_buffer_cursor_column()
261 col = self._get_input_buffer_cursor_column()
261 cursor = self._control.textCursor()
262 cursor = self._control.textCursor()
262 if col > 3 and not cursor.hasSelection():
263 if col > 3 and not cursor.hasSelection():
263 text = self._get_input_buffer_cursor_line()[:col]
264 text = self._get_input_buffer_cursor_line()[:col]
264 if text.endswith(' ') and not text.strip():
265 if text.endswith(' ') and not text.strip():
265 cursor.movePosition(QtGui.QTextCursor.Left,
266 cursor.movePosition(QtGui.QTextCursor.Left,
266 QtGui.QTextCursor.KeepAnchor, 4)
267 QtGui.QTextCursor.KeepAnchor, 4)
267 cursor.removeSelectedText()
268 cursor.removeSelectedText()
268 return True
269 return True
269
270
270 return super(FrontendWidget, self)._event_filter_console_keypress(event)
271 return super(FrontendWidget, self)._event_filter_console_keypress(event)
271
272
272 def _insert_continuation_prompt(self, cursor):
273 def _insert_continuation_prompt(self, cursor):
273 """ Reimplemented for auto-indentation.
274 """ Reimplemented for auto-indentation.
274 """
275 """
275 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
276 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
276 cursor.insertText(' ' * self._input_splitter.indent_spaces)
277 cursor.insertText(' ' * self._input_splitter.indent_spaces)
277
278
278 #---------------------------------------------------------------------------
279 #---------------------------------------------------------------------------
279 # 'BaseFrontendMixin' abstract interface
280 # 'BaseFrontendMixin' abstract interface
280 #---------------------------------------------------------------------------
281 #---------------------------------------------------------------------------
281
282
282 def _handle_complete_reply(self, rep):
283 def _handle_complete_reply(self, rep):
283 """ Handle replies for tab completion.
284 """ Handle replies for tab completion.
284 """
285 """
285 cursor = self._get_cursor()
286 cursor = self._get_cursor()
286 info = self._request_info.get('complete')
287 info = self._request_info.get('complete')
287 if info and info.id == rep['parent_header']['msg_id'] and \
288 if info and info.id == rep['parent_header']['msg_id'] and \
288 info.pos == cursor.position():
289 info.pos == cursor.position():
289 text = '.'.join(self._get_context())
290 text = '.'.join(self._get_context())
290 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
291 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
291 self._complete_with_items(cursor, rep['content']['matches'])
292 self._complete_with_items(cursor, rep['content']['matches'])
292
293
293 def _handle_execute_reply(self, msg):
294 def _handle_execute_reply(self, msg):
294 """ Handles replies for code execution.
295 """ Handles replies for code execution.
295 """
296 """
296 info = self._request_info.get('execute')
297 info = self._request_info.get('execute')
297 if info and info.id == msg['parent_header']['msg_id'] and \
298 if info and info.id == msg['parent_header']['msg_id'] and \
298 info.kind == 'user' and not self._hidden:
299 info.kind == 'user' and not self._hidden:
299 # Make sure that all output from the SUB channel has been processed
300 # Make sure that all output from the SUB channel has been processed
300 # before writing a new prompt.
301 # before writing a new prompt.
301 self.kernel_manager.sub_channel.flush()
302 self.kernel_manager.sub_channel.flush()
302
303
303 # Reset the ANSI style information to prevent bad text in stdout
304 # Reset the ANSI style information to prevent bad text in stdout
304 # from messing up our colors. We're not a true terminal so we're
305 # from messing up our colors. We're not a true terminal so we're
305 # allowed to do this.
306 # allowed to do this.
306 if self.ansi_codes:
307 if self.ansi_codes:
307 self._ansi_processor.reset_sgr()
308 self._ansi_processor.reset_sgr()
308
309
309 content = msg['content']
310 content = msg['content']
310 status = content['status']
311 status = content['status']
311 if status == 'ok':
312 if status == 'ok':
312 self._process_execute_ok(msg)
313 self._process_execute_ok(msg)
313 elif status == 'error':
314 elif status == 'error':
314 self._process_execute_error(msg)
315 self._process_execute_error(msg)
315 elif status == 'abort':
316 elif status == 'abort':
316 self._process_execute_abort(msg)
317 self._process_execute_abort(msg)
317
318
318 self._show_interpreter_prompt_for_reply(msg)
319 self._show_interpreter_prompt_for_reply(msg)
319 self.executed.emit(msg)
320 self.executed.emit(msg)
320
321
321 def _handle_input_request(self, msg):
322 def _handle_input_request(self, msg):
322 """ Handle requests for raw_input.
323 """ Handle requests for raw_input.
323 """
324 """
324 if self._hidden:
325 if self._hidden:
325 raise RuntimeError('Request for raw input during hidden execution.')
326 raise RuntimeError('Request for raw input during hidden execution.')
326
327
327 # Make sure that all output from the SUB channel has been processed
328 # Make sure that all output from the SUB channel has been processed
328 # before entering readline mode.
329 # before entering readline mode.
329 self.kernel_manager.sub_channel.flush()
330 self.kernel_manager.sub_channel.flush()
330
331
331 def callback(line):
332 def callback(line):
332 self.kernel_manager.rep_channel.input(line)
333 self.kernel_manager.rep_channel.input(line)
333 self._readline(msg['content']['prompt'], callback=callback)
334 self._readline(msg['content']['prompt'], callback=callback)
334
335
335 def _handle_kernel_died(self, since_last_heartbeat):
336 def _handle_kernel_died(self, since_last_heartbeat):
336 """ Handle the kernel's death by asking if the user wants to restart.
337 """ Handle the kernel's death by asking if the user wants to restart.
337 """
338 """
338 if self.custom_restart:
339 if self.custom_restart:
339 self.custom_restart_kernel_died.emit(since_last_heartbeat)
340 self.custom_restart_kernel_died.emit(since_last_heartbeat)
340 else:
341 else:
341 message = 'The kernel heartbeat has been inactive for %.2f ' \
342 message = 'The kernel heartbeat has been inactive for %.2f ' \
342 'seconds. Do you want to restart the kernel? You may ' \
343 'seconds. Do you want to restart the kernel? You may ' \
343 'first want to check the network connection.' % \
344 'first want to check the network connection.' % \
344 since_last_heartbeat
345 since_last_heartbeat
345 self.restart_kernel(message, now=True)
346 self.restart_kernel(message, now=True)
346
347
347 def _handle_object_info_reply(self, rep):
348 def _handle_object_info_reply(self, rep):
348 """ Handle replies for call tips.
349 """ Handle replies for call tips.
349 """
350 """
350 cursor = self._get_cursor()
351 cursor = self._get_cursor()
351 info = self._request_info.get('call_tip')
352 info = self._request_info.get('call_tip')
352 if info and info.id == rep['parent_header']['msg_id'] and \
353 if info and info.id == rep['parent_header']['msg_id'] and \
353 info.pos == cursor.position():
354 info.pos == cursor.position():
354 # Get the information for a call tip. For now we format the call
355 # Get the information for a call tip. For now we format the call
355 # line as string, later we can pass False to format_call and
356 # line as string, later we can pass False to format_call and
356 # syntax-highlight it ourselves for nicer formatting in the
357 # syntax-highlight it ourselves for nicer formatting in the
357 # calltip.
358 # calltip.
358 content = rep['content']
359 content = rep['content']
359 # if this is from pykernel, 'docstring' will be the only key
360 # if this is from pykernel, 'docstring' will be the only key
360 if content.get('ismagic', False):
361 if content.get('ismagic', False):
361 # Don't generate a call-tip for magics. Ideally, we should
362 # Don't generate a call-tip for magics. Ideally, we should
362 # generate a tooltip, but not on ( like we do for actual
363 # generate a tooltip, but not on ( like we do for actual
363 # callables.
364 # callables.
364 call_info, doc = None, None
365 call_info, doc = None, None
365 else:
366 else:
366 call_info, doc = call_tip(content, format_call=True)
367 call_info, doc = call_tip(content, format_call=True)
367 if call_info or doc:
368 if call_info or doc:
368 self._call_tip_widget.show_call_info(call_info, doc)
369 self._call_tip_widget.show_call_info(call_info, doc)
369
370
370 def _handle_pyout(self, msg):
371 def _handle_pyout(self, msg):
371 """ Handle display hook output.
372 """ Handle display hook output.
372 """
373 """
373 if not self._hidden and self._is_from_this_session(msg):
374 if not self._hidden and self._is_from_this_session(msg):
374 data = msg['content']['data']
375 data = msg['content']['data']
375 if isinstance(data, basestring):
376 if isinstance(data, basestring):
376 # plaintext data from pure Python kernel
377 # plaintext data from pure Python kernel
377 text = data
378 text = data
378 else:
379 else:
379 # formatted output from DisplayFormatter (IPython kernel)
380 # formatted output from DisplayFormatter (IPython kernel)
380 text = data.get('text/plain', '')
381 text = data.get('text/plain', '')
381 self._append_plain_text(text + '\n')
382 self._append_plain_text(text + '\n')
382
383
383 def _handle_stream(self, msg):
384 def _handle_stream(self, msg):
384 """ Handle stdout, stderr, and stdin.
385 """ Handle stdout, stderr, and stdin.
385 """
386 """
386 if not self._hidden and self._is_from_this_session(msg):
387 if not self._hidden and self._is_from_this_session(msg):
387 # Most consoles treat tabs as being 8 space characters. Convert tabs
388 # Most consoles treat tabs as being 8 space characters. Convert tabs
388 # to spaces so that output looks as expected regardless of this
389 # to spaces so that output looks as expected regardless of this
389 # widget's tab width.
390 # widget's tab width.
390 text = msg['content']['data'].expandtabs(8)
391 text = msg['content']['data'].expandtabs(8)
391
392
392 self._append_plain_text(text)
393 self._append_plain_text(text)
393 self._control.moveCursor(QtGui.QTextCursor.End)
394 self._control.moveCursor(QtGui.QTextCursor.End)
394
395
395 def _handle_shutdown_reply(self, msg):
396 def _handle_shutdown_reply(self, msg):
396 """ Handle shutdown signal, only if from other console.
397 """ Handle shutdown signal, only if from other console.
397 """
398 """
398 if not self._hidden and not self._is_from_this_session(msg):
399 if not self._hidden and not self._is_from_this_session(msg):
399 if self._local_kernel:
400 if self._local_kernel:
400 if not msg['content']['restart']:
401 if not msg['content']['restart']:
401 sys.exit(0)
402 sys.exit(0)
402 else:
403 else:
403 # we just got notified of a restart!
404 # we just got notified of a restart!
404 time.sleep(0.25) # wait 1/4 sec to reset
405 time.sleep(0.25) # wait 1/4 sec to reset
405 # lest the request for a new prompt
406 # lest the request for a new prompt
406 # goes to the old kernel
407 # goes to the old kernel
407 self.reset()
408 self.reset()
408 else: # remote kernel, prompt on Kernel shutdown/reset
409 else: # remote kernel, prompt on Kernel shutdown/reset
409 title = self.window().windowTitle()
410 title = self.window().windowTitle()
410 if not msg['content']['restart']:
411 if not msg['content']['restart']:
411 reply = QtGui.QMessageBox.question(self, title,
412 reply = QtGui.QMessageBox.question(self, title,
412 "Kernel has been shutdown permanently. "
413 "Kernel has been shutdown permanently. "
413 "Close the Console?",
414 "Close the Console?",
414 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
415 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
415 if reply == QtGui.QMessageBox.Yes:
416 if reply == QtGui.QMessageBox.Yes:
416 sys.exit(0)
417 sys.exit(0)
417 else:
418 else:
418 reply = QtGui.QMessageBox.question(self, title,
419 reply = QtGui.QMessageBox.question(self, title,
419 "Kernel has been reset. Clear the Console?",
420 "Kernel has been reset. Clear the Console?",
420 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
421 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
421 if reply == QtGui.QMessageBox.Yes:
422 if reply == QtGui.QMessageBox.Yes:
422 time.sleep(0.25) # wait 1/4 sec to reset
423 time.sleep(0.25) # wait 1/4 sec to reset
423 # lest the request for a new prompt
424 # lest the request for a new prompt
424 # goes to the old kernel
425 # goes to the old kernel
425 self.reset()
426 self.reset()
426
427
427 def _started_channels(self):
428 def _started_channels(self):
428 """ Called when the KernelManager channels have started listening or
429 """ Called when the KernelManager channels have started listening or
429 when the frontend is assigned an already listening KernelManager.
430 when the frontend is assigned an already listening KernelManager.
430 """
431 """
431 self.reset()
432 self.reset()
432
433
433 #---------------------------------------------------------------------------
434 #---------------------------------------------------------------------------
434 # 'FrontendWidget' public interface
435 # 'FrontendWidget' public interface
435 #---------------------------------------------------------------------------
436 #---------------------------------------------------------------------------
436
437
437 def copy_raw(self):
438 def copy_raw(self):
438 """ Copy the currently selected text to the clipboard without attempting
439 """ Copy the currently selected text to the clipboard without attempting
439 to remove prompts or otherwise alter the text.
440 to remove prompts or otherwise alter the text.
440 """
441 """
441 self._control.copy()
442 self._control.copy()
442
443
443 def execute_file(self, path, hidden=False):
444 def execute_file(self, path, hidden=False):
444 """ Attempts to execute file with 'path'. If 'hidden', no output is
445 """ Attempts to execute file with 'path'. If 'hidden', no output is
445 shown.
446 shown.
446 """
447 """
447 self.execute('execfile(%r)' % path, hidden=hidden)
448 self.execute('execfile(%r)' % path, hidden=hidden)
448
449
449 def interrupt_kernel(self):
450 def interrupt_kernel(self):
450 """ Attempts to interrupt the running kernel.
451 """ Attempts to interrupt the running kernel.
451 """
452 """
452 if self.custom_interrupt:
453 if self.custom_interrupt:
453 self.custom_interrupt_requested.emit()
454 self.custom_interrupt_requested.emit()
454 elif self.kernel_manager.has_kernel:
455 elif self.kernel_manager.has_kernel:
455 self.kernel_manager.interrupt_kernel()
456 self.kernel_manager.interrupt_kernel()
456 else:
457 else:
457 self._append_plain_text('Kernel process is either remote or '
458 self._append_plain_text('Kernel process is either remote or '
458 'unspecified. Cannot interrupt.\n')
459 'unspecified. Cannot interrupt.\n')
459
460
460 def reset(self):
461 def reset(self):
461 """ Resets the widget to its initial state. Similar to ``clear``, but
462 """ Resets the widget to its initial state. Similar to ``clear``, but
462 also re-writes the banner and aborts execution if necessary.
463 also re-writes the banner and aborts execution if necessary.
463 """
464 """
464 if self._executing:
465 if self._executing:
465 self._executing = False
466 self._executing = False
466 self._request_info['execute'] = None
467 self._request_info['execute'] = None
467 self._reading = False
468 self._reading = False
468 self._highlighter.highlighting_on = False
469 self._highlighter.highlighting_on = False
469
470
470 self._control.clear()
471 self._control.clear()
471 self._append_plain_text(self._get_banner())
472 self._append_plain_text(self._get_banner())
472 self._show_interpreter_prompt()
473 self._show_interpreter_prompt()
473
474
474 def restart_kernel(self, message, now=False):
475 def restart_kernel(self, message, now=False):
475 """ Attempts to restart the running kernel.
476 """ Attempts to restart the running kernel.
476 """
477 """
477 # FIXME: now should be configurable via a checkbox in the dialog. Right
478 # FIXME: now should be configurable via a checkbox in the dialog. Right
478 # now at least the heartbeat path sets it to True and the manual restart
479 # now at least the heartbeat path sets it to True and the manual restart
479 # to False. But those should just be the pre-selected states of a
480 # to False. But those should just be the pre-selected states of a
480 # checkbox that the user could override if so desired. But I don't know
481 # checkbox that the user could override if so desired. But I don't know
481 # enough Qt to go implementing the checkbox now.
482 # enough Qt to go implementing the checkbox now.
482
483
483 if self.custom_restart:
484 if self.custom_restart:
484 self.custom_restart_requested.emit()
485 self.custom_restart_requested.emit()
485
486
486 elif self.kernel_manager.has_kernel:
487 elif self.kernel_manager.has_kernel:
487 # Pause the heart beat channel to prevent further warnings.
488 # Pause the heart beat channel to prevent further warnings.
488 self.kernel_manager.hb_channel.pause()
489 self.kernel_manager.hb_channel.pause()
489
490
490 # Prompt the user to restart the kernel. Un-pause the heartbeat if
491 # Prompt the user to restart the kernel. Un-pause the heartbeat if
491 # they decline. (If they accept, the heartbeat will be un-paused
492 # they decline. (If they accept, the heartbeat will be un-paused
492 # automatically when the kernel is restarted.)
493 # automatically when the kernel is restarted.)
493 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
494 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
494 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
495 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
495 message, buttons)
496 message, buttons)
496 if result == QtGui.QMessageBox.Yes:
497 if result == QtGui.QMessageBox.Yes:
497 try:
498 try:
498 self.kernel_manager.restart_kernel(now=now)
499 self.kernel_manager.restart_kernel(now=now)
499 except RuntimeError:
500 except RuntimeError:
500 self._append_plain_text('Kernel started externally. '
501 self._append_plain_text('Kernel started externally. '
501 'Cannot restart.\n')
502 'Cannot restart.\n')
502 else:
503 else:
503 self.reset()
504 self.reset()
504 else:
505 else:
505 self.kernel_manager.hb_channel.unpause()
506 self.kernel_manager.hb_channel.unpause()
506
507
507 else:
508 else:
508 self._append_plain_text('Kernel process is either remote or '
509 self._append_plain_text('Kernel process is either remote or '
509 'unspecified. Cannot restart.\n')
510 'unspecified. Cannot restart.\n')
510
511
511 #---------------------------------------------------------------------------
512 #---------------------------------------------------------------------------
512 # 'FrontendWidget' protected interface
513 # 'FrontendWidget' protected interface
513 #---------------------------------------------------------------------------
514 #---------------------------------------------------------------------------
514
515
515 def _call_tip(self):
516 def _call_tip(self):
516 """ Shows a call tip, if appropriate, at the current cursor location.
517 """ Shows a call tip, if appropriate, at the current cursor location.
517 """
518 """
518 # Decide if it makes sense to show a call tip
519 # Decide if it makes sense to show a call tip
519 cursor = self._get_cursor()
520 cursor = self._get_cursor()
520 cursor.movePosition(QtGui.QTextCursor.Left)
521 cursor.movePosition(QtGui.QTextCursor.Left)
521 if cursor.document().characterAt(cursor.position()) != '(':
522 if cursor.document().characterAt(cursor.position()) != '(':
522 return False
523 return False
523 context = self._get_context(cursor)
524 context = self._get_context(cursor)
524 if not context:
525 if not context:
525 return False
526 return False
526
527
527 # Send the metadata request to the kernel
528 # Send the metadata request to the kernel
528 name = '.'.join(context)
529 name = '.'.join(context)
529 msg_id = self.kernel_manager.xreq_channel.object_info(name)
530 msg_id = self.kernel_manager.xreq_channel.object_info(name)
530 pos = self._get_cursor().position()
531 pos = self._get_cursor().position()
531 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
532 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
532 return True
533 return True
533
534
534 def _complete(self):
535 def _complete(self):
535 """ Performs completion at the current cursor location.
536 """ Performs completion at the current cursor location.
536 """
537 """
537 context = self._get_context()
538 context = self._get_context()
538 if context:
539 if context:
539 # Send the completion request to the kernel
540 # Send the completion request to the kernel
540 msg_id = self.kernel_manager.xreq_channel.complete(
541 msg_id = self.kernel_manager.xreq_channel.complete(
541 '.'.join(context), # text
542 '.'.join(context), # text
542 self._get_input_buffer_cursor_line(), # line
543 self._get_input_buffer_cursor_line(), # line
543 self._get_input_buffer_cursor_column(), # cursor_pos
544 self._get_input_buffer_cursor_column(), # cursor_pos
544 self.input_buffer) # block
545 self.input_buffer) # block
545 pos = self._get_cursor().position()
546 pos = self._get_cursor().position()
546 info = self._CompletionRequest(msg_id, pos)
547 info = self._CompletionRequest(msg_id, pos)
547 self._request_info['complete'] = info
548 self._request_info['complete'] = info
548
549
549 def _get_banner(self):
550 def _get_banner(self):
550 """ Gets a banner to display at the beginning of a session.
551 """ Gets a banner to display at the beginning of a session.
551 """
552 """
552 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
553 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
553 '"license" for more information.'
554 '"license" for more information.'
554 return banner % (sys.version, sys.platform)
555 return banner % (sys.version, sys.platform)
555
556
556 def _get_context(self, cursor=None):
557 def _get_context(self, cursor=None):
557 """ Gets the context for the specified cursor (or the current cursor
558 """ Gets the context for the specified cursor (or the current cursor
558 if none is specified).
559 if none is specified).
559 """
560 """
560 if cursor is None:
561 if cursor is None:
561 cursor = self._get_cursor()
562 cursor = self._get_cursor()
562 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
563 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
563 QtGui.QTextCursor.KeepAnchor)
564 QtGui.QTextCursor.KeepAnchor)
564 text = cursor.selection().toPlainText()
565 text = cursor.selection().toPlainText()
565 return self._completion_lexer.get_context(text)
566 return self._completion_lexer.get_context(text)
566
567
567 def _process_execute_abort(self, msg):
568 def _process_execute_abort(self, msg):
568 """ Process a reply for an aborted execution request.
569 """ Process a reply for an aborted execution request.
569 """
570 """
570 self._append_plain_text("ERROR: execution aborted\n")
571 self._append_plain_text("ERROR: execution aborted\n")
571
572
572 def _process_execute_error(self, msg):
573 def _process_execute_error(self, msg):
573 """ Process a reply for an execution request that resulted in an error.
574 """ Process a reply for an execution request that resulted in an error.
574 """
575 """
575 content = msg['content']
576 content = msg['content']
576 # If a SystemExit is passed along, this means exit() was called - also
577 # If a SystemExit is passed along, this means exit() was called - also
577 # all the ipython %exit magic syntax of '-k' to be used to keep
578 # all the ipython %exit magic syntax of '-k' to be used to keep
578 # the kernel running
579 # the kernel running
579 if content['ename']=='SystemExit':
580 if content['ename']=='SystemExit':
580 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
581 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
581 self._keep_kernel_on_exit = keepkernel
582 self._keep_kernel_on_exit = keepkernel
582 self.exit_requested.emit()
583 self.exit_requested.emit()
583 else:
584 else:
584 traceback = ''.join(content['traceback'])
585 traceback = ''.join(content['traceback'])
585 self._append_plain_text(traceback)
586 self._append_plain_text(traceback)
586
587
587 def _process_execute_ok(self, msg):
588 def _process_execute_ok(self, msg):
588 """ Process a reply for a successful execution equest.
589 """ Process a reply for a successful execution equest.
589 """
590 """
590 payload = msg['content']['payload']
591 payload = msg['content']['payload']
591 for item in payload:
592 for item in payload:
592 if not self._process_execute_payload(item):
593 if not self._process_execute_payload(item):
593 warning = 'Warning: received unknown payload of type %s'
594 warning = 'Warning: received unknown payload of type %s'
594 print(warning % repr(item['source']))
595 print(warning % repr(item['source']))
595
596
596 def _process_execute_payload(self, item):
597 def _process_execute_payload(self, item):
597 """ Process a single payload item from the list of payload items in an
598 """ Process a single payload item from the list of payload items in an
598 execution reply. Returns whether the payload was handled.
599 execution reply. Returns whether the payload was handled.
599 """
600 """
600 # The basic FrontendWidget doesn't handle payloads, as they are a
601 # The basic FrontendWidget doesn't handle payloads, as they are a
601 # mechanism for going beyond the standard Python interpreter model.
602 # mechanism for going beyond the standard Python interpreter model.
602 return False
603 return False
603
604
604 def _show_interpreter_prompt(self):
605 def _show_interpreter_prompt(self):
605 """ Shows a prompt for the interpreter.
606 """ Shows a prompt for the interpreter.
606 """
607 """
607 self._show_prompt('>>> ')
608 self._show_prompt('>>> ')
608
609
609 def _show_interpreter_prompt_for_reply(self, msg):
610 def _show_interpreter_prompt_for_reply(self, msg):
610 """ Shows a prompt for the interpreter given an 'execute_reply' message.
611 """ Shows a prompt for the interpreter given an 'execute_reply' message.
611 """
612 """
612 self._show_interpreter_prompt()
613 self._show_interpreter_prompt()
613
614
614 #------ Signal handlers ----------------------------------------------------
615 #------ Signal handlers ----------------------------------------------------
615
616
616 def _document_contents_change(self, position, removed, added):
617 def _document_contents_change(self, position, removed, added):
617 """ Called whenever the document's content changes. Display a call tip
618 """ Called whenever the document's content changes. Display a call tip
618 if appropriate.
619 if appropriate.
619 """
620 """
620 # Calculate where the cursor should be *after* the change:
621 # Calculate where the cursor should be *after* the change:
621 position += added
622 position += added
622
623
623 document = self._control.document()
624 document = self._control.document()
624 if position == self._get_cursor().position():
625 if position == self._get_cursor().position():
625 self._call_tip()
626 self._call_tip()
@@ -1,492 +1,503 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import os.path
12 import re
12 import re
13 from subprocess import Popen
13 from subprocess import Popen
14 import sys
14 import sys
15 from textwrap import dedent
15 from textwrap import dedent
16
16
17 # System library imports
17 # System library imports
18 from IPython.external.qt import QtCore, QtGui
18 from IPython.external.qt import QtCore, QtGui
19
19
20 # Local imports
20 # Local imports
21 from IPython.core.inputsplitter import IPythonInputSplitter, \
21 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 transform_ipy_prompt
22 transform_ipy_prompt
23 from IPython.core.usage import default_gui_banner
23 from IPython.core.usage import default_gui_banner
24 from IPython.utils.traitlets import Bool, Str, Unicode
24 from IPython.utils.traitlets import Bool, Str, Unicode
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26 from styles import (default_light_style_sheet, default_light_syntax_style,
26 import styles
27 default_dark_style_sheet, default_dark_syntax_style,
28 default_bw_style_sheet, default_bw_syntax_style)
29
27
30 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
31 # Constants
29 # Constants
32 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
33
31
34 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
35 # in between)
33 # in between)
36 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
37 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
38 default_input_sep = '\n'
36 default_input_sep = '\n'
39 default_output_sep = ''
37 default_output_sep = ''
40 default_output_sep2 = ''
38 default_output_sep2 = ''
41
39
42 # Base path for most payload sources.
40 # Base path for most payload sources.
43 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
44
42
45 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
46 # IPythonWidget class
44 # IPythonWidget class
47 #-----------------------------------------------------------------------------
45 #-----------------------------------------------------------------------------
48
46
49 class IPythonWidget(FrontendWidget):
47 class IPythonWidget(FrontendWidget):
50 """ A FrontendWidget for an IPython kernel.
48 """ A FrontendWidget for an IPython kernel.
51 """
49 """
52
50
53 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
51 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
52 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 # settings.
53 # settings.
56 custom_edit = Bool(False)
54 custom_edit = Bool(False)
57 custom_edit_requested = QtCore.Signal(object, object)
55 custom_edit_requested = QtCore.Signal(object, object)
58
56
59 # A command for invoking a system text editor. If the string contains a
57 editor = Unicode('default', config=True,
60 # {filename} format specifier, it will be used. Otherwise, the filename will
58 help="""
61 # be appended to the end the command.
59 A command for invoking a system text editor. If the string contains a
62 editor = Unicode('default', config=True)
60 {filename} format specifier, it will be used. Otherwise, the filename will
63
61 be appended to the end the command.
64 # The editor command to use when a specific line number is requested. The
62 """)
65 # string should contain two format specifiers: {line} and {filename}. If
63
66 # this parameter is not specified, the line number option to the %edit magic
64 editor_line = Unicode(config=True,
67 # will be ignored.
65 help="""
68 editor_line = Unicode(config=True)
66 The editor command to use when a specific line number is requested. The
69
67 string should contain two format specifiers: {line} and {filename}. If
70 # A CSS stylesheet. The stylesheet can contain classes for:
68 this parameter is not specified, the line number option to the %edit magic
71 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
69 will be ignored.
72 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
70 """)
73 # 3. IPython: .error, .in-prompt, .out-prompt, etc
71
74 style_sheet = Unicode(config=True)
72 style_sheet = Unicode(config=True,
73 help="""
74 A CSS stylesheet. The stylesheet can contain classes for:
75 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
76 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
77 3. IPython: .error, .in-prompt, .out-prompt, etc
78 """)
75
79
76 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
80
77 # the style sheet is queried for Pygments style information.
81 syntax_style = Str(config=True,
78 syntax_style = Str(config=True)
82 help="""
83 If not empty, use this Pygments style for syntax highlighting. Otherwise,
84 the style sheet is queried for Pygments style information.
85 """)
79
86
80 # Prompts.
87 # Prompts.
81 in_prompt = Str(default_in_prompt, config=True)
88 in_prompt = Str(default_in_prompt, config=True)
82 out_prompt = Str(default_out_prompt, config=True)
89 out_prompt = Str(default_out_prompt, config=True)
83 input_sep = Str(default_input_sep, config=True)
90 input_sep = Str(default_input_sep, config=True)
84 output_sep = Str(default_output_sep, config=True)
91 output_sep = Str(default_output_sep, config=True)
85 output_sep2 = Str(default_output_sep2, config=True)
92 output_sep2 = Str(default_output_sep2, config=True)
86
93
87 # FrontendWidget protected class variables.
94 # FrontendWidget protected class variables.
88 _input_splitter_class = IPythonInputSplitter
95 _input_splitter_class = IPythonInputSplitter
89
96
90 # IPythonWidget protected class variables.
97 # IPythonWidget protected class variables.
91 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
98 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
92 _payload_source_edit = zmq_shell_source + '.edit_magic'
99 _payload_source_edit = zmq_shell_source + '.edit_magic'
93 _payload_source_exit = zmq_shell_source + '.ask_exit'
100 _payload_source_exit = zmq_shell_source + '.ask_exit'
94 _payload_source_next_input = zmq_shell_source + '.set_next_input'
101 _payload_source_next_input = zmq_shell_source + '.set_next_input'
95 _payload_source_page = 'IPython.zmq.page.page'
102 _payload_source_page = 'IPython.zmq.page.page'
96
103
97 #---------------------------------------------------------------------------
104 #---------------------------------------------------------------------------
98 # 'object' interface
105 # 'object' interface
99 #---------------------------------------------------------------------------
106 #---------------------------------------------------------------------------
100
107
101 def __init__(self, *args, **kw):
108 def __init__(self, *args, **kw):
102 super(IPythonWidget, self).__init__(*args, **kw)
109 super(IPythonWidget, self).__init__(*args, **kw)
103
110
104 # IPythonWidget protected variables.
111 # IPythonWidget protected variables.
105 self._payload_handlers = {
112 self._payload_handlers = {
106 self._payload_source_edit : self._handle_payload_edit,
113 self._payload_source_edit : self._handle_payload_edit,
107 self._payload_source_exit : self._handle_payload_exit,
114 self._payload_source_exit : self._handle_payload_exit,
108 self._payload_source_page : self._handle_payload_page,
115 self._payload_source_page : self._handle_payload_page,
109 self._payload_source_next_input : self._handle_payload_next_input }
116 self._payload_source_next_input : self._handle_payload_next_input }
110 self._previous_prompt_obj = None
117 self._previous_prompt_obj = None
111 self._keep_kernel_on_exit = None
118 self._keep_kernel_on_exit = None
112
119
113 # Initialize widget styling.
120 # Initialize widget styling.
114 if self.style_sheet:
121 if self.style_sheet:
115 self._style_sheet_changed()
122 self._style_sheet_changed()
116 self._syntax_style_changed()
123 self._syntax_style_changed()
117 else:
124 else:
118 self.set_default_style()
125 self.set_default_style()
119
126
120 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
121 # 'BaseFrontendMixin' abstract interface
128 # 'BaseFrontendMixin' abstract interface
122 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
123
130
124 def _handle_complete_reply(self, rep):
131 def _handle_complete_reply(self, rep):
125 """ Reimplemented to support IPython's improved completion machinery.
132 """ Reimplemented to support IPython's improved completion machinery.
126 """
133 """
127 cursor = self._get_cursor()
134 cursor = self._get_cursor()
128 info = self._request_info.get('complete')
135 info = self._request_info.get('complete')
129 if info and info.id == rep['parent_header']['msg_id'] and \
136 if info and info.id == rep['parent_header']['msg_id'] and \
130 info.pos == cursor.position():
137 info.pos == cursor.position():
131 matches = rep['content']['matches']
138 matches = rep['content']['matches']
132 text = rep['content']['matched_text']
139 text = rep['content']['matched_text']
133 offset = len(text)
140 offset = len(text)
134
141
135 # Clean up matches with period and path separators if the matched
142 # Clean up matches with period and path separators if the matched
136 # text has not been transformed. This is done by truncating all
143 # text has not been transformed. This is done by truncating all
137 # but the last component and then suitably decreasing the offset
144 # but the last component and then suitably decreasing the offset
138 # between the current cursor position and the start of completion.
145 # between the current cursor position and the start of completion.
139 if len(matches) > 1 and matches[0][:offset] == text:
146 if len(matches) > 1 and matches[0][:offset] == text:
140 parts = re.split(r'[./\\]', text)
147 parts = re.split(r'[./\\]', text)
141 sep_count = len(parts) - 1
148 sep_count = len(parts) - 1
142 if sep_count:
149 if sep_count:
143 chop_length = sum(map(len, parts[:sep_count])) + sep_count
150 chop_length = sum(map(len, parts[:sep_count])) + sep_count
144 matches = [ match[chop_length:] for match in matches ]
151 matches = [ match[chop_length:] for match in matches ]
145 offset -= chop_length
152 offset -= chop_length
146
153
147 # Move the cursor to the start of the match and complete.
154 # Move the cursor to the start of the match and complete.
148 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
155 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
149 self._complete_with_items(cursor, matches)
156 self._complete_with_items(cursor, matches)
150
157
151 def _handle_execute_reply(self, msg):
158 def _handle_execute_reply(self, msg):
152 """ Reimplemented to support prompt requests.
159 """ Reimplemented to support prompt requests.
153 """
160 """
154 info = self._request_info.get('execute')
161 info = self._request_info.get('execute')
155 if info and info.id == msg['parent_header']['msg_id']:
162 if info and info.id == msg['parent_header']['msg_id']:
156 if info.kind == 'prompt':
163 if info.kind == 'prompt':
157 number = msg['content']['execution_count'] + 1
164 number = msg['content']['execution_count'] + 1
158 self._show_interpreter_prompt(number)
165 self._show_interpreter_prompt(number)
159 else:
166 else:
160 super(IPythonWidget, self)._handle_execute_reply(msg)
167 super(IPythonWidget, self)._handle_execute_reply(msg)
161
168
162 def _handle_history_reply(self, msg):
169 def _handle_history_reply(self, msg):
163 """ Implemented to handle history tail replies, which are only supported
170 """ Implemented to handle history tail replies, which are only supported
164 by the IPython kernel.
171 by the IPython kernel.
165 """
172 """
166 history_items = msg['content']['history']
173 history_items = msg['content']['history']
167 items = [ line.rstrip() for _, _, line in history_items ]
174 items = [ line.rstrip() for _, _, line in history_items ]
168 self._set_history(items)
175 self._set_history(items)
169
176
170 def _handle_pyout(self, msg):
177 def _handle_pyout(self, msg):
171 """ Reimplemented for IPython-style "display hook".
178 """ Reimplemented for IPython-style "display hook".
172 """
179 """
173 if not self._hidden and self._is_from_this_session(msg):
180 if not self._hidden and self._is_from_this_session(msg):
174 content = msg['content']
181 content = msg['content']
175 prompt_number = content['execution_count']
182 prompt_number = content['execution_count']
176 data = content['data']
183 data = content['data']
177 if data.has_key('text/html'):
184 if data.has_key('text/html'):
178 self._append_plain_text(self.output_sep)
185 self._append_plain_text(self.output_sep)
179 self._append_html(self._make_out_prompt(prompt_number))
186 self._append_html(self._make_out_prompt(prompt_number))
180 html = data['text/html']
187 html = data['text/html']
181 self._append_plain_text('\n')
188 self._append_plain_text('\n')
182 self._append_html(html + self.output_sep2)
189 self._append_html(html + self.output_sep2)
183 elif data.has_key('text/plain'):
190 elif data.has_key('text/plain'):
184 self._append_plain_text(self.output_sep)
191 self._append_plain_text(self.output_sep)
185 self._append_html(self._make_out_prompt(prompt_number))
192 self._append_html(self._make_out_prompt(prompt_number))
186 text = data['text/plain']
193 text = data['text/plain']
187 # If the repr is multiline, make sure we start on a new line,
194 # If the repr is multiline, make sure we start on a new line,
188 # so that its lines are aligned.
195 # so that its lines are aligned.
189 if "\n" in text and not self.output_sep.endswith("\n"):
196 if "\n" in text and not self.output_sep.endswith("\n"):
190 self._append_plain_text('\n')
197 self._append_plain_text('\n')
191 self._append_plain_text(text + self.output_sep2)
198 self._append_plain_text(text + self.output_sep2)
192
199
193 def _handle_display_data(self, msg):
200 def _handle_display_data(self, msg):
194 """ The base handler for the ``display_data`` message.
201 """ The base handler for the ``display_data`` message.
195 """
202 """
196 # For now, we don't display data from other frontends, but we
203 # For now, we don't display data from other frontends, but we
197 # eventually will as this allows all frontends to monitor the display
204 # eventually will as this allows all frontends to monitor the display
198 # data. But we need to figure out how to handle this in the GUI.
205 # data. But we need to figure out how to handle this in the GUI.
199 if not self._hidden and self._is_from_this_session(msg):
206 if not self._hidden and self._is_from_this_session(msg):
200 source = msg['content']['source']
207 source = msg['content']['source']
201 data = msg['content']['data']
208 data = msg['content']['data']
202 metadata = msg['content']['metadata']
209 metadata = msg['content']['metadata']
203 # In the regular IPythonWidget, we simply print the plain text
210 # In the regular IPythonWidget, we simply print the plain text
204 # representation.
211 # representation.
205 if data.has_key('text/html'):
212 if data.has_key('text/html'):
206 html = data['text/html']
213 html = data['text/html']
207 self._append_html(html)
214 self._append_html(html)
208 elif data.has_key('text/plain'):
215 elif data.has_key('text/plain'):
209 text = data['text/plain']
216 text = data['text/plain']
210 self._append_plain_text(text)
217 self._append_plain_text(text)
211 # This newline seems to be needed for text and html output.
218 # This newline seems to be needed for text and html output.
212 self._append_plain_text(u'\n')
219 self._append_plain_text(u'\n')
213
220
214 def _started_channels(self):
221 def _started_channels(self):
215 """ Reimplemented to make a history request.
222 """ Reimplemented to make a history request.
216 """
223 """
217 super(IPythonWidget, self)._started_channels()
224 super(IPythonWidget, self)._started_channels()
218 self.kernel_manager.xreq_channel.history(hist_access_type='tail', n=1000)
225 self.kernel_manager.xreq_channel.history(hist_access_type='tail', n=1000)
219
226
220 #---------------------------------------------------------------------------
227 #---------------------------------------------------------------------------
221 # 'ConsoleWidget' public interface
228 # 'ConsoleWidget' public interface
222 #---------------------------------------------------------------------------
229 #---------------------------------------------------------------------------
223
230
224 def copy(self):
231 def copy(self):
225 """ Copy the currently selected text to the clipboard, removing prompts
232 """ Copy the currently selected text to the clipboard, removing prompts
226 if possible.
233 if possible.
227 """
234 """
228 text = self._control.textCursor().selection().toPlainText()
235 text = self._control.textCursor().selection().toPlainText()
229 if text:
236 if text:
230 lines = map(transform_ipy_prompt, text.splitlines())
237 lines = map(transform_ipy_prompt, text.splitlines())
231 text = '\n'.join(lines)
238 text = '\n'.join(lines)
232 QtGui.QApplication.clipboard().setText(text)
239 QtGui.QApplication.clipboard().setText(text)
233
240
234 #---------------------------------------------------------------------------
241 #---------------------------------------------------------------------------
235 # 'FrontendWidget' public interface
242 # 'FrontendWidget' public interface
236 #---------------------------------------------------------------------------
243 #---------------------------------------------------------------------------
237
244
238 def execute_file(self, path, hidden=False):
245 def execute_file(self, path, hidden=False):
239 """ Reimplemented to use the 'run' magic.
246 """ Reimplemented to use the 'run' magic.
240 """
247 """
241 # Use forward slashes on Windows to avoid escaping each separator.
248 # Use forward slashes on Windows to avoid escaping each separator.
242 if sys.platform == 'win32':
249 if sys.platform == 'win32':
243 path = os.path.normpath(path).replace('\\', '/')
250 path = os.path.normpath(path).replace('\\', '/')
244
251
245 self.execute('%%run %s' % path, hidden=hidden)
252 self.execute('%%run %s' % path, hidden=hidden)
246
253
247 #---------------------------------------------------------------------------
254 #---------------------------------------------------------------------------
248 # 'FrontendWidget' protected interface
255 # 'FrontendWidget' protected interface
249 #---------------------------------------------------------------------------
256 #---------------------------------------------------------------------------
250
257
251 def _complete(self):
258 def _complete(self):
252 """ Reimplemented to support IPython's improved completion machinery.
259 """ Reimplemented to support IPython's improved completion machinery.
253 """
260 """
254 # We let the kernel split the input line, so we *always* send an empty
261 # We let the kernel split the input line, so we *always* send an empty
255 # text field. Readline-based frontends do get a real text field which
262 # text field. Readline-based frontends do get a real text field which
256 # they can use.
263 # they can use.
257 text = ''
264 text = ''
258
265
259 # Send the completion request to the kernel
266 # Send the completion request to the kernel
260 msg_id = self.kernel_manager.xreq_channel.complete(
267 msg_id = self.kernel_manager.xreq_channel.complete(
261 text, # text
268 text, # text
262 self._get_input_buffer_cursor_line(), # line
269 self._get_input_buffer_cursor_line(), # line
263 self._get_input_buffer_cursor_column(), # cursor_pos
270 self._get_input_buffer_cursor_column(), # cursor_pos
264 self.input_buffer) # block
271 self.input_buffer) # block
265 pos = self._get_cursor().position()
272 pos = self._get_cursor().position()
266 info = self._CompletionRequest(msg_id, pos)
273 info = self._CompletionRequest(msg_id, pos)
267 self._request_info['complete'] = info
274 self._request_info['complete'] = info
268
275
269 def _get_banner(self):
276 def _get_banner(self):
270 """ Reimplemented to return IPython's default banner.
277 """ Reimplemented to return IPython's default banner.
271 """
278 """
272 return default_gui_banner
279 return default_gui_banner
273
280
274 def _process_execute_error(self, msg):
281 def _process_execute_error(self, msg):
275 """ Reimplemented for IPython-style traceback formatting.
282 """ Reimplemented for IPython-style traceback formatting.
276 """
283 """
277 content = msg['content']
284 content = msg['content']
278 traceback = '\n'.join(content['traceback']) + '\n'
285 traceback = '\n'.join(content['traceback']) + '\n'
279 if False:
286 if False:
280 # FIXME: For now, tracebacks come as plain text, so we can't use
287 # FIXME: For now, tracebacks come as plain text, so we can't use
281 # the html renderer yet. Once we refactor ultratb to produce
288 # the html renderer yet. Once we refactor ultratb to produce
282 # properly styled tracebacks, this branch should be the default
289 # properly styled tracebacks, this branch should be the default
283 traceback = traceback.replace(' ', '&nbsp;')
290 traceback = traceback.replace(' ', '&nbsp;')
284 traceback = traceback.replace('\n', '<br/>')
291 traceback = traceback.replace('\n', '<br/>')
285
292
286 ename = content['ename']
293 ename = content['ename']
287 ename_styled = '<span class="error">%s</span>' % ename
294 ename_styled = '<span class="error">%s</span>' % ename
288 traceback = traceback.replace(ename, ename_styled)
295 traceback = traceback.replace(ename, ename_styled)
289
296
290 self._append_html(traceback)
297 self._append_html(traceback)
291 else:
298 else:
292 # This is the fallback for now, using plain text with ansi escapes
299 # This is the fallback for now, using plain text with ansi escapes
293 self._append_plain_text(traceback)
300 self._append_plain_text(traceback)
294
301
295 def _process_execute_payload(self, item):
302 def _process_execute_payload(self, item):
296 """ Reimplemented to dispatch payloads to handler methods.
303 """ Reimplemented to dispatch payloads to handler methods.
297 """
304 """
298 handler = self._payload_handlers.get(item['source'])
305 handler = self._payload_handlers.get(item['source'])
299 if handler is None:
306 if handler is None:
300 # We have no handler for this type of payload, simply ignore it
307 # We have no handler for this type of payload, simply ignore it
301 return False
308 return False
302 else:
309 else:
303 handler(item)
310 handler(item)
304 return True
311 return True
305
312
306 def _show_interpreter_prompt(self, number=None):
313 def _show_interpreter_prompt(self, number=None):
307 """ Reimplemented for IPython-style prompts.
314 """ Reimplemented for IPython-style prompts.
308 """
315 """
309 # If a number was not specified, make a prompt number request.
316 # If a number was not specified, make a prompt number request.
310 if number is None:
317 if number is None:
311 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
318 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
312 info = self._ExecutionRequest(msg_id, 'prompt')
319 info = self._ExecutionRequest(msg_id, 'prompt')
313 self._request_info['execute'] = info
320 self._request_info['execute'] = info
314 return
321 return
315
322
316 # Show a new prompt and save information about it so that it can be
323 # Show a new prompt and save information about it so that it can be
317 # updated later if the prompt number turns out to be wrong.
324 # updated later if the prompt number turns out to be wrong.
318 self._prompt_sep = self.input_sep
325 self._prompt_sep = self.input_sep
319 self._show_prompt(self._make_in_prompt(number), html=True)
326 self._show_prompt(self._make_in_prompt(number), html=True)
320 block = self._control.document().lastBlock()
327 block = self._control.document().lastBlock()
321 length = len(self._prompt)
328 length = len(self._prompt)
322 self._previous_prompt_obj = self._PromptBlock(block, length, number)
329 self._previous_prompt_obj = self._PromptBlock(block, length, number)
323
330
324 # Update continuation prompt to reflect (possibly) new prompt length.
331 # Update continuation prompt to reflect (possibly) new prompt length.
325 self._set_continuation_prompt(
332 self._set_continuation_prompt(
326 self._make_continuation_prompt(self._prompt), html=True)
333 self._make_continuation_prompt(self._prompt), html=True)
327
334
328 def _show_interpreter_prompt_for_reply(self, msg):
335 def _show_interpreter_prompt_for_reply(self, msg):
329 """ Reimplemented for IPython-style prompts.
336 """ Reimplemented for IPython-style prompts.
330 """
337 """
331 # Update the old prompt number if necessary.
338 # Update the old prompt number if necessary.
332 content = msg['content']
339 content = msg['content']
333 previous_prompt_number = content['execution_count']
340 previous_prompt_number = content['execution_count']
334 if self._previous_prompt_obj and \
341 if self._previous_prompt_obj and \
335 self._previous_prompt_obj.number != previous_prompt_number:
342 self._previous_prompt_obj.number != previous_prompt_number:
336 block = self._previous_prompt_obj.block
343 block = self._previous_prompt_obj.block
337
344
338 # Make sure the prompt block has not been erased.
345 # Make sure the prompt block has not been erased.
339 if block.isValid() and block.text():
346 if block.isValid() and block.text():
340
347
341 # Remove the old prompt and insert a new prompt.
348 # Remove the old prompt and insert a new prompt.
342 cursor = QtGui.QTextCursor(block)
349 cursor = QtGui.QTextCursor(block)
343 cursor.movePosition(QtGui.QTextCursor.Right,
350 cursor.movePosition(QtGui.QTextCursor.Right,
344 QtGui.QTextCursor.KeepAnchor,
351 QtGui.QTextCursor.KeepAnchor,
345 self._previous_prompt_obj.length)
352 self._previous_prompt_obj.length)
346 prompt = self._make_in_prompt(previous_prompt_number)
353 prompt = self._make_in_prompt(previous_prompt_number)
347 self._prompt = self._insert_html_fetching_plain_text(
354 self._prompt = self._insert_html_fetching_plain_text(
348 cursor, prompt)
355 cursor, prompt)
349
356
350 # When the HTML is inserted, Qt blows away the syntax
357 # When the HTML is inserted, Qt blows away the syntax
351 # highlighting for the line, so we need to rehighlight it.
358 # highlighting for the line, so we need to rehighlight it.
352 self._highlighter.rehighlightBlock(cursor.block())
359 self._highlighter.rehighlightBlock(cursor.block())
353
360
354 self._previous_prompt_obj = None
361 self._previous_prompt_obj = None
355
362
356 # Show a new prompt with the kernel's estimated prompt number.
363 # Show a new prompt with the kernel's estimated prompt number.
357 self._show_interpreter_prompt(previous_prompt_number + 1)
364 self._show_interpreter_prompt(previous_prompt_number + 1)
358
365
359 #---------------------------------------------------------------------------
366 #---------------------------------------------------------------------------
360 # 'IPythonWidget' interface
367 # 'IPythonWidget' interface
361 #---------------------------------------------------------------------------
368 #---------------------------------------------------------------------------
362
369
363 def set_default_style(self, colors='lightbg'):
370 def set_default_style(self, colors='lightbg'):
364 """ Sets the widget style to the class defaults.
371 """ Sets the widget style to the class defaults.
365
372
366 Parameters:
373 Parameters:
367 -----------
374 -----------
368 colors : str, optional (default lightbg)
375 colors : str, optional (default lightbg)
369 Whether to use the default IPython light background or dark
376 Whether to use the default IPython light background or dark
370 background or B&W style.
377 background or B&W style.
371 """
378 """
372 colors = colors.lower()
379 colors = colors.lower()
373 if colors=='lightbg':
380 if colors=='lightbg':
374 self.style_sheet = default_light_style_sheet
381 self.style_sheet = styles.default_light_style_sheet
375 self.syntax_style = default_light_syntax_style
382 self.syntax_style = styles.default_light_syntax_style
376 elif colors=='linux':
383 elif colors=='linux':
377 self.style_sheet = default_dark_style_sheet
384 self.style_sheet = styles.default_dark_style_sheet
378 self.syntax_style = default_dark_syntax_style
385 self.syntax_style = styles.default_dark_syntax_style
379 elif colors=='nocolor':
386 elif colors=='nocolor':
380 self.style_sheet = default_bw_style_sheet
387 self.style_sheet = styles.default_bw_style_sheet
381 self.syntax_style = default_bw_syntax_style
388 self.syntax_style = styles.default_bw_syntax_style
382 else:
389 else:
383 raise KeyError("No such color scheme: %s"%colors)
390 raise KeyError("No such color scheme: %s"%colors)
384
391
385 #---------------------------------------------------------------------------
392 #---------------------------------------------------------------------------
386 # 'IPythonWidget' protected interface
393 # 'IPythonWidget' protected interface
387 #---------------------------------------------------------------------------
394 #---------------------------------------------------------------------------
388
395
389 def _edit(self, filename, line=None):
396 def _edit(self, filename, line=None):
390 """ Opens a Python script for editing.
397 """ Opens a Python script for editing.
391
398
392 Parameters:
399 Parameters:
393 -----------
400 -----------
394 filename : str
401 filename : str
395 A path to a local system file.
402 A path to a local system file.
396
403
397 line : int, optional
404 line : int, optional
398 A line of interest in the file.
405 A line of interest in the file.
399 """
406 """
400 if self.custom_edit:
407 if self.custom_edit:
401 self.custom_edit_requested.emit(filename, line)
408 self.custom_edit_requested.emit(filename, line)
402 elif self.editor == 'default':
409 elif self.editor == 'default':
403 self._append_plain_text('No default editor available.\n')
410 self._append_plain_text('No default editor available.\n')
404 else:
411 else:
405 try:
412 try:
406 filename = '"%s"' % filename
413 filename = '"%s"' % filename
407 if line and self.editor_line:
414 if line and self.editor_line:
408 command = self.editor_line.format(filename=filename,
415 command = self.editor_line.format(filename=filename,
409 line=line)
416 line=line)
410 else:
417 else:
411 try:
418 try:
412 command = self.editor.format()
419 command = self.editor.format()
413 except KeyError:
420 except KeyError:
414 command = self.editor.format(filename=filename)
421 command = self.editor.format(filename=filename)
415 else:
422 else:
416 command += ' ' + filename
423 command += ' ' + filename
417 except KeyError:
424 except KeyError:
418 self._append_plain_text('Invalid editor command.\n')
425 self._append_plain_text('Invalid editor command.\n')
419 else:
426 else:
420 try:
427 try:
421 Popen(command, shell=True)
428 Popen(command, shell=True)
422 except OSError:
429 except OSError:
423 msg = 'Opening editor with command "%s" failed.\n'
430 msg = 'Opening editor with command "%s" failed.\n'
424 self._append_plain_text(msg % command)
431 self._append_plain_text(msg % command)
425
432
426 def _make_in_prompt(self, number):
433 def _make_in_prompt(self, number):
427 """ Given a prompt number, returns an HTML In prompt.
434 """ Given a prompt number, returns an HTML In prompt.
428 """
435 """
429 body = self.in_prompt % number
436 body = self.in_prompt % number
430 return '<span class="in-prompt">%s</span>' % body
437 return '<span class="in-prompt">%s</span>' % body
431
438
432 def _make_continuation_prompt(self, prompt):
439 def _make_continuation_prompt(self, prompt):
433 """ Given a plain text version of an In prompt, returns an HTML
440 """ Given a plain text version of an In prompt, returns an HTML
434 continuation prompt.
441 continuation prompt.
435 """
442 """
436 end_chars = '...: '
443 end_chars = '...: '
437 space_count = len(prompt.lstrip('\n')) - len(end_chars)
444 space_count = len(prompt.lstrip('\n')) - len(end_chars)
438 body = '&nbsp;' * space_count + end_chars
445 body = '&nbsp;' * space_count + end_chars
439 return '<span class="in-prompt">%s</span>' % body
446 return '<span class="in-prompt">%s</span>' % body
440
447
441 def _make_out_prompt(self, number):
448 def _make_out_prompt(self, number):
442 """ Given a prompt number, returns an HTML Out prompt.
449 """ Given a prompt number, returns an HTML Out prompt.
443 """
450 """
444 body = self.out_prompt % number
451 body = self.out_prompt % number
445 return '<span class="out-prompt">%s</span>' % body
452 return '<span class="out-prompt">%s</span>' % body
446
453
447 #------ Payload handlers --------------------------------------------------
454 #------ Payload handlers --------------------------------------------------
448
455
449 # Payload handlers with a generic interface: each takes the opaque payload
456 # Payload handlers with a generic interface: each takes the opaque payload
450 # dict, unpacks it and calls the underlying functions with the necessary
457 # dict, unpacks it and calls the underlying functions with the necessary
451 # arguments.
458 # arguments.
452
459
453 def _handle_payload_edit(self, item):
460 def _handle_payload_edit(self, item):
454 self._edit(item['filename'], item['line_number'])
461 self._edit(item['filename'], item['line_number'])
455
462
456 def _handle_payload_exit(self, item):
463 def _handle_payload_exit(self, item):
457 self._keep_kernel_on_exit = item['keepkernel']
464 self._keep_kernel_on_exit = item['keepkernel']
458 self.exit_requested.emit()
465 self.exit_requested.emit()
459
466
460 def _handle_payload_next_input(self, item):
467 def _handle_payload_next_input(self, item):
461 self.input_buffer = dedent(item['text'].rstrip())
468 self.input_buffer = dedent(item['text'].rstrip())
462
469
463 def _handle_payload_page(self, item):
470 def _handle_payload_page(self, item):
464 # Since the plain text widget supports only a very small subset of HTML
471 # Since the plain text widget supports only a very small subset of HTML
465 # and we have no control over the HTML source, we only page HTML
472 # and we have no control over the HTML source, we only page HTML
466 # payloads in the rich text widget.
473 # payloads in the rich text widget.
467 if item['html'] and self.kind == 'rich':
474 if item['html'] and self.kind == 'rich':
468 self._page(item['html'], html=True)
475 self._page(item['html'], html=True)
469 else:
476 else:
470 self._page(item['text'], html=False)
477 self._page(item['text'], html=False)
471
478
472 #------ Trait change handlers --------------------------------------------
479 #------ Trait change handlers --------------------------------------------
473
480
474 def _style_sheet_changed(self):
481 def _style_sheet_changed(self):
475 """ Set the style sheets of the underlying widgets.
482 """ Set the style sheets of the underlying widgets.
476 """
483 """
477 self.setStyleSheet(self.style_sheet)
484 self.setStyleSheet(self.style_sheet)
478 self._control.document().setDefaultStyleSheet(self.style_sheet)
485 self._control.document().setDefaultStyleSheet(self.style_sheet)
479 if self._page_control:
486 if self._page_control:
480 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
487 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
481
488
482 bg_color = self._control.palette().window().color()
489 bg_color = self._control.palette().window().color()
483 self._ansi_processor.set_background_color(bg_color)
490 self._ansi_processor.set_background_color(bg_color)
484
491
492
485 def _syntax_style_changed(self):
493 def _syntax_style_changed(self):
486 """ Set the style for the syntax highlighter.
494 """ Set the style for the syntax highlighter.
487 """
495 """
496 if self._highlighter is None:
497 # ignore premature calls
498 return
488 if self.syntax_style:
499 if self.syntax_style:
489 self._highlighter.set_style(self.syntax_style)
500 self._highlighter.set_style(self.syntax_style)
490 else:
501 else:
491 self._highlighter.set_style_sheet(self.style_sheet)
502 self._highlighter.set_style_sheet(self.style_sheet)
492
503
@@ -1,280 +1,372 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2 """
2 """
3
3
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Imports
5 # Imports
6 #-----------------------------------------------------------------------------
6 #-----------------------------------------------------------------------------
7
7
8 # Systemm library imports
8 # stdlib imports
9 import os
10 import signal
11 import sys
12
13 # System library imports
9 from IPython.external.qt import QtGui
14 from IPython.external.qt import QtGui
10 from pygments.styles import get_all_styles
15 from pygments.styles import get_all_styles
11
16
12 # Local imports
17 # Local imports
13 from IPython.external.argparse import ArgumentParser
18 from IPython.core.newapplication import ProfileDir, BaseIPythonApplication
14 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
19 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
15 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
20 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
16 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
21 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
17 from IPython.frontend.qt.console import styles
22 from IPython.frontend.qt.console import styles
18 from IPython.frontend.qt.kernelmanager import QtKernelManager
23 from IPython.frontend.qt.kernelmanager import QtKernelManager
24 from IPython.utils.traitlets import (
25 Dict, List, Unicode, Int, CaselessStrEnum, Bool, Any
26 )
27 from IPython.zmq.ipkernel import (
28 flags as ipkernel_flags,
29 aliases as ipkernel_aliases,
30 IPKernelApp
31 )
32 from IPython.zmq.zmqshell import ZMQInteractiveShell
33
19
34
20 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
21 # Network Constants
36 # Network Constants
22 #-----------------------------------------------------------------------------
37 #-----------------------------------------------------------------------------
23
38
24 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
39 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
25
40
26 #-----------------------------------------------------------------------------
41 #-----------------------------------------------------------------------------
27 # Classes
42 # Classes
28 #-----------------------------------------------------------------------------
43 #-----------------------------------------------------------------------------
29
44
30 class MainWindow(QtGui.QMainWindow):
45 class MainWindow(QtGui.QMainWindow):
31
46
32 #---------------------------------------------------------------------------
47 #---------------------------------------------------------------------------
33 # 'object' interface
48 # 'object' interface
34 #---------------------------------------------------------------------------
49 #---------------------------------------------------------------------------
35
50
36 def __init__(self, app, frontend, existing=False, may_close=True):
51 def __init__(self, app, frontend, existing=False, may_close=True):
37 """ Create a MainWindow for the specified FrontendWidget.
52 """ Create a MainWindow for the specified FrontendWidget.
38
53
39 The app is passed as an argument to allow for different
54 The app is passed as an argument to allow for different
40 closing behavior depending on whether we are the Kernel's parent.
55 closing behavior depending on whether we are the Kernel's parent.
41
56
42 If existing is True, then this Console does not own the Kernel.
57 If existing is True, then this Console does not own the Kernel.
43
58
44 If may_close is True, then this Console is permitted to close the kernel
59 If may_close is True, then this Console is permitted to close the kernel
45 """
60 """
46 super(MainWindow, self).__init__()
61 super(MainWindow, self).__init__()
47 self._app = app
62 self._app = app
48 self._frontend = frontend
63 self._frontend = frontend
49 self._existing = existing
64 self._existing = existing
50 if existing:
65 if existing:
51 self._may_close = may_close
66 self._may_close = may_close
52 else:
67 else:
53 self._may_close = True
68 self._may_close = True
54 self._frontend.exit_requested.connect(self.close)
69 self._frontend.exit_requested.connect(self.close)
55 self.setCentralWidget(frontend)
70 self.setCentralWidget(frontend)
56
71
57 #---------------------------------------------------------------------------
72 #---------------------------------------------------------------------------
58 # QWidget interface
73 # QWidget interface
59 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
60
75
61 def closeEvent(self, event):
76 def closeEvent(self, event):
62 """ Close the window and the kernel (if necessary).
77 """ Close the window and the kernel (if necessary).
63
78
64 This will prompt the user if they are finished with the kernel, and if
79 This will prompt the user if they are finished with the kernel, and if
65 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
80 so, closes the kernel cleanly. Alternatively, if the exit magic is used,
66 it closes without prompt.
81 it closes without prompt.
67 """
82 """
68 keepkernel = None #Use the prompt by default
83 keepkernel = None #Use the prompt by default
69 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
84 if hasattr(self._frontend,'_keep_kernel_on_exit'): #set by exit magic
70 keepkernel = self._frontend._keep_kernel_on_exit
85 keepkernel = self._frontend._keep_kernel_on_exit
71
86
72 kernel_manager = self._frontend.kernel_manager
87 kernel_manager = self._frontend.kernel_manager
73
88
74 if keepkernel is None: #show prompt
89 if keepkernel is None: #show prompt
75 if kernel_manager and kernel_manager.channels_running:
90 if kernel_manager and kernel_manager.channels_running:
76 title = self.window().windowTitle()
91 title = self.window().windowTitle()
77 cancel = QtGui.QMessageBox.Cancel
92 cancel = QtGui.QMessageBox.Cancel
78 okay = QtGui.QMessageBox.Ok
93 okay = QtGui.QMessageBox.Ok
79 if self._may_close:
94 if self._may_close:
80 msg = "You are closing this Console window."
95 msg = "You are closing this Console window."
81 info = "Would you like to quit the Kernel and all attached Consoles as well?"
96 info = "Would you like to quit the Kernel and all attached Consoles as well?"
82 justthis = QtGui.QPushButton("&No, just this Console", self)
97 justthis = QtGui.QPushButton("&No, just this Console", self)
83 justthis.setShortcut('N')
98 justthis.setShortcut('N')
84 closeall = QtGui.QPushButton("&Yes, quit everything", self)
99 closeall = QtGui.QPushButton("&Yes, quit everything", self)
85 closeall.setShortcut('Y')
100 closeall.setShortcut('Y')
86 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
101 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
87 title, msg)
102 title, msg)
88 box.setInformativeText(info)
103 box.setInformativeText(info)
89 box.addButton(cancel)
104 box.addButton(cancel)
90 box.addButton(justthis, QtGui.QMessageBox.NoRole)
105 box.addButton(justthis, QtGui.QMessageBox.NoRole)
91 box.addButton(closeall, QtGui.QMessageBox.YesRole)
106 box.addButton(closeall, QtGui.QMessageBox.YesRole)
92 box.setDefaultButton(closeall)
107 box.setDefaultButton(closeall)
93 box.setEscapeButton(cancel)
108 box.setEscapeButton(cancel)
94 reply = box.exec_()
109 reply = box.exec_()
95 if reply == 1: # close All
110 if reply == 1: # close All
96 kernel_manager.shutdown_kernel()
111 kernel_manager.shutdown_kernel()
97 #kernel_manager.stop_channels()
112 #kernel_manager.stop_channels()
98 event.accept()
113 event.accept()
99 elif reply == 0: # close Console
114 elif reply == 0: # close Console
100 if not self._existing:
115 if not self._existing:
101 # Have kernel: don't quit, just close the window
116 # Have kernel: don't quit, just close the window
102 self._app.setQuitOnLastWindowClosed(False)
117 self._app.setQuitOnLastWindowClosed(False)
103 self.deleteLater()
118 self.deleteLater()
104 event.accept()
119 event.accept()
105 else:
120 else:
106 event.ignore()
121 event.ignore()
107 else:
122 else:
108 reply = QtGui.QMessageBox.question(self, title,
123 reply = QtGui.QMessageBox.question(self, title,
109 "Are you sure you want to close this Console?"+
124 "Are you sure you want to close this Console?"+
110 "\nThe Kernel and other Consoles will remain active.",
125 "\nThe Kernel and other Consoles will remain active.",
111 okay|cancel,
126 okay|cancel,
112 defaultButton=okay
127 defaultButton=okay
113 )
128 )
114 if reply == okay:
129 if reply == okay:
115 event.accept()
130 event.accept()
116 else:
131 else:
117 event.ignore()
132 event.ignore()
118 elif keepkernel: #close console but leave kernel running (no prompt)
133 elif keepkernel: #close console but leave kernel running (no prompt)
119 if kernel_manager and kernel_manager.channels_running:
134 if kernel_manager and kernel_manager.channels_running:
120 if not self._existing:
135 if not self._existing:
121 # I have the kernel: don't quit, just close the window
136 # I have the kernel: don't quit, just close the window
122 self._app.setQuitOnLastWindowClosed(False)
137 self._app.setQuitOnLastWindowClosed(False)
123 event.accept()
138 event.accept()
124 else: #close console and kernel (no prompt)
139 else: #close console and kernel (no prompt)
125 if kernel_manager and kernel_manager.channels_running:
140 if kernel_manager and kernel_manager.channels_running:
126 kernel_manager.shutdown_kernel()
141 kernel_manager.shutdown_kernel()
127 event.accept()
142 event.accept()
128
143
129 #-----------------------------------------------------------------------------
144 #-----------------------------------------------------------------------------
130 # Main entry point
145 # Aliases and Flags
131 #-----------------------------------------------------------------------------
146 #-----------------------------------------------------------------------------
132
147
133 def main():
148 flags = dict(ipkernel_flags)
134 """ Entry point for application.
149
135 """
150 flags.update({
136 # Parse command line arguments.
151 'existing' : ({'IPythonQtConsoleApp' : {'existing' : True}},
137 parser = ArgumentParser()
152 "Connect to an existing kernel."),
138 kgroup = parser.add_argument_group('kernel options')
153 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
139 kgroup.add_argument('-e', '--existing', action='store_true',
154 "Use a pure Python kernel instead of an IPython kernel."),
140 help='connect to an existing kernel')
155 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
141 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
156 "Disable rich text support."),
142 help=\
157 'gui-completion' : ({'FrontendWidget' : {'gui_completion' : True}},
143 "set the kernel\'s IP address [default localhost].\
158 "use a GUI widget for tab completion"),
144 If the IP address is something other than localhost, then \
159 })
145 Consoles on other machines will be able to connect\
160
146 to the Kernel, so be careful!")
161 qt_flags = ['existing', 'pure', 'plain', 'gui-completion']
147 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
162
148 help='set the XREQ channel port [default random]')
163 aliases = dict(ipkernel_aliases)
149 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
164
150 help='set the SUB channel port [default random]')
165 aliases.update(dict(
151 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
166 hb = 'IPythonQtConsoleApp.hb_port',
152 help='set the REP channel port [default random]')
167 shell = 'IPythonQtConsoleApp.shell_port',
153 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
168 iopub = 'IPythonQtConsoleApp.iopub_port',
154 help='set the heartbeat port [default random]')
169 stdin = 'IPythonQtConsoleApp.stdin_port',
155
170 ip = 'IPythonQtConsoleApp.ip',
156 egroup = kgroup.add_mutually_exclusive_group()
171
157 egroup.add_argument('--pure', action='store_true', help = \
172 plain = 'IPythonQtConsoleApp.plain',
158 'use a pure Python kernel instead of an IPython kernel')
173 pure = 'IPythonQtConsoleApp.pure',
159 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
174 gui_completion = 'FrontendWidget.gui_completion',
160 const='auto', help = \
175 style = 'IPythonWidget.syntax_style',
161 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
176 stylesheet = 'IPythonQtConsoleApp.stylesheet',
162 given, the GUI backend is matplotlib's, otherwise use one of: \
177 colors = 'ZMQInteractiveShell.colors',
163 ['tk', 'gtk', 'qt', 'wx', 'inline'].")
178
164
179 editor = 'IPythonWidget.editor',
165 wgroup = parser.add_argument_group('widget options')
180 pi = 'IPythonWidget.in_prompt',
166 wgroup.add_argument('--paging', type=str, default='inside',
181 po = 'IPythonWidget.out_prompt',
167 choices = ['inside', 'hsplit', 'vsplit', 'none'],
182 si = 'IPythonWidget.input_sep',
168 help='set the paging style [default inside]')
183 so = 'IPythonWidget.output_sep',
169 wgroup.add_argument('--plain', action='store_true',
184 so2 = 'IPythonWidget.output_sep2',
170 help='disable rich text support')
185 ))
171 wgroup.add_argument('--gui-completion', action='store_true',
186
172 help='use a GUI widget for tab completion')
187 #-----------------------------------------------------------------------------
173 wgroup.add_argument('--style', type=str,
188 # IPythonQtConsole
174 choices = list(get_all_styles()),
189 #-----------------------------------------------------------------------------
175 help='specify a pygments style for by name')
190
176 wgroup.add_argument('--stylesheet', type=str,
191 class IPythonQtConsoleApp(BaseIPythonApplication):
177 help='path to a custom CSS stylesheet')
192 name = 'ipython-qtconsole'
178 wgroup.add_argument('--colors', type=str, help = \
193 default_config_file_name='ipython_config.py'
179 "Set the color scheme (LightBG,Linux,NoColor). This is guessed \
194 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir]
180 based on the pygments style if not set.")
195 flags = Dict(flags)
181
196 aliases = Dict(aliases)
182 args = parser.parse_args()
197
183
198 kernel_argv = List(Unicode)
184 # parse the colors arg down to current known labels
199
185 if args.colors:
200 # connection info:
186 colors=args.colors.lower()
201 ip = Unicode(LOCALHOST, config=True,
187 if colors in ('lightbg', 'light'):
202 help="""Set the kernel\'s IP address [default localhost].
188 colors='lightbg'
203 If the IP address is something other than localhost, then
189 elif colors in ('dark', 'linux'):
204 Consoles on other machines will be able to connect
190 colors='linux'
205 to the Kernel, so be careful!"""
191 else:
206 )
192 colors='nocolor'
207 hb_port = Int(0, config=True,
193 elif args.style:
208 help="set the heartbeat port [default: random]")
194 if args.style=='bw':
209 shell_port = Int(0, config=True,
195 colors='nocolor'
210 help="set the shell (XREP) port [default: random]")
196 elif styles.dark_style(args.style):
211 iopub_port = Int(0, config=True,
197 colors='linux'
212 help="set the iopub (PUB) port [default: random]")
213 stdin_port = Int(0, config=True,
214 help="set the stdin (XREQ) port [default: random]")
215
216 existing = Bool(False, config=True,
217 help="Whether to connect to an already running Kernel.")
218
219 stylesheet = Unicode('', config=True,
220 help="path to a custom CSS stylesheet")
221
222 pure = Bool(False, config=True,
223 help="Use a pure Python kernel instead of an IPython kernel.")
224 plain = Bool(False, config=True,
225 help="Use a pure Python kernel instead of an IPython kernel.")
226
227 def _pure_changed(self, name, old, new):
228 kind = 'plain' if self.plain else 'rich'
229 self.config.ConsoleWidget.kind = kind
230 if self.pure:
231 self.widget_factory = FrontendWidget
232 elif self.plain:
233 self.widget_factory = IPythonWidget
198 else:
234 else:
199 colors='lightbg'
235 self.widget_factory = RichIPythonWidget
200 else:
236
201 colors=None
237 _plain_changed = _pure_changed
202
238
203 # Don't let Qt or ZMQ swallow KeyboardInterupts.
239 # the factory for creating a widget
204 import signal
240 widget_factory = Any(RichIPythonWidget)
205 signal.signal(signal.SIGINT, signal.SIG_DFL)
241
206
242 def parse_command_line(self, argv=None):
207 # Create a KernelManager and start a kernel.
243 super(IPythonQtConsoleApp, self).parse_command_line(argv)
208 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
244 if argv is None:
209 sub_address=(args.ip, args.sub),
245 argv = sys.argv[1:]
210 rep_address=(args.ip, args.rep),
246
211 hb_address=(args.ip, args.hb))
247 self.kernel_argv = list(argv) # copy
212 if not args.existing:
248
213 # if not args.ip in LOCAL_IPS+ALL_ALIAS:
249 # scrub frontend-specific flags
214 # raise ValueError("Must bind a local ip, such as: %s"%LOCAL_IPS)
250 for a in argv:
215
251 if a.startswith('--') and a[2:] in qt_flags:
216 kwargs = dict(ip=args.ip)
252 self.kernel_argv.remove(a)
217 if args.pure:
253
218 kwargs['ipython']=False
254 def init_kernel_manager(self):
255 # Don't let Qt or ZMQ swallow KeyboardInterupts.
256 signal.signal(signal.SIGINT, signal.SIG_DFL)
257
258 # Create a KernelManager and start a kernel.
259 self.kernel_manager = QtKernelManager(
260 xreq_address=(self.ip, self.shell_port),
261 sub_address=(self.ip, self.iopub_port),
262 rep_address=(self.ip, self.stdin_port),
263 hb_address=(self.ip, self.hb_port)
264 )
265 # start the kernel
266 if not self.existing:
267 kwargs = dict(ip=self.ip, ipython=not self.pure)
268 kwargs['extra_arguments'] = self.kernel_argv
269 self.kernel_manager.start_kernel(**kwargs)
270 self.kernel_manager.start_channels()
271
272
273 def init_qt_elements(self):
274 # Create the widget.
275 self.app = QtGui.QApplication([])
276 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
277 self.widget = self.widget_factory(config=self.config,
278 local_kernel=local_kernel)
279 self.widget.kernel_manager = self.kernel_manager
280 self.window = MainWindow(self.app, self.widget, self.existing,
281 may_close=local_kernel)
282 self.window.setWindowTitle('Python' if self.pure else 'IPython')
283
284 def init_colors(self):
285 """Configure the coloring of the widget"""
286 # Note: This will be dramatically simplified when colors
287 # are removed from the backend.
288
289 if self.pure:
290 # only IPythonWidget supports styling
291 return
292
293 # parse the colors arg down to current known labels
294 try:
295 colors = self.config.ZMQInteractiveShell.colors
296 except AttributeError:
297 colors = None
298 try:
299 style = self.config.IPythonWidget.colors
300 except AttributeError:
301 style = None
302
303 # find the value for colors:
304 if colors:
305 colors=colors.lower()
306 if colors in ('lightbg', 'light'):
307 colors='lightbg'
308 elif colors in ('dark', 'linux'):
309 colors='linux'
310 else:
311 colors='nocolor'
312 elif style:
313 if style=='bw':
314 colors='nocolor'
315 elif styles.dark_style(style):
316 colors='linux'
317 else:
318 colors='lightbg'
219 else:
319 else:
220 extra = []
320 colors=None
221 if colors:
321
222 extra.append("colors=%s"%colors)
322 # Configure the style.
223 if args.pylab:
323 widget = self.widget
224 extra.append("pylab=%s"%args.pylab)
324 if style:
225 kwargs['extra_arguments'] = extra
325 widget.style_sheet = styles.sheet_from_template(style, colors)
226
326 widget.syntax_style = style
227 kernel_manager.start_kernel(**kwargs)
228 kernel_manager.start_channels()
229
230 # Create the widget.
231 app = QtGui.QApplication([])
232 local_kernel = (not args.existing) or args.ip in LOCAL_IPS
233 if args.pure:
234 kind = 'plain' if args.plain else 'rich'
235 widget = FrontendWidget(kind=kind, paging=args.paging,
236 local_kernel=local_kernel)
237 elif args.plain:
238 widget = IPythonWidget(paging=args.paging, local_kernel=local_kernel)
239 else:
240 widget = RichIPythonWidget(paging=args.paging,
241 local_kernel=local_kernel)
242 widget.gui_completion = args.gui_completion
243 widget.kernel_manager = kernel_manager
244
245 # Configure the style.
246 if not args.pure: # only IPythonWidget supports styles
247 if args.style:
248 widget.syntax_style = args.style
249 widget.style_sheet = styles.sheet_from_template(args.style, colors)
250 widget._syntax_style_changed()
327 widget._syntax_style_changed()
251 widget._style_sheet_changed()
328 widget._style_sheet_changed()
252 elif colors:
329 elif colors:
253 # use a default style
330 # use a default style
254 widget.set_default_style(colors=colors)
331 widget.set_default_style(colors=colors)
255 else:
332 else:
256 # this is redundant for now, but allows the widget's
333 # this is redundant for now, but allows the widget's
257 # defaults to change
334 # defaults to change
258 widget.set_default_style()
335 widget.set_default_style()
259
336
260 if args.stylesheet:
337 if self.stylesheet:
261 # we got an expicit stylesheet
338 # we got an expicit stylesheet
262 if os.path.isfile(args.stylesheet):
339 if os.path.isfile(self.stylesheet):
263 with open(args.stylesheet) as f:
340 with open(self.stylesheet) as f:
264 sheet = f.read()
341 sheet = f.read()
265 widget.style_sheet = sheet
342 widget.style_sheet = sheet
266 widget._style_sheet_changed()
343 widget._style_sheet_changed()
267 else:
344 else:
268 raise IOError("Stylesheet %r not found."%args.stylesheet)
345 raise IOError("Stylesheet %r not found."%self.stylesheet)
346
347 def initialize(self, argv=None):
348 super(IPythonQtConsoleApp, self).initialize(argv)
349 self.init_kernel_manager()
350 self.init_qt_elements()
351 self.init_colors()
352
353 def start(self):
354
355 # draw the window
356 self.window.show()
269
357
270 # Create the main window.
358 # Start the application main loop.
271 window = MainWindow(app, widget, args.existing, may_close=local_kernel)
359 self.app.exec_()
272 window.setWindowTitle('Python' if args.pure else 'IPython')
273 window.show()
274
360
275 # Start the application main loop.
361 #-----------------------------------------------------------------------------
276 app.exec_()
362 # Main entry point
363 #-----------------------------------------------------------------------------
364
365 def main():
366 app = IPythonQtConsoleApp()
367 app.initialize()
368 app.start()
277
369
278
370
279 if __name__ == '__main__':
371 if __name__ == '__main__':
280 main()
372 main()
General Comments 0
You need to be logged in to leave comments. Login now