##// END OF EJS Templates
Updated docstrings, following the convention in kernelmanager.py.
Mark Voorhies -
Show More
@@ -1,1761 +1,1781 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 from os.path import commonprefix
8 from os.path import commonprefix
9 import re
9 import re
10 import sys
10 import sys
11 from textwrap import dedent
11 from textwrap import dedent
12
12
13 # System library imports
13 # System library imports
14 from PyQt4 import QtCore, QtGui
14 from PyQt4 import QtCore, QtGui
15
15
16 # Local imports
16 # Local imports
17 from IPython.config.configurable import Configurable
17 from IPython.config.configurable import Configurable
18 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
18 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
19 from IPython.utils.traitlets import Bool, Enum, Int
19 from IPython.utils.traitlets import Bool, Enum, Int
20 from ansi_code_processor import QtAnsiCodeProcessor
20 from ansi_code_processor import QtAnsiCodeProcessor
21 from completion_widget import CompletionWidget
21 from completion_widget import CompletionWidget
22
22
23 #-----------------------------------------------------------------------------
23 #-----------------------------------------------------------------------------
24 # Classes
24 # Classes
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26
26
27 class ConsoleWidget(Configurable, QtGui.QWidget):
27 class ConsoleWidget(Configurable, QtGui.QWidget):
28 """ An abstract base class for console-type widgets. This class has
28 """ An abstract base class for console-type widgets. This class has
29 functionality for:
29 functionality for:
30
30
31 * Maintaining a prompt and editing region
31 * Maintaining a prompt and editing region
32 * Providing the traditional Unix-style console keyboard shortcuts
32 * Providing the traditional Unix-style console keyboard shortcuts
33 * Performing tab completion
33 * Performing tab completion
34 * Paging text
34 * Paging text
35 * Handling ANSI escape codes
35 * Handling ANSI escape codes
36
36
37 ConsoleWidget also provides a number of utility methods that will be
37 ConsoleWidget also provides a number of utility methods that will be
38 convenient to implementors of a console-style widget.
38 convenient to implementors of a console-style widget.
39 """
39 """
40 __metaclass__ = MetaQObjectHasTraits
40 __metaclass__ = MetaQObjectHasTraits
41
41
42 #------ Configuration ------------------------------------------------------
42 #------ Configuration ------------------------------------------------------
43
43
44 # Whether to process ANSI escape codes.
44 # Whether to process ANSI escape codes.
45 ansi_codes = Bool(True, config=True)
45 ansi_codes = Bool(True, config=True)
46
46
47 # The maximum number of lines of text before truncation. Specifying a
47 # The maximum number of lines of text before truncation. Specifying a
48 # non-positive number disables text truncation (not recommended).
48 # non-positive number disables text truncation (not recommended).
49 buffer_size = Int(500, config=True)
49 buffer_size = Int(500, config=True)
50
50
51 # Whether to use a list widget or plain text output for tab completion.
51 # Whether to use a list widget or plain text output for tab completion.
52 gui_completion = Bool(False, config=True)
52 gui_completion = Bool(False, config=True)
53
53
54 # The type of underlying text widget to use. Valid values are 'plain', which
54 # The type of underlying text widget to use. Valid values are 'plain', which
55 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
55 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
56 # NOTE: this value can only be specified during initialization.
56 # NOTE: this value can only be specified during initialization.
57 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
57 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
58
58
59 # The type of paging to use. Valid values are:
59 # The type of paging to use. Valid values are:
60 # 'inside' : The widget pages like a traditional terminal.
60 # 'inside' : The widget pages like a traditional terminal.
61 # 'hsplit' : When paging is requested, the widget is split
61 # 'hsplit' : When paging is requested, the widget is split
62 # horizontally. The top pane contains the console, and the
62 # horizontally. The top pane contains the console, and the
63 # bottom pane contains the paged text.
63 # bottom pane contains the paged text.
64 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
64 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
65 # 'custom' : No action is taken by the widget beyond emitting a
65 # 'custom' : No action is taken by the widget beyond emitting a
66 # 'custom_page_requested(str)' signal.
66 # 'custom_page_requested(str)' signal.
67 # 'none' : The text is written directly to the console.
67 # 'none' : The text is written directly to the console.
68 # NOTE: this value can only be specified during initialization.
68 # NOTE: this value can only be specified during initialization.
69 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
69 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
70 default_value='inside', config=True)
70 default_value='inside', config=True)
71
71
72 # Whether to override ShortcutEvents for the keybindings defined by this
72 # Whether to override ShortcutEvents for the keybindings defined by this
73 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
73 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
74 # priority (when it has focus) over, e.g., window-level menu shortcuts.
74 # priority (when it has focus) over, e.g., window-level menu shortcuts.
75 override_shortcuts = Bool(False)
75 override_shortcuts = Bool(False)
76
76
77 #------ Signals ------------------------------------------------------------
77 #------ Signals ------------------------------------------------------------
78
78
79 # Signals that indicate ConsoleWidget state.
79 # Signals that indicate ConsoleWidget state.
80 copy_available = QtCore.pyqtSignal(bool)
80 copy_available = QtCore.pyqtSignal(bool)
81 redo_available = QtCore.pyqtSignal(bool)
81 redo_available = QtCore.pyqtSignal(bool)
82 undo_available = QtCore.pyqtSignal(bool)
82 undo_available = QtCore.pyqtSignal(bool)
83
83
84 # Signal emitted when paging is needed and the paging style has been
84 # Signal emitted when paging is needed and the paging style has been
85 # specified as 'custom'.
85 # specified as 'custom'.
86 custom_page_requested = QtCore.pyqtSignal(object)
86 custom_page_requested = QtCore.pyqtSignal(object)
87
87
88 # Signal emitted when the font is changed.
88 # Signal emitted when the font is changed.
89 font_changed = QtCore.pyqtSignal(QtGui.QFont)
89 font_changed = QtCore.pyqtSignal(QtGui.QFont)
90
90
91 #------ Protected class variables ------------------------------------------
91 #------ Protected class variables ------------------------------------------
92
92
93 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
93 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
94 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
94 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
95 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
95 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
96 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
96 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
97 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
97 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
98 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
98 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
99 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
99 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
100
100
101 _shortcuts = set(_ctrl_down_remap.keys() +
101 _shortcuts = set(_ctrl_down_remap.keys() +
102 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
102 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
103 QtCore.Qt.Key_V ])
103 QtCore.Qt.Key_V ])
104
104
105 #---------------------------------------------------------------------------
105 #---------------------------------------------------------------------------
106 # 'QObject' interface
106 # 'QObject' interface
107 #---------------------------------------------------------------------------
107 #---------------------------------------------------------------------------
108
108
109 def __init__(self, parent=None, **kw):
109 def __init__(self, parent=None, **kw):
110 """ Create a ConsoleWidget.
110 """ Create a ConsoleWidget.
111
111
112 Parameters:
112 Parameters:
113 -----------
113 -----------
114 parent : QWidget, optional [default None]
114 parent : QWidget, optional [default None]
115 The parent for this widget.
115 The parent for this widget.
116 """
116 """
117 QtGui.QWidget.__init__(self, parent)
117 QtGui.QWidget.__init__(self, parent)
118 Configurable.__init__(self, **kw)
118 Configurable.__init__(self, **kw)
119
119
120 # Create the layout and underlying text widget.
120 # Create the layout and underlying text widget.
121 layout = QtGui.QStackedLayout(self)
121 layout = QtGui.QStackedLayout(self)
122 layout.setContentsMargins(0, 0, 0, 0)
122 layout.setContentsMargins(0, 0, 0, 0)
123 self._control = self._create_control()
123 self._control = self._create_control()
124 self._page_control = None
124 self._page_control = None
125 self._splitter = None
125 self._splitter = None
126 if self.paging in ('hsplit', 'vsplit'):
126 if self.paging in ('hsplit', 'vsplit'):
127 self._splitter = QtGui.QSplitter()
127 self._splitter = QtGui.QSplitter()
128 if self.paging == 'hsplit':
128 if self.paging == 'hsplit':
129 self._splitter.setOrientation(QtCore.Qt.Horizontal)
129 self._splitter.setOrientation(QtCore.Qt.Horizontal)
130 else:
130 else:
131 self._splitter.setOrientation(QtCore.Qt.Vertical)
131 self._splitter.setOrientation(QtCore.Qt.Vertical)
132 self._splitter.addWidget(self._control)
132 self._splitter.addWidget(self._control)
133 layout.addWidget(self._splitter)
133 layout.addWidget(self._splitter)
134 else:
134 else:
135 layout.addWidget(self._control)
135 layout.addWidget(self._control)
136
136
137 # Create the paging widget, if necessary.
137 # Create the paging widget, if necessary.
138 if self.paging in ('inside', 'hsplit', 'vsplit'):
138 if self.paging in ('inside', 'hsplit', 'vsplit'):
139 self._page_control = self._create_page_control()
139 self._page_control = self._create_page_control()
140 if self._splitter:
140 if self._splitter:
141 self._page_control.hide()
141 self._page_control.hide()
142 self._splitter.addWidget(self._page_control)
142 self._splitter.addWidget(self._page_control)
143 else:
143 else:
144 layout.addWidget(self._page_control)
144 layout.addWidget(self._page_control)
145
145
146 # Initialize protected variables. Some variables contain useful state
146 # Initialize protected variables. Some variables contain useful state
147 # information for subclasses; they should be considered read-only.
147 # information for subclasses; they should be considered read-only.
148 self._ansi_processor = QtAnsiCodeProcessor()
148 self._ansi_processor = QtAnsiCodeProcessor()
149 self._completion_widget = CompletionWidget(self._control)
149 self._completion_widget = CompletionWidget(self._control)
150 self._continuation_prompt = '> '
150 self._continuation_prompt = '> '
151 self._continuation_prompt_html = None
151 self._continuation_prompt_html = None
152 self._executing = False
152 self._executing = False
153 self._filter_drag = False
153 self._filter_drag = False
154 self._filter_resize = False
154 self._filter_resize = False
155 self._prompt = ''
155 self._prompt = ''
156 self._prompt_html = None
156 self._prompt_html = None
157 self._prompt_pos = 0
157 self._prompt_pos = 0
158 self._prompt_sep = ''
158 self._prompt_sep = ''
159 self._reading = False
159 self._reading = False
160 self._reading_callback = None
160 self._reading_callback = None
161 self._tab_width = 8
161 self._tab_width = 8
162 self._text_completing_pos = 0
162 self._text_completing_pos = 0
163
163
164 # Set a monospaced font.
164 # Set a monospaced font.
165 self.reset_font()
165 self.reset_font()
166
166
167 def eventFilter(self, obj, event):
167 def eventFilter(self, obj, event):
168 """ Reimplemented to ensure a console-like behavior in the underlying
168 """ Reimplemented to ensure a console-like behavior in the underlying
169 text widgets.
169 text widgets.
170 """
170 """
171 etype = event.type()
171 etype = event.type()
172 if etype == QtCore.QEvent.KeyPress:
172 if etype == QtCore.QEvent.KeyPress:
173
173
174 # Re-map keys for all filtered widgets.
174 # Re-map keys for all filtered widgets.
175 key = event.key()
175 key = event.key()
176 if self._control_key_down(event.modifiers()) and \
176 if self._control_key_down(event.modifiers()) and \
177 key in self._ctrl_down_remap:
177 key in self._ctrl_down_remap:
178 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
178 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
179 self._ctrl_down_remap[key],
179 self._ctrl_down_remap[key],
180 QtCore.Qt.NoModifier)
180 QtCore.Qt.NoModifier)
181 QtGui.qApp.sendEvent(obj, new_event)
181 QtGui.qApp.sendEvent(obj, new_event)
182 return True
182 return True
183
183
184 elif obj == self._control:
184 elif obj == self._control:
185 return self._event_filter_console_keypress(event)
185 return self._event_filter_console_keypress(event)
186
186
187 elif obj == self._page_control:
187 elif obj == self._page_control:
188 return self._event_filter_page_keypress(event)
188 return self._event_filter_page_keypress(event)
189
189
190 # Make middle-click paste safe.
190 # Make middle-click paste safe.
191 elif etype == QtCore.QEvent.MouseButtonRelease and \
191 elif etype == QtCore.QEvent.MouseButtonRelease and \
192 event.button() == QtCore.Qt.MidButton and \
192 event.button() == QtCore.Qt.MidButton and \
193 obj == self._control.viewport():
193 obj == self._control.viewport():
194 cursor = self._control.cursorForPosition(event.pos())
194 cursor = self._control.cursorForPosition(event.pos())
195 self._control.setTextCursor(cursor)
195 self._control.setTextCursor(cursor)
196 self.paste(QtGui.QClipboard.Selection)
196 self.paste(QtGui.QClipboard.Selection)
197 return True
197 return True
198
198
199 # Manually adjust the scrollbars *after* a resize event is dispatched.
199 # Manually adjust the scrollbars *after* a resize event is dispatched.
200 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
200 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
201 self._filter_resize = True
201 self._filter_resize = True
202 QtGui.qApp.sendEvent(obj, event)
202 QtGui.qApp.sendEvent(obj, event)
203 self._adjust_scrollbars()
203 self._adjust_scrollbars()
204 self._filter_resize = False
204 self._filter_resize = False
205 return True
205 return True
206
206
207 # Override shortcuts for all filtered widgets.
207 # Override shortcuts for all filtered widgets.
208 elif etype == QtCore.QEvent.ShortcutOverride and \
208 elif etype == QtCore.QEvent.ShortcutOverride and \
209 self.override_shortcuts and \
209 self.override_shortcuts and \
210 self._control_key_down(event.modifiers()) and \
210 self._control_key_down(event.modifiers()) and \
211 event.key() in self._shortcuts:
211 event.key() in self._shortcuts:
212 event.accept()
212 event.accept()
213
213
214 # Ensure that drags are safe. The problem is that the drag starting
214 # Ensure that drags are safe. The problem is that the drag starting
215 # logic, which determines whether the drag is a Copy or Move, is locked
215 # logic, which determines whether the drag is a Copy or Move, is locked
216 # down in QTextControl. If the widget is editable, which it must be if
216 # down in QTextControl. If the widget is editable, which it must be if
217 # we're not executing, the drag will be a Move. The following hack
217 # we're not executing, the drag will be a Move. The following hack
218 # prevents QTextControl from deleting the text by clearing the selection
218 # prevents QTextControl from deleting the text by clearing the selection
219 # when a drag leave event originating from this widget is dispatched.
219 # when a drag leave event originating from this widget is dispatched.
220 # The fact that we have to clear the user's selection is unfortunate,
220 # The fact that we have to clear the user's selection is unfortunate,
221 # but the alternative--trying to prevent Qt from using its hardwired
221 # but the alternative--trying to prevent Qt from using its hardwired
222 # drag logic and writing our own--is worse.
222 # drag logic and writing our own--is worse.
223 elif etype == QtCore.QEvent.DragEnter and \
223 elif etype == QtCore.QEvent.DragEnter and \
224 obj == self._control.viewport() and \
224 obj == self._control.viewport() and \
225 event.source() == self._control.viewport():
225 event.source() == self._control.viewport():
226 self._filter_drag = True
226 self._filter_drag = True
227 elif etype == QtCore.QEvent.DragLeave and \
227 elif etype == QtCore.QEvent.DragLeave and \
228 obj == self._control.viewport() and \
228 obj == self._control.viewport() and \
229 self._filter_drag:
229 self._filter_drag:
230 cursor = self._control.textCursor()
230 cursor = self._control.textCursor()
231 cursor.clearSelection()
231 cursor.clearSelection()
232 self._control.setTextCursor(cursor)
232 self._control.setTextCursor(cursor)
233 self._filter_drag = False
233 self._filter_drag = False
234
234
235 # Ensure that drops are safe.
235 # Ensure that drops are safe.
236 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
236 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
237 cursor = self._control.cursorForPosition(event.pos())
237 cursor = self._control.cursorForPosition(event.pos())
238 if self._in_buffer(cursor.position()):
238 if self._in_buffer(cursor.position()):
239 text = unicode(event.mimeData().text())
239 text = unicode(event.mimeData().text())
240 self._insert_plain_text_into_buffer(cursor, text)
240 self._insert_plain_text_into_buffer(cursor, text)
241
241
242 # Qt is expecting to get something here--drag and drop occurs in its
242 # Qt is expecting to get something here--drag and drop occurs in its
243 # own event loop. Send a DragLeave event to end it.
243 # own event loop. Send a DragLeave event to end it.
244 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
244 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
245 return True
245 return True
246
246
247 return super(ConsoleWidget, self).eventFilter(obj, event)
247 return super(ConsoleWidget, self).eventFilter(obj, event)
248
248
249 #---------------------------------------------------------------------------
249 #---------------------------------------------------------------------------
250 # 'QWidget' interface
250 # 'QWidget' interface
251 #---------------------------------------------------------------------------
251 #---------------------------------------------------------------------------
252
252
253 def sizeHint(self):
253 def sizeHint(self):
254 """ Reimplemented to suggest a size that is 80 characters wide and
254 """ Reimplemented to suggest a size that is 80 characters wide and
255 25 lines high.
255 25 lines high.
256 """
256 """
257 font_metrics = QtGui.QFontMetrics(self.font)
257 font_metrics = QtGui.QFontMetrics(self.font)
258 margin = (self._control.frameWidth() +
258 margin = (self._control.frameWidth() +
259 self._control.document().documentMargin()) * 2
259 self._control.document().documentMargin()) * 2
260 style = self.style()
260 style = self.style()
261 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
261 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
262
262
263 # Note 1: Despite my best efforts to take the various margins into
263 # Note 1: Despite my best efforts to take the various margins into
264 # account, the width is still coming out a bit too small, so we include
264 # account, the width is still coming out a bit too small, so we include
265 # a fudge factor of one character here.
265 # a fudge factor of one character here.
266 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
266 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
267 # to a Qt bug on certain Mac OS systems where it returns 0.
267 # to a Qt bug on certain Mac OS systems where it returns 0.
268 width = font_metrics.width(' ') * 81 + margin
268 width = font_metrics.width(' ') * 81 + margin
269 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
269 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
270 if self.paging == 'hsplit':
270 if self.paging == 'hsplit':
271 width = width * 2 + splitwidth
271 width = width * 2 + splitwidth
272
272
273 height = font_metrics.height() * 25 + margin
273 height = font_metrics.height() * 25 + margin
274 if self.paging == 'vsplit':
274 if self.paging == 'vsplit':
275 height = height * 2 + splitwidth
275 height = height * 2 + splitwidth
276
276
277 return QtCore.QSize(width, height)
277 return QtCore.QSize(width, height)
278
278
279 #---------------------------------------------------------------------------
279 #---------------------------------------------------------------------------
280 # 'ConsoleWidget' public interface
280 # 'ConsoleWidget' public interface
281 #---------------------------------------------------------------------------
281 #---------------------------------------------------------------------------
282
282
283 def can_copy(self):
283 def can_copy(self):
284 """ Returns whether text can be copied to the clipboard.
284 """ Returns whether text can be copied to the clipboard.
285 """
285 """
286 return self._control.textCursor().hasSelection()
286 return self._control.textCursor().hasSelection()
287
287
288 def can_cut(self):
288 def can_cut(self):
289 """ Returns whether text can be cut to the clipboard.
289 """ Returns whether text can be cut to the clipboard.
290 """
290 """
291 cursor = self._control.textCursor()
291 cursor = self._control.textCursor()
292 return (cursor.hasSelection() and
292 return (cursor.hasSelection() and
293 self._in_buffer(cursor.anchor()) and
293 self._in_buffer(cursor.anchor()) and
294 self._in_buffer(cursor.position()))
294 self._in_buffer(cursor.position()))
295
295
296 def can_paste(self):
296 def can_paste(self):
297 """ Returns whether text can be pasted from the clipboard.
297 """ Returns whether text can be pasted from the clipboard.
298 """
298 """
299 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
299 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
300 return not QtGui.QApplication.clipboard().text().isEmpty()
300 return not QtGui.QApplication.clipboard().text().isEmpty()
301 return False
301 return False
302
302
303 def clear(self, keep_input=True):
303 def clear(self, keep_input=True):
304 """ Clear the console.
304 """ Clear the console.
305
305
306 Parameters:
306 Parameters:
307 -----------
307 -----------
308 keep_input : bool, optional (default True)
308 keep_input : bool, optional (default True)
309 If set, restores the old input buffer if a new prompt is written.
309 If set, restores the old input buffer if a new prompt is written.
310 """
310 """
311 if self._executing:
311 if self._executing:
312 self._control.clear()
312 self._control.clear()
313 else:
313 else:
314 if keep_input:
314 if keep_input:
315 input_buffer = self.input_buffer
315 input_buffer = self.input_buffer
316 self._control.clear()
316 self._control.clear()
317 self._show_prompt()
317 self._show_prompt()
318 if keep_input:
318 if keep_input:
319 self.input_buffer = input_buffer
319 self.input_buffer = input_buffer
320
320
321 def copy(self):
321 def copy(self):
322 """ Copy the currently selected text to the clipboard.
322 """ Copy the currently selected text to the clipboard.
323 """
323 """
324 self._control.copy()
324 self._control.copy()
325
325
326 def cut(self):
326 def cut(self):
327 """ Copy the currently selected text to the clipboard and delete it
327 """ Copy the currently selected text to the clipboard and delete it
328 if it's inside the input buffer.
328 if it's inside the input buffer.
329 """
329 """
330 self.copy()
330 self.copy()
331 if self.can_cut():
331 if self.can_cut():
332 self._control.textCursor().removeSelectedText()
332 self._control.textCursor().removeSelectedText()
333
333
334 def execute(self, source=None, hidden=False, interactive=False):
334 def execute(self, source=None, hidden=False, interactive=False):
335 """ Executes source or the input buffer, possibly prompting for more
335 """ Executes source or the input buffer, possibly prompting for more
336 input.
336 input.
337
337
338 Parameters:
338 Parameters:
339 -----------
339 -----------
340 source : str, optional
340 source : str, optional
341
341
342 The source to execute. If not specified, the input buffer will be
342 The source to execute. If not specified, the input buffer will be
343 used. If specified and 'hidden' is False, the input buffer will be
343 used. If specified and 'hidden' is False, the input buffer will be
344 replaced with the source before execution.
344 replaced with the source before execution.
345
345
346 hidden : bool, optional (default False)
346 hidden : bool, optional (default False)
347
347
348 If set, no output will be shown and the prompt will not be modified.
348 If set, no output will be shown and the prompt will not be modified.
349 In other words, it will be completely invisible to the user that
349 In other words, it will be completely invisible to the user that
350 an execution has occurred.
350 an execution has occurred.
351
351
352 interactive : bool, optional (default False)
352 interactive : bool, optional (default False)
353
353
354 Whether the console is to treat the source as having been manually
354 Whether the console is to treat the source as having been manually
355 entered by the user. The effect of this parameter depends on the
355 entered by the user. The effect of this parameter depends on the
356 subclass implementation.
356 subclass implementation.
357
357
358 Raises:
358 Raises:
359 -------
359 -------
360 RuntimeError
360 RuntimeError
361 If incomplete input is given and 'hidden' is True. In this case,
361 If incomplete input is given and 'hidden' is True. In this case,
362 it is not possible to prompt for more input.
362 it is not possible to prompt for more input.
363
363
364 Returns:
364 Returns:
365 --------
365 --------
366 A boolean indicating whether the source was executed.
366 A boolean indicating whether the source was executed.
367 """
367 """
368 # WARNING: The order in which things happen here is very particular, in
368 # WARNING: The order in which things happen here is very particular, in
369 # large part because our syntax highlighting is fragile. If you change
369 # large part because our syntax highlighting is fragile. If you change
370 # something, test carefully!
370 # something, test carefully!
371
371
372 # Decide what to execute.
372 # Decide what to execute.
373 if source is None:
373 if source is None:
374 source = self.input_buffer
374 source = self.input_buffer
375 if not hidden:
375 if not hidden:
376 # A newline is appended later, but it should be considered part
376 # A newline is appended later, but it should be considered part
377 # of the input buffer.
377 # of the input buffer.
378 source += '\n'
378 source += '\n'
379 elif not hidden:
379 elif not hidden:
380 self.input_buffer = source
380 self.input_buffer = source
381
381
382 # Execute the source or show a continuation prompt if it is incomplete.
382 # Execute the source or show a continuation prompt if it is incomplete.
383 complete = self._is_complete(source, interactive)
383 complete = self._is_complete(source, interactive)
384 if hidden:
384 if hidden:
385 if complete:
385 if complete:
386 self._execute(source, hidden)
386 self._execute(source, hidden)
387 else:
387 else:
388 error = 'Incomplete noninteractive input: "%s"'
388 error = 'Incomplete noninteractive input: "%s"'
389 raise RuntimeError(error % source)
389 raise RuntimeError(error % source)
390 else:
390 else:
391 if complete:
391 if complete:
392 self._append_plain_text('\n')
392 self._append_plain_text('\n')
393 self._executing_input_buffer = self.input_buffer
393 self._executing_input_buffer = self.input_buffer
394 self._executing = True
394 self._executing = True
395 self._prompt_finished()
395 self._prompt_finished()
396
396
397 # The maximum block count is only in effect during execution.
397 # The maximum block count is only in effect during execution.
398 # This ensures that _prompt_pos does not become invalid due to
398 # This ensures that _prompt_pos does not become invalid due to
399 # text truncation.
399 # text truncation.
400 self._control.document().setMaximumBlockCount(self.buffer_size)
400 self._control.document().setMaximumBlockCount(self.buffer_size)
401
401
402 # Setting a positive maximum block count will automatically
402 # Setting a positive maximum block count will automatically
403 # disable the undo/redo history, but just to be safe:
403 # disable the undo/redo history, but just to be safe:
404 self._control.setUndoRedoEnabled(False)
404 self._control.setUndoRedoEnabled(False)
405
405
406 # Perform actual execution.
406 # Perform actual execution.
407 self._execute(source, hidden)
407 self._execute(source, hidden)
408
408
409 else:
409 else:
410 # Do this inside an edit block so continuation prompts are
410 # Do this inside an edit block so continuation prompts are
411 # removed seamlessly via undo/redo.
411 # removed seamlessly via undo/redo.
412 cursor = self._get_end_cursor()
412 cursor = self._get_end_cursor()
413 cursor.beginEditBlock()
413 cursor.beginEditBlock()
414 cursor.insertText('\n')
414 cursor.insertText('\n')
415 self._insert_continuation_prompt(cursor)
415 self._insert_continuation_prompt(cursor)
416 cursor.endEditBlock()
416 cursor.endEditBlock()
417
417
418 # Do not do this inside the edit block. It works as expected
418 # Do not do this inside the edit block. It works as expected
419 # when using a QPlainTextEdit control, but does not have an
419 # when using a QPlainTextEdit control, but does not have an
420 # effect when using a QTextEdit. I believe this is a Qt bug.
420 # effect when using a QTextEdit. I believe this is a Qt bug.
421 self._control.moveCursor(QtGui.QTextCursor.End)
421 self._control.moveCursor(QtGui.QTextCursor.End)
422
422
423 return complete
423 return complete
424
424
425 def _get_input_buffer(self):
425 def _get_input_buffer(self):
426 """ The text that the user has entered entered at the current prompt.
426 """ The text that the user has entered entered at the current prompt.
427 """
427 """
428 # If we're executing, the input buffer may not even exist anymore due to
428 # If we're executing, the input buffer may not even exist anymore due to
429 # the limit imposed by 'buffer_size'. Therefore, we store it.
429 # the limit imposed by 'buffer_size'. Therefore, we store it.
430 if self._executing:
430 if self._executing:
431 return self._executing_input_buffer
431 return self._executing_input_buffer
432
432
433 cursor = self._get_end_cursor()
433 cursor = self._get_end_cursor()
434 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
434 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
435 input_buffer = unicode(cursor.selection().toPlainText())
435 input_buffer = unicode(cursor.selection().toPlainText())
436
436
437 # Strip out continuation prompts.
437 # Strip out continuation prompts.
438 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
438 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
439
439
440 def _set_input_buffer(self, string):
440 def _set_input_buffer(self, string):
441 """ Replaces the text in the input buffer with 'string'.
441 """ Replaces the text in the input buffer with 'string'.
442 """
442 """
443 # For now, it is an error to modify the input buffer during execution.
443 # For now, it is an error to modify the input buffer during execution.
444 if self._executing:
444 if self._executing:
445 raise RuntimeError("Cannot change input buffer during execution.")
445 raise RuntimeError("Cannot change input buffer during execution.")
446
446
447 # Remove old text.
447 # Remove old text.
448 cursor = self._get_end_cursor()
448 cursor = self._get_end_cursor()
449 cursor.beginEditBlock()
449 cursor.beginEditBlock()
450 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
450 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
451 cursor.removeSelectedText()
451 cursor.removeSelectedText()
452
452
453 # Insert new text with continuation prompts.
453 # Insert new text with continuation prompts.
454 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
454 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
455 cursor.endEditBlock()
455 cursor.endEditBlock()
456 self._control.moveCursor(QtGui.QTextCursor.End)
456 self._control.moveCursor(QtGui.QTextCursor.End)
457
457
458 input_buffer = property(_get_input_buffer, _set_input_buffer)
458 input_buffer = property(_get_input_buffer, _set_input_buffer)
459
459
460 def _get_font(self):
460 def _get_font(self):
461 """ The base font being used by the ConsoleWidget.
461 """ The base font being used by the ConsoleWidget.
462 """
462 """
463 return self._control.document().defaultFont()
463 return self._control.document().defaultFont()
464
464
465 def _set_font(self, font):
465 def _set_font(self, font):
466 """ Sets the base font for the ConsoleWidget to the specified QFont.
466 """ Sets the base font for the ConsoleWidget to the specified QFont.
467 """
467 """
468 font_metrics = QtGui.QFontMetrics(font)
468 font_metrics = QtGui.QFontMetrics(font)
469 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
469 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
470
470
471 self._completion_widget.setFont(font)
471 self._completion_widget.setFont(font)
472 self._control.document().setDefaultFont(font)
472 self._control.document().setDefaultFont(font)
473 if self._page_control:
473 if self._page_control:
474 self._page_control.document().setDefaultFont(font)
474 self._page_control.document().setDefaultFont(font)
475
475
476 self.font_changed.emit(font)
476 self.font_changed.emit(font)
477
477
478 font = property(_get_font, _set_font)
478 font = property(_get_font, _set_font)
479
479
480 def paste(self, mode=QtGui.QClipboard.Clipboard):
480 def paste(self, mode=QtGui.QClipboard.Clipboard):
481 """ Paste the contents of the clipboard into the input region.
481 """ Paste the contents of the clipboard into the input region.
482
482
483 Parameters:
483 Parameters:
484 -----------
484 -----------
485 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
485 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
486
486
487 Controls which part of the system clipboard is used. This can be
487 Controls which part of the system clipboard is used. This can be
488 used to access the selection clipboard in X11 and the Find buffer
488 used to access the selection clipboard in X11 and the Find buffer
489 in Mac OS. By default, the regular clipboard is used.
489 in Mac OS. By default, the regular clipboard is used.
490 """
490 """
491 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
491 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
492 # Make sure the paste is safe.
492 # Make sure the paste is safe.
493 self._keep_cursor_in_buffer()
493 self._keep_cursor_in_buffer()
494 cursor = self._control.textCursor()
494 cursor = self._control.textCursor()
495
495
496 # Remove any trailing newline, which confuses the GUI and forces the
496 # Remove any trailing newline, which confuses the GUI and forces the
497 # user to backspace.
497 # user to backspace.
498 text = unicode(QtGui.QApplication.clipboard().text(mode)).rstrip()
498 text = unicode(QtGui.QApplication.clipboard().text(mode)).rstrip()
499 self._insert_plain_text_into_buffer(cursor, dedent(text))
499 self._insert_plain_text_into_buffer(cursor, dedent(text))
500
500
501 def print_(self, printer = None):
501 def print_(self, printer = None):
502 """ Print the contents of the ConsoleWidget to the specified QPrinter.
502 """ Print the contents of the ConsoleWidget to the specified QPrinter.
503 """
503 """
504 if(printer is None):
504 if(printer is None):
505 printer = QtGui.QPrinter()
505 printer = QtGui.QPrinter()
506 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
506 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
507 return
507 return
508 self._control.print_(printer)
508 self._control.print_(printer)
509
509
510 def export_html_inline(self, parent = None):
510 def export_html_inline(self, parent = None):
511 """ Export the contents of the ConsoleWidget as HTML with inline PNGs.
512 """
511 self.export_html(parent, inline = True)
513 self.export_html(parent, inline = True)
512
514
513 def export_html(self, parent = None, inline = False):
515 def export_html(self, parent = None, inline = False):
514 """ Export the contents of the ConsoleWidget as an HTML file.
516 """ Export the contents of the ConsoleWidget as HTML.
517
518 Parameters:
519 -----------
520 inline : bool, optional [default True]
515
521
516 If inline == True, include images as inline PNGs. Otherwise,
522 If True, include images as inline PNGs. Otherwise,
517 include them as links to external PNG files, mimicking the
523 include them as links to external PNG files, mimicking
518 Firefox's "Web Page, complete" behavior.
524 Firefox's "Web Page, complete" behavior.
519 """
525 """
520 dialog = QtGui.QFileDialog(parent, 'Save HTML Document')
526 dialog = QtGui.QFileDialog(parent, 'Save HTML Document')
521 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
527 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
522 dialog.setDefaultSuffix('htm')
528 dialog.setDefaultSuffix('htm')
523 dialog.setNameFilter('HTML document (*.htm)')
529 dialog.setNameFilter('HTML document (*.htm)')
524 if dialog.exec_():
530 if dialog.exec_():
525 filename = str(dialog.selectedFiles()[0])
531 filename = str(dialog.selectedFiles()[0])
526 if(inline):
532 if(inline):
527 path = None
533 path = None
528 else:
534 else:
529 offset = filename.rfind(".")
535 offset = filename.rfind(".")
530 if(offset > 0):
536 if(offset > 0):
531 path = filename[:offset]+"_files"
537 path = filename[:offset]+"_files"
532 else:
538 else:
533 path = filename+"_files"
539 path = filename+"_files"
534 import os
540 import os
535 try:
541 try:
536 os.mkdir(path)
542 os.mkdir(path)
537 except OSError:
543 except OSError:
538 # TODO: check that this is an "already exists" error
544 # TODO: check that this is an "already exists" error
539 pass
545 pass
540
546
541 f = open(filename, 'w')
547 f = open(filename, 'w')
542 try:
548 try:
543 # N.B. this is overly restrictive, but Qt's output is
549 # N.B. this is overly restrictive, but Qt's output is
544 # predictable...
550 # predictable...
545 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
551 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
546 f.write(img_re.sub(
552 f.write(img_re.sub(
547 lambda x: self.image_tag(x, path = path, format = "png"),
553 lambda x: self.image_tag(x, path = path, format = "png"),
548 str(self._control.toHtml().toUtf8())))
554 str(self._control.toHtml().toUtf8())))
549 finally:
555 finally:
550 f.close()
556 f.close()
551 return filename
557 return filename
552 return None
558 return None
553
559
554 def export_xhtml(self, parent = None):
560 def export_xhtml(self, parent = None):
555 """ Export the contents of the ConsoleWidget as an XHTML file
561 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
556 with figures as inline SVG.
557 """
562 """
558 dialog = QtGui.QFileDialog(parent, 'Save XHTML Document')
563 dialog = QtGui.QFileDialog(parent, 'Save XHTML Document')
559 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
564 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
560 dialog.setDefaultSuffix('xml')
565 dialog.setDefaultSuffix('xml')
561 dialog.setNameFilter('XHTML document (*.xml)')
566 dialog.setNameFilter('XHTML document (*.xml)')
562 if dialog.exec_():
567 if dialog.exec_():
563 filename = str(dialog.selectedFiles()[0])
568 filename = str(dialog.selectedFiles()[0])
564 f = open(filename, 'w')
569 f = open(filename, 'w')
565 try:
570 try:
566 # N.B. this is overly restrictive, but Qt's output is
571 # N.B. this is overly restrictive, but Qt's output is
567 # predictable...
572 # predictable...
568 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
573 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
569 html = str(self._control.toHtml().toUtf8())
574 html = str(self._control.toHtml().toUtf8())
570 # Hack to make xhtml header -- note that we are not doing
575 # Hack to make xhtml header -- note that we are not doing
571 # any check for valid xml
576 # any check for valid xml
572 offset = html.find("<html>")
577 offset = html.find("<html>")
573 assert(offset > -1)
578 assert(offset > -1)
574 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
579 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
575 html[offset+6:])
580 html[offset+6:])
576 f.write(img_re.sub(
581 f.write(img_re.sub(
577 lambda x: self.image_tag(x, path = None, format = "svg"),
582 lambda x: self.image_tag(x, path = None, format = "svg"),
578 html))
583 html))
579 finally:
584 finally:
580 f.close()
585 f.close()
581 return filename
586 return filename
582 return None
587 return None
583
588
584 def image_tag(self, match, path = None):
589 def image_tag(self, match, path = None, format = "png"):
585 """ Given an re.match object matching an image name in an HTML export,
590 """ Return (X)HTML mark-up for the image-tag given by match.
586 return an appropriate substitution string for the image tag
591
587 (e.g., link, embedded image, ...). As a side effect, files may
592 Parameters
588 be generated in the directory given by path."""
593 ----------
594 match : re.SRE_Match
595 A match to an HTML image tag as exported by Qt, with
596 match.group("Name") containing the matched image ID.
597
598 path : string|None, optional [default None]
599 If not None, specifies a path to which supporting files
600 may be written (e.g., for linked images).
601 If None, all images are to be included inline.
602
603 format : "png"|"svg", optional [default "png"]
604 Format for returned or referenced images.
605
606 Subclasses supporting image display should override this
607 method.
608 """
589
609
590 # Default case -- not enough information to generate tag
610 # Default case -- not enough information to generate tag
591 return ""
611 return ""
592
612
593 def prompt_to_top(self):
613 def prompt_to_top(self):
594 """ Moves the prompt to the top of the viewport.
614 """ Moves the prompt to the top of the viewport.
595 """
615 """
596 if not self._executing:
616 if not self._executing:
597 prompt_cursor = self._get_prompt_cursor()
617 prompt_cursor = self._get_prompt_cursor()
598 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
618 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
599 self._set_cursor(prompt_cursor)
619 self._set_cursor(prompt_cursor)
600 self._set_top_cursor(prompt_cursor)
620 self._set_top_cursor(prompt_cursor)
601
621
602 def redo(self):
622 def redo(self):
603 """ Redo the last operation. If there is no operation to redo, nothing
623 """ Redo the last operation. If there is no operation to redo, nothing
604 happens.
624 happens.
605 """
625 """
606 self._control.redo()
626 self._control.redo()
607
627
608 def reset_font(self):
628 def reset_font(self):
609 """ Sets the font to the default fixed-width font for this platform.
629 """ Sets the font to the default fixed-width font for this platform.
610 """
630 """
611 if sys.platform == 'win32':
631 if sys.platform == 'win32':
612 # Consolas ships with Vista/Win7, fallback to Courier if needed
632 # Consolas ships with Vista/Win7, fallback to Courier if needed
613 family, fallback = 'Consolas', 'Courier'
633 family, fallback = 'Consolas', 'Courier'
614 elif sys.platform == 'darwin':
634 elif sys.platform == 'darwin':
615 # OSX always has Monaco, no need for a fallback
635 # OSX always has Monaco, no need for a fallback
616 family, fallback = 'Monaco', None
636 family, fallback = 'Monaco', None
617 else:
637 else:
618 # FIXME: remove Consolas as a default on Linux once our font
638 # FIXME: remove Consolas as a default on Linux once our font
619 # selections are configurable by the user.
639 # selections are configurable by the user.
620 family, fallback = 'Consolas', 'Monospace'
640 family, fallback = 'Consolas', 'Monospace'
621 font = get_font(family, fallback)
641 font = get_font(family, fallback)
622 font.setPointSize(QtGui.qApp.font().pointSize())
642 font.setPointSize(QtGui.qApp.font().pointSize())
623 font.setStyleHint(QtGui.QFont.TypeWriter)
643 font.setStyleHint(QtGui.QFont.TypeWriter)
624 self._set_font(font)
644 self._set_font(font)
625
645
626 def change_font_size(self, delta):
646 def change_font_size(self, delta):
627 """Change the font size by the specified amount (in points).
647 """Change the font size by the specified amount (in points).
628 """
648 """
629 font = self.font
649 font = self.font
630 font.setPointSize(font.pointSize() + delta)
650 font.setPointSize(font.pointSize() + delta)
631 self._set_font(font)
651 self._set_font(font)
632
652
633 def select_all(self):
653 def select_all(self):
634 """ Selects all the text in the buffer.
654 """ Selects all the text in the buffer.
635 """
655 """
636 self._control.selectAll()
656 self._control.selectAll()
637
657
638 def _get_tab_width(self):
658 def _get_tab_width(self):
639 """ The width (in terms of space characters) for tab characters.
659 """ The width (in terms of space characters) for tab characters.
640 """
660 """
641 return self._tab_width
661 return self._tab_width
642
662
643 def _set_tab_width(self, tab_width):
663 def _set_tab_width(self, tab_width):
644 """ Sets the width (in terms of space characters) for tab characters.
664 """ Sets the width (in terms of space characters) for tab characters.
645 """
665 """
646 font_metrics = QtGui.QFontMetrics(self.font)
666 font_metrics = QtGui.QFontMetrics(self.font)
647 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
667 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
648
668
649 self._tab_width = tab_width
669 self._tab_width = tab_width
650
670
651 tab_width = property(_get_tab_width, _set_tab_width)
671 tab_width = property(_get_tab_width, _set_tab_width)
652
672
653 def undo(self):
673 def undo(self):
654 """ Undo the last operation. If there is no operation to undo, nothing
674 """ Undo the last operation. If there is no operation to undo, nothing
655 happens.
675 happens.
656 """
676 """
657 self._control.undo()
677 self._control.undo()
658
678
659 #---------------------------------------------------------------------------
679 #---------------------------------------------------------------------------
660 # 'ConsoleWidget' abstract interface
680 # 'ConsoleWidget' abstract interface
661 #---------------------------------------------------------------------------
681 #---------------------------------------------------------------------------
662
682
663 def _is_complete(self, source, interactive):
683 def _is_complete(self, source, interactive):
664 """ Returns whether 'source' can be executed. When triggered by an
684 """ Returns whether 'source' can be executed. When triggered by an
665 Enter/Return key press, 'interactive' is True; otherwise, it is
685 Enter/Return key press, 'interactive' is True; otherwise, it is
666 False.
686 False.
667 """
687 """
668 raise NotImplementedError
688 raise NotImplementedError
669
689
670 def _execute(self, source, hidden):
690 def _execute(self, source, hidden):
671 """ Execute 'source'. If 'hidden', do not show any output.
691 """ Execute 'source'. If 'hidden', do not show any output.
672 """
692 """
673 raise NotImplementedError
693 raise NotImplementedError
674
694
675 def _prompt_started_hook(self):
695 def _prompt_started_hook(self):
676 """ Called immediately after a new prompt is displayed.
696 """ Called immediately after a new prompt is displayed.
677 """
697 """
678 pass
698 pass
679
699
680 def _prompt_finished_hook(self):
700 def _prompt_finished_hook(self):
681 """ Called immediately after a prompt is finished, i.e. when some input
701 """ Called immediately after a prompt is finished, i.e. when some input
682 will be processed and a new prompt displayed.
702 will be processed and a new prompt displayed.
683 """
703 """
684 pass
704 pass
685
705
686 def _up_pressed(self):
706 def _up_pressed(self):
687 """ Called when the up key is pressed. Returns whether to continue
707 """ Called when the up key is pressed. Returns whether to continue
688 processing the event.
708 processing the event.
689 """
709 """
690 return True
710 return True
691
711
692 def _down_pressed(self):
712 def _down_pressed(self):
693 """ Called when the down key is pressed. Returns whether to continue
713 """ Called when the down key is pressed. Returns whether to continue
694 processing the event.
714 processing the event.
695 """
715 """
696 return True
716 return True
697
717
698 def _tab_pressed(self):
718 def _tab_pressed(self):
699 """ Called when the tab key is pressed. Returns whether to continue
719 """ Called when the tab key is pressed. Returns whether to continue
700 processing the event.
720 processing the event.
701 """
721 """
702 return False
722 return False
703
723
704 #--------------------------------------------------------------------------
724 #--------------------------------------------------------------------------
705 # 'ConsoleWidget' protected interface
725 # 'ConsoleWidget' protected interface
706 #--------------------------------------------------------------------------
726 #--------------------------------------------------------------------------
707
727
708 def _append_html(self, html):
728 def _append_html(self, html):
709 """ Appends html at the end of the console buffer.
729 """ Appends html at the end of the console buffer.
710 """
730 """
711 cursor = self._get_end_cursor()
731 cursor = self._get_end_cursor()
712 self._insert_html(cursor, html)
732 self._insert_html(cursor, html)
713
733
714 def _append_html_fetching_plain_text(self, html):
734 def _append_html_fetching_plain_text(self, html):
715 """ Appends 'html', then returns the plain text version of it.
735 """ Appends 'html', then returns the plain text version of it.
716 """
736 """
717 cursor = self._get_end_cursor()
737 cursor = self._get_end_cursor()
718 return self._insert_html_fetching_plain_text(cursor, html)
738 return self._insert_html_fetching_plain_text(cursor, html)
719
739
720 def _append_plain_text(self, text):
740 def _append_plain_text(self, text):
721 """ Appends plain text at the end of the console buffer, processing
741 """ Appends plain text at the end of the console buffer, processing
722 ANSI codes if enabled.
742 ANSI codes if enabled.
723 """
743 """
724 cursor = self._get_end_cursor()
744 cursor = self._get_end_cursor()
725 self._insert_plain_text(cursor, text)
745 self._insert_plain_text(cursor, text)
726
746
727 def _append_plain_text_keeping_prompt(self, text):
747 def _append_plain_text_keeping_prompt(self, text):
728 """ Writes 'text' after the current prompt, then restores the old prompt
748 """ Writes 'text' after the current prompt, then restores the old prompt
729 with its old input buffer.
749 with its old input buffer.
730 """
750 """
731 input_buffer = self.input_buffer
751 input_buffer = self.input_buffer
732 self._append_plain_text('\n')
752 self._append_plain_text('\n')
733 self._prompt_finished()
753 self._prompt_finished()
734
754
735 self._append_plain_text(text)
755 self._append_plain_text(text)
736 self._show_prompt()
756 self._show_prompt()
737 self.input_buffer = input_buffer
757 self.input_buffer = input_buffer
738
758
739 def _cancel_text_completion(self):
759 def _cancel_text_completion(self):
740 """ If text completion is progress, cancel it.
760 """ If text completion is progress, cancel it.
741 """
761 """
742 if self._text_completing_pos:
762 if self._text_completing_pos:
743 self._clear_temporary_buffer()
763 self._clear_temporary_buffer()
744 self._text_completing_pos = 0
764 self._text_completing_pos = 0
745
765
746 def _clear_temporary_buffer(self):
766 def _clear_temporary_buffer(self):
747 """ Clears the "temporary text" buffer, i.e. all the text following
767 """ Clears the "temporary text" buffer, i.e. all the text following
748 the prompt region.
768 the prompt region.
749 """
769 """
750 # Select and remove all text below the input buffer.
770 # Select and remove all text below the input buffer.
751 cursor = self._get_prompt_cursor()
771 cursor = self._get_prompt_cursor()
752 prompt = self._continuation_prompt.lstrip()
772 prompt = self._continuation_prompt.lstrip()
753 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
773 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
754 temp_cursor = QtGui.QTextCursor(cursor)
774 temp_cursor = QtGui.QTextCursor(cursor)
755 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
775 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
756 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
776 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
757 if not text.startswith(prompt):
777 if not text.startswith(prompt):
758 break
778 break
759 else:
779 else:
760 # We've reached the end of the input buffer and no text follows.
780 # We've reached the end of the input buffer and no text follows.
761 return
781 return
762 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
782 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
763 cursor.movePosition(QtGui.QTextCursor.End,
783 cursor.movePosition(QtGui.QTextCursor.End,
764 QtGui.QTextCursor.KeepAnchor)
784 QtGui.QTextCursor.KeepAnchor)
765 cursor.removeSelectedText()
785 cursor.removeSelectedText()
766
786
767 # After doing this, we have no choice but to clear the undo/redo
787 # After doing this, we have no choice but to clear the undo/redo
768 # history. Otherwise, the text is not "temporary" at all, because it
788 # history. Otherwise, the text is not "temporary" at all, because it
769 # can be recalled with undo/redo. Unfortunately, Qt does not expose
789 # can be recalled with undo/redo. Unfortunately, Qt does not expose
770 # fine-grained control to the undo/redo system.
790 # fine-grained control to the undo/redo system.
771 if self._control.isUndoRedoEnabled():
791 if self._control.isUndoRedoEnabled():
772 self._control.setUndoRedoEnabled(False)
792 self._control.setUndoRedoEnabled(False)
773 self._control.setUndoRedoEnabled(True)
793 self._control.setUndoRedoEnabled(True)
774
794
775 def _complete_with_items(self, cursor, items):
795 def _complete_with_items(self, cursor, items):
776 """ Performs completion with 'items' at the specified cursor location.
796 """ Performs completion with 'items' at the specified cursor location.
777 """
797 """
778 self._cancel_text_completion()
798 self._cancel_text_completion()
779
799
780 if len(items) == 1:
800 if len(items) == 1:
781 cursor.setPosition(self._control.textCursor().position(),
801 cursor.setPosition(self._control.textCursor().position(),
782 QtGui.QTextCursor.KeepAnchor)
802 QtGui.QTextCursor.KeepAnchor)
783 cursor.insertText(items[0])
803 cursor.insertText(items[0])
784
804
785 elif len(items) > 1:
805 elif len(items) > 1:
786 current_pos = self._control.textCursor().position()
806 current_pos = self._control.textCursor().position()
787 prefix = commonprefix(items)
807 prefix = commonprefix(items)
788 if prefix:
808 if prefix:
789 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
809 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
790 cursor.insertText(prefix)
810 cursor.insertText(prefix)
791 current_pos = cursor.position()
811 current_pos = cursor.position()
792
812
793 if self.gui_completion:
813 if self.gui_completion:
794 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
814 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
795 self._completion_widget.show_items(cursor, items)
815 self._completion_widget.show_items(cursor, items)
796 else:
816 else:
797 cursor.beginEditBlock()
817 cursor.beginEditBlock()
798 self._append_plain_text('\n')
818 self._append_plain_text('\n')
799 self._page(self._format_as_columns(items))
819 self._page(self._format_as_columns(items))
800 cursor.endEditBlock()
820 cursor.endEditBlock()
801
821
802 cursor.setPosition(current_pos)
822 cursor.setPosition(current_pos)
803 self._control.moveCursor(QtGui.QTextCursor.End)
823 self._control.moveCursor(QtGui.QTextCursor.End)
804 self._control.setTextCursor(cursor)
824 self._control.setTextCursor(cursor)
805 self._text_completing_pos = current_pos
825 self._text_completing_pos = current_pos
806
826
807 def _context_menu_make(self, pos):
827 def _context_menu_make(self, pos):
808 """ Creates a context menu for the given QPoint (in widget coordinates).
828 """ Creates a context menu for the given QPoint (in widget coordinates).
809 """
829 """
810 menu = QtGui.QMenu()
830 menu = QtGui.QMenu()
811
831
812 cut_action = menu.addAction('Cut', self.cut)
832 cut_action = menu.addAction('Cut', self.cut)
813 cut_action.setEnabled(self.can_cut())
833 cut_action.setEnabled(self.can_cut())
814 cut_action.setShortcut(QtGui.QKeySequence.Cut)
834 cut_action.setShortcut(QtGui.QKeySequence.Cut)
815
835
816 copy_action = menu.addAction('Copy', self.copy)
836 copy_action = menu.addAction('Copy', self.copy)
817 copy_action.setEnabled(self.can_copy())
837 copy_action.setEnabled(self.can_copy())
818 copy_action.setShortcut(QtGui.QKeySequence.Copy)
838 copy_action.setShortcut(QtGui.QKeySequence.Copy)
819
839
820 paste_action = menu.addAction('Paste', self.paste)
840 paste_action = menu.addAction('Paste', self.paste)
821 paste_action.setEnabled(self.can_paste())
841 paste_action.setEnabled(self.can_paste())
822 paste_action.setShortcut(QtGui.QKeySequence.Paste)
842 paste_action.setShortcut(QtGui.QKeySequence.Paste)
823
843
824 menu.addSeparator()
844 menu.addSeparator()
825 menu.addAction('Select All', self.select_all)
845 menu.addAction('Select All', self.select_all)
826
846
827 menu.addSeparator()
847 menu.addSeparator()
828 print_action = menu.addAction('Print', self.print_)
848 print_action = menu.addAction('Print', self.print_)
829 print_action.setEnabled(True)
849 print_action.setEnabled(True)
830 html_action = menu.addAction('Export HTML (external PNGs)',
850 html_action = menu.addAction('Export HTML (external PNGs)',
831 self.export_html)
851 self.export_html)
832 html_action.setEnabled(True)
852 html_action.setEnabled(True)
833 html_inline_action = menu.addAction('Export HTML (inline PNGs)',
853 html_inline_action = menu.addAction('Export HTML (inline PNGs)',
834 self.export_html_inline)
854 self.export_html_inline)
835 html_inline_action.setEnabled(True)
855 html_inline_action.setEnabled(True)
836 xhtml_action = menu.addAction('Export XHTML (inline SVGs)',
856 xhtml_action = menu.addAction('Export XHTML (inline SVGs)',
837 self.export_xhtml)
857 self.export_xhtml)
838 xhtml_action.setEnabled(True)
858 xhtml_action.setEnabled(True)
839 return menu
859 return menu
840
860
841 def _control_key_down(self, modifiers, include_command=True):
861 def _control_key_down(self, modifiers, include_command=True):
842 """ Given a KeyboardModifiers flags object, return whether the Control
862 """ Given a KeyboardModifiers flags object, return whether the Control
843 key is down.
863 key is down.
844
864
845 Parameters:
865 Parameters:
846 -----------
866 -----------
847 include_command : bool, optional (default True)
867 include_command : bool, optional (default True)
848 Whether to treat the Command key as a (mutually exclusive) synonym
868 Whether to treat the Command key as a (mutually exclusive) synonym
849 for Control when in Mac OS.
869 for Control when in Mac OS.
850 """
870 """
851 # Note that on Mac OS, ControlModifier corresponds to the Command key
871 # Note that on Mac OS, ControlModifier corresponds to the Command key
852 # while MetaModifier corresponds to the Control key.
872 # while MetaModifier corresponds to the Control key.
853 if sys.platform == 'darwin':
873 if sys.platform == 'darwin':
854 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
874 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
855 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
875 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
856 else:
876 else:
857 return bool(modifiers & QtCore.Qt.ControlModifier)
877 return bool(modifiers & QtCore.Qt.ControlModifier)
858
878
859 def _create_control(self):
879 def _create_control(self):
860 """ Creates and connects the underlying text widget.
880 """ Creates and connects the underlying text widget.
861 """
881 """
862 # Create the underlying control.
882 # Create the underlying control.
863 if self.kind == 'plain':
883 if self.kind == 'plain':
864 control = QtGui.QPlainTextEdit()
884 control = QtGui.QPlainTextEdit()
865 elif self.kind == 'rich':
885 elif self.kind == 'rich':
866 control = QtGui.QTextEdit()
886 control = QtGui.QTextEdit()
867 control.setAcceptRichText(False)
887 control.setAcceptRichText(False)
868
888
869 # Install event filters. The filter on the viewport is needed for
889 # Install event filters. The filter on the viewport is needed for
870 # mouse events and drag events.
890 # mouse events and drag events.
871 control.installEventFilter(self)
891 control.installEventFilter(self)
872 control.viewport().installEventFilter(self)
892 control.viewport().installEventFilter(self)
873
893
874 # Connect signals.
894 # Connect signals.
875 control.cursorPositionChanged.connect(self._cursor_position_changed)
895 control.cursorPositionChanged.connect(self._cursor_position_changed)
876 control.customContextMenuRequested.connect(
896 control.customContextMenuRequested.connect(
877 self._custom_context_menu_requested)
897 self._custom_context_menu_requested)
878 control.copyAvailable.connect(self.copy_available)
898 control.copyAvailable.connect(self.copy_available)
879 control.redoAvailable.connect(self.redo_available)
899 control.redoAvailable.connect(self.redo_available)
880 control.undoAvailable.connect(self.undo_available)
900 control.undoAvailable.connect(self.undo_available)
881
901
882 # Hijack the document size change signal to prevent Qt from adjusting
902 # Hijack the document size change signal to prevent Qt from adjusting
883 # the viewport's scrollbar. We are relying on an implementation detail
903 # the viewport's scrollbar. We are relying on an implementation detail
884 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
904 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
885 # this functionality we cannot create a nice terminal interface.
905 # this functionality we cannot create a nice terminal interface.
886 layout = control.document().documentLayout()
906 layout = control.document().documentLayout()
887 layout.documentSizeChanged.disconnect()
907 layout.documentSizeChanged.disconnect()
888 layout.documentSizeChanged.connect(self._adjust_scrollbars)
908 layout.documentSizeChanged.connect(self._adjust_scrollbars)
889
909
890 # Configure the control.
910 # Configure the control.
891 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
911 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
892 control.setReadOnly(True)
912 control.setReadOnly(True)
893 control.setUndoRedoEnabled(False)
913 control.setUndoRedoEnabled(False)
894 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
914 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
895 return control
915 return control
896
916
897 def _create_page_control(self):
917 def _create_page_control(self):
898 """ Creates and connects the underlying paging widget.
918 """ Creates and connects the underlying paging widget.
899 """
919 """
900 if self.kind == 'plain':
920 if self.kind == 'plain':
901 control = QtGui.QPlainTextEdit()
921 control = QtGui.QPlainTextEdit()
902 elif self.kind == 'rich':
922 elif self.kind == 'rich':
903 control = QtGui.QTextEdit()
923 control = QtGui.QTextEdit()
904 control.installEventFilter(self)
924 control.installEventFilter(self)
905 control.setReadOnly(True)
925 control.setReadOnly(True)
906 control.setUndoRedoEnabled(False)
926 control.setUndoRedoEnabled(False)
907 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
927 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
908 return control
928 return control
909
929
910 def _event_filter_console_keypress(self, event):
930 def _event_filter_console_keypress(self, event):
911 """ Filter key events for the underlying text widget to create a
931 """ Filter key events for the underlying text widget to create a
912 console-like interface.
932 console-like interface.
913 """
933 """
914 intercepted = False
934 intercepted = False
915 cursor = self._control.textCursor()
935 cursor = self._control.textCursor()
916 position = cursor.position()
936 position = cursor.position()
917 key = event.key()
937 key = event.key()
918 ctrl_down = self._control_key_down(event.modifiers())
938 ctrl_down = self._control_key_down(event.modifiers())
919 alt_down = event.modifiers() & QtCore.Qt.AltModifier
939 alt_down = event.modifiers() & QtCore.Qt.AltModifier
920 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
940 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
921
941
922 #------ Special sequences ----------------------------------------------
942 #------ Special sequences ----------------------------------------------
923
943
924 if event.matches(QtGui.QKeySequence.Copy):
944 if event.matches(QtGui.QKeySequence.Copy):
925 self.copy()
945 self.copy()
926 intercepted = True
946 intercepted = True
927
947
928 elif event.matches(QtGui.QKeySequence.Cut):
948 elif event.matches(QtGui.QKeySequence.Cut):
929 self.cut()
949 self.cut()
930 intercepted = True
950 intercepted = True
931
951
932 elif event.matches(QtGui.QKeySequence.Paste):
952 elif event.matches(QtGui.QKeySequence.Paste):
933 self.paste()
953 self.paste()
934 intercepted = True
954 intercepted = True
935
955
936 #------ Special modifier logic -----------------------------------------
956 #------ Special modifier logic -----------------------------------------
937
957
938 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
958 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
939 intercepted = True
959 intercepted = True
940
960
941 # Special handling when tab completing in text mode.
961 # Special handling when tab completing in text mode.
942 self._cancel_text_completion()
962 self._cancel_text_completion()
943
963
944 if self._in_buffer(position):
964 if self._in_buffer(position):
945 if self._reading:
965 if self._reading:
946 self._append_plain_text('\n')
966 self._append_plain_text('\n')
947 self._reading = False
967 self._reading = False
948 if self._reading_callback:
968 if self._reading_callback:
949 self._reading_callback()
969 self._reading_callback()
950
970
951 # If the input buffer is a single line or there is only
971 # If the input buffer is a single line or there is only
952 # whitespace after the cursor, execute. Otherwise, split the
972 # whitespace after the cursor, execute. Otherwise, split the
953 # line with a continuation prompt.
973 # line with a continuation prompt.
954 elif not self._executing:
974 elif not self._executing:
955 cursor.movePosition(QtGui.QTextCursor.End,
975 cursor.movePosition(QtGui.QTextCursor.End,
956 QtGui.QTextCursor.KeepAnchor)
976 QtGui.QTextCursor.KeepAnchor)
957 at_end = cursor.selectedText().trimmed().isEmpty()
977 at_end = cursor.selectedText().trimmed().isEmpty()
958 single_line = (self._get_end_cursor().blockNumber() ==
978 single_line = (self._get_end_cursor().blockNumber() ==
959 self._get_prompt_cursor().blockNumber())
979 self._get_prompt_cursor().blockNumber())
960 if (at_end or shift_down or single_line) and not ctrl_down:
980 if (at_end or shift_down or single_line) and not ctrl_down:
961 self.execute(interactive = not shift_down)
981 self.execute(interactive = not shift_down)
962 else:
982 else:
963 # Do this inside an edit block for clean undo/redo.
983 # Do this inside an edit block for clean undo/redo.
964 cursor.beginEditBlock()
984 cursor.beginEditBlock()
965 cursor.setPosition(position)
985 cursor.setPosition(position)
966 cursor.insertText('\n')
986 cursor.insertText('\n')
967 self._insert_continuation_prompt(cursor)
987 self._insert_continuation_prompt(cursor)
968 cursor.endEditBlock()
988 cursor.endEditBlock()
969
989
970 # Ensure that the whole input buffer is visible.
990 # Ensure that the whole input buffer is visible.
971 # FIXME: This will not be usable if the input buffer is
991 # FIXME: This will not be usable if the input buffer is
972 # taller than the console widget.
992 # taller than the console widget.
973 self._control.moveCursor(QtGui.QTextCursor.End)
993 self._control.moveCursor(QtGui.QTextCursor.End)
974 self._control.setTextCursor(cursor)
994 self._control.setTextCursor(cursor)
975
995
976 #------ Control/Cmd modifier -------------------------------------------
996 #------ Control/Cmd modifier -------------------------------------------
977
997
978 elif ctrl_down:
998 elif ctrl_down:
979 if key == QtCore.Qt.Key_G:
999 if key == QtCore.Qt.Key_G:
980 self._keyboard_quit()
1000 self._keyboard_quit()
981 intercepted = True
1001 intercepted = True
982
1002
983 elif key == QtCore.Qt.Key_K:
1003 elif key == QtCore.Qt.Key_K:
984 if self._in_buffer(position):
1004 if self._in_buffer(position):
985 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1005 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
986 QtGui.QTextCursor.KeepAnchor)
1006 QtGui.QTextCursor.KeepAnchor)
987 if not cursor.hasSelection():
1007 if not cursor.hasSelection():
988 # Line deletion (remove continuation prompt)
1008 # Line deletion (remove continuation prompt)
989 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1009 cursor.movePosition(QtGui.QTextCursor.NextBlock,
990 QtGui.QTextCursor.KeepAnchor)
1010 QtGui.QTextCursor.KeepAnchor)
991 cursor.movePosition(QtGui.QTextCursor.Right,
1011 cursor.movePosition(QtGui.QTextCursor.Right,
992 QtGui.QTextCursor.KeepAnchor,
1012 QtGui.QTextCursor.KeepAnchor,
993 len(self._continuation_prompt))
1013 len(self._continuation_prompt))
994 cursor.removeSelectedText()
1014 cursor.removeSelectedText()
995 intercepted = True
1015 intercepted = True
996
1016
997 elif key == QtCore.Qt.Key_L:
1017 elif key == QtCore.Qt.Key_L:
998 self.prompt_to_top()
1018 self.prompt_to_top()
999 intercepted = True
1019 intercepted = True
1000
1020
1001 elif key == QtCore.Qt.Key_O:
1021 elif key == QtCore.Qt.Key_O:
1002 if self._page_control and self._page_control.isVisible():
1022 if self._page_control and self._page_control.isVisible():
1003 self._page_control.setFocus()
1023 self._page_control.setFocus()
1004 intercepted = True
1024 intercepted = True
1005
1025
1006 elif key == QtCore.Qt.Key_Y:
1026 elif key == QtCore.Qt.Key_Y:
1007 self.paste()
1027 self.paste()
1008 intercepted = True
1028 intercepted = True
1009
1029
1010 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1030 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1011 intercepted = True
1031 intercepted = True
1012
1032
1013 elif key == QtCore.Qt.Key_Plus:
1033 elif key == QtCore.Qt.Key_Plus:
1014 self.change_font_size(1)
1034 self.change_font_size(1)
1015 intercepted = True
1035 intercepted = True
1016
1036
1017 elif key == QtCore.Qt.Key_Minus:
1037 elif key == QtCore.Qt.Key_Minus:
1018 self.change_font_size(-1)
1038 self.change_font_size(-1)
1019 intercepted = True
1039 intercepted = True
1020
1040
1021 #------ Alt modifier ---------------------------------------------------
1041 #------ Alt modifier ---------------------------------------------------
1022
1042
1023 elif alt_down:
1043 elif alt_down:
1024 if key == QtCore.Qt.Key_B:
1044 if key == QtCore.Qt.Key_B:
1025 self._set_cursor(self._get_word_start_cursor(position))
1045 self._set_cursor(self._get_word_start_cursor(position))
1026 intercepted = True
1046 intercepted = True
1027
1047
1028 elif key == QtCore.Qt.Key_F:
1048 elif key == QtCore.Qt.Key_F:
1029 self._set_cursor(self._get_word_end_cursor(position))
1049 self._set_cursor(self._get_word_end_cursor(position))
1030 intercepted = True
1050 intercepted = True
1031
1051
1032 elif key == QtCore.Qt.Key_Backspace:
1052 elif key == QtCore.Qt.Key_Backspace:
1033 cursor = self._get_word_start_cursor(position)
1053 cursor = self._get_word_start_cursor(position)
1034 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1054 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1035 cursor.removeSelectedText()
1055 cursor.removeSelectedText()
1036 intercepted = True
1056 intercepted = True
1037
1057
1038 elif key == QtCore.Qt.Key_D:
1058 elif key == QtCore.Qt.Key_D:
1039 cursor = self._get_word_end_cursor(position)
1059 cursor = self._get_word_end_cursor(position)
1040 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1060 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1041 cursor.removeSelectedText()
1061 cursor.removeSelectedText()
1042 intercepted = True
1062 intercepted = True
1043
1063
1044 elif key == QtCore.Qt.Key_Delete:
1064 elif key == QtCore.Qt.Key_Delete:
1045 intercepted = True
1065 intercepted = True
1046
1066
1047 elif key == QtCore.Qt.Key_Greater:
1067 elif key == QtCore.Qt.Key_Greater:
1048 self._control.moveCursor(QtGui.QTextCursor.End)
1068 self._control.moveCursor(QtGui.QTextCursor.End)
1049 intercepted = True
1069 intercepted = True
1050
1070
1051 elif key == QtCore.Qt.Key_Less:
1071 elif key == QtCore.Qt.Key_Less:
1052 self._control.setTextCursor(self._get_prompt_cursor())
1072 self._control.setTextCursor(self._get_prompt_cursor())
1053 intercepted = True
1073 intercepted = True
1054
1074
1055 #------ No modifiers ---------------------------------------------------
1075 #------ No modifiers ---------------------------------------------------
1056
1076
1057 else:
1077 else:
1058 if shift_down:
1078 if shift_down:
1059 anchormode=QtGui.QTextCursor.KeepAnchor
1079 anchormode=QtGui.QTextCursor.KeepAnchor
1060 else:
1080 else:
1061 anchormode=QtGui.QTextCursor.MoveAnchor
1081 anchormode=QtGui.QTextCursor.MoveAnchor
1062
1082
1063 if key == QtCore.Qt.Key_Escape:
1083 if key == QtCore.Qt.Key_Escape:
1064 self._keyboard_quit()
1084 self._keyboard_quit()
1065 intercepted = True
1085 intercepted = True
1066
1086
1067 elif key == QtCore.Qt.Key_Up:
1087 elif key == QtCore.Qt.Key_Up:
1068 if self._reading or not self._up_pressed():
1088 if self._reading or not self._up_pressed():
1069 intercepted = True
1089 intercepted = True
1070 else:
1090 else:
1071 prompt_line = self._get_prompt_cursor().blockNumber()
1091 prompt_line = self._get_prompt_cursor().blockNumber()
1072 intercepted = cursor.blockNumber() <= prompt_line
1092 intercepted = cursor.blockNumber() <= prompt_line
1073
1093
1074 elif key == QtCore.Qt.Key_Down:
1094 elif key == QtCore.Qt.Key_Down:
1075 if self._reading or not self._down_pressed():
1095 if self._reading or not self._down_pressed():
1076 intercepted = True
1096 intercepted = True
1077 else:
1097 else:
1078 end_line = self._get_end_cursor().blockNumber()
1098 end_line = self._get_end_cursor().blockNumber()
1079 intercepted = cursor.blockNumber() == end_line
1099 intercepted = cursor.blockNumber() == end_line
1080
1100
1081 elif key == QtCore.Qt.Key_Tab:
1101 elif key == QtCore.Qt.Key_Tab:
1082 if not self._reading:
1102 if not self._reading:
1083 intercepted = not self._tab_pressed()
1103 intercepted = not self._tab_pressed()
1084
1104
1085 elif key == QtCore.Qt.Key_Left:
1105 elif key == QtCore.Qt.Key_Left:
1086
1106
1087 # Move to the previous line
1107 # Move to the previous line
1088 line, col = cursor.blockNumber(), cursor.columnNumber()
1108 line, col = cursor.blockNumber(), cursor.columnNumber()
1089 if line > self._get_prompt_cursor().blockNumber() and \
1109 if line > self._get_prompt_cursor().blockNumber() and \
1090 col == len(self._continuation_prompt):
1110 col == len(self._continuation_prompt):
1091 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1111 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1092 mode=anchormode)
1112 mode=anchormode)
1093 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1113 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1094 mode=anchormode)
1114 mode=anchormode)
1095 intercepted = True
1115 intercepted = True
1096
1116
1097 # Regular left movement
1117 # Regular left movement
1098 else:
1118 else:
1099 intercepted = not self._in_buffer(position - 1)
1119 intercepted = not self._in_buffer(position - 1)
1100
1120
1101 elif key == QtCore.Qt.Key_Right:
1121 elif key == QtCore.Qt.Key_Right:
1102 original_block_number = cursor.blockNumber()
1122 original_block_number = cursor.blockNumber()
1103 cursor.movePosition(QtGui.QTextCursor.Right,
1123 cursor.movePosition(QtGui.QTextCursor.Right,
1104 mode=anchormode)
1124 mode=anchormode)
1105 if cursor.blockNumber() != original_block_number:
1125 if cursor.blockNumber() != original_block_number:
1106 cursor.movePosition(QtGui.QTextCursor.Right,
1126 cursor.movePosition(QtGui.QTextCursor.Right,
1107 n=len(self._continuation_prompt),
1127 n=len(self._continuation_prompt),
1108 mode=anchormode)
1128 mode=anchormode)
1109 self._set_cursor(cursor)
1129 self._set_cursor(cursor)
1110 intercepted = True
1130 intercepted = True
1111
1131
1112 elif key == QtCore.Qt.Key_Home:
1132 elif key == QtCore.Qt.Key_Home:
1113 start_line = cursor.blockNumber()
1133 start_line = cursor.blockNumber()
1114 if start_line == self._get_prompt_cursor().blockNumber():
1134 if start_line == self._get_prompt_cursor().blockNumber():
1115 start_pos = self._prompt_pos
1135 start_pos = self._prompt_pos
1116 else:
1136 else:
1117 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1137 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1118 QtGui.QTextCursor.KeepAnchor)
1138 QtGui.QTextCursor.KeepAnchor)
1119 start_pos = cursor.position()
1139 start_pos = cursor.position()
1120 start_pos += len(self._continuation_prompt)
1140 start_pos += len(self._continuation_prompt)
1121 cursor.setPosition(position)
1141 cursor.setPosition(position)
1122 if shift_down and self._in_buffer(position):
1142 if shift_down and self._in_buffer(position):
1123 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1143 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1124 else:
1144 else:
1125 cursor.setPosition(start_pos)
1145 cursor.setPosition(start_pos)
1126 self._set_cursor(cursor)
1146 self._set_cursor(cursor)
1127 intercepted = True
1147 intercepted = True
1128
1148
1129 elif key == QtCore.Qt.Key_Backspace:
1149 elif key == QtCore.Qt.Key_Backspace:
1130
1150
1131 # Line deletion (remove continuation prompt)
1151 # Line deletion (remove continuation prompt)
1132 line, col = cursor.blockNumber(), cursor.columnNumber()
1152 line, col = cursor.blockNumber(), cursor.columnNumber()
1133 if not self._reading and \
1153 if not self._reading and \
1134 col == len(self._continuation_prompt) and \
1154 col == len(self._continuation_prompt) and \
1135 line > self._get_prompt_cursor().blockNumber():
1155 line > self._get_prompt_cursor().blockNumber():
1136 cursor.beginEditBlock()
1156 cursor.beginEditBlock()
1137 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1157 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1138 QtGui.QTextCursor.KeepAnchor)
1158 QtGui.QTextCursor.KeepAnchor)
1139 cursor.removeSelectedText()
1159 cursor.removeSelectedText()
1140 cursor.deletePreviousChar()
1160 cursor.deletePreviousChar()
1141 cursor.endEditBlock()
1161 cursor.endEditBlock()
1142 intercepted = True
1162 intercepted = True
1143
1163
1144 # Regular backwards deletion
1164 # Regular backwards deletion
1145 else:
1165 else:
1146 anchor = cursor.anchor()
1166 anchor = cursor.anchor()
1147 if anchor == position:
1167 if anchor == position:
1148 intercepted = not self._in_buffer(position - 1)
1168 intercepted = not self._in_buffer(position - 1)
1149 else:
1169 else:
1150 intercepted = not self._in_buffer(min(anchor, position))
1170 intercepted = not self._in_buffer(min(anchor, position))
1151
1171
1152 elif key == QtCore.Qt.Key_Delete:
1172 elif key == QtCore.Qt.Key_Delete:
1153
1173
1154 # Line deletion (remove continuation prompt)
1174 # Line deletion (remove continuation prompt)
1155 if not self._reading and self._in_buffer(position) and \
1175 if not self._reading and self._in_buffer(position) and \
1156 cursor.atBlockEnd() and not cursor.hasSelection():
1176 cursor.atBlockEnd() and not cursor.hasSelection():
1157 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1177 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1158 QtGui.QTextCursor.KeepAnchor)
1178 QtGui.QTextCursor.KeepAnchor)
1159 cursor.movePosition(QtGui.QTextCursor.Right,
1179 cursor.movePosition(QtGui.QTextCursor.Right,
1160 QtGui.QTextCursor.KeepAnchor,
1180 QtGui.QTextCursor.KeepAnchor,
1161 len(self._continuation_prompt))
1181 len(self._continuation_prompt))
1162 cursor.removeSelectedText()
1182 cursor.removeSelectedText()
1163 intercepted = True
1183 intercepted = True
1164
1184
1165 # Regular forwards deletion:
1185 # Regular forwards deletion:
1166 else:
1186 else:
1167 anchor = cursor.anchor()
1187 anchor = cursor.anchor()
1168 intercepted = (not self._in_buffer(anchor) or
1188 intercepted = (not self._in_buffer(anchor) or
1169 not self._in_buffer(position))
1189 not self._in_buffer(position))
1170
1190
1171 # Don't move the cursor if control is down to allow copy-paste using
1191 # Don't move the cursor if control is down to allow copy-paste using
1172 # the keyboard in any part of the buffer.
1192 # the keyboard in any part of the buffer.
1173 if not ctrl_down:
1193 if not ctrl_down:
1174 self._keep_cursor_in_buffer()
1194 self._keep_cursor_in_buffer()
1175
1195
1176 return intercepted
1196 return intercepted
1177
1197
1178 def _event_filter_page_keypress(self, event):
1198 def _event_filter_page_keypress(self, event):
1179 """ Filter key events for the paging widget to create console-like
1199 """ Filter key events for the paging widget to create console-like
1180 interface.
1200 interface.
1181 """
1201 """
1182 key = event.key()
1202 key = event.key()
1183 ctrl_down = self._control_key_down(event.modifiers())
1203 ctrl_down = self._control_key_down(event.modifiers())
1184 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1204 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1185
1205
1186 if ctrl_down:
1206 if ctrl_down:
1187 if key == QtCore.Qt.Key_O:
1207 if key == QtCore.Qt.Key_O:
1188 self._control.setFocus()
1208 self._control.setFocus()
1189 intercept = True
1209 intercept = True
1190
1210
1191 elif alt_down:
1211 elif alt_down:
1192 if key == QtCore.Qt.Key_Greater:
1212 if key == QtCore.Qt.Key_Greater:
1193 self._page_control.moveCursor(QtGui.QTextCursor.End)
1213 self._page_control.moveCursor(QtGui.QTextCursor.End)
1194 intercepted = True
1214 intercepted = True
1195
1215
1196 elif key == QtCore.Qt.Key_Less:
1216 elif key == QtCore.Qt.Key_Less:
1197 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1217 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1198 intercepted = True
1218 intercepted = True
1199
1219
1200 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1220 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1201 if self._splitter:
1221 if self._splitter:
1202 self._page_control.hide()
1222 self._page_control.hide()
1203 else:
1223 else:
1204 self.layout().setCurrentWidget(self._control)
1224 self.layout().setCurrentWidget(self._control)
1205 return True
1225 return True
1206
1226
1207 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1227 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1208 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1228 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1209 QtCore.Qt.Key_PageDown,
1229 QtCore.Qt.Key_PageDown,
1210 QtCore.Qt.NoModifier)
1230 QtCore.Qt.NoModifier)
1211 QtGui.qApp.sendEvent(self._page_control, new_event)
1231 QtGui.qApp.sendEvent(self._page_control, new_event)
1212 return True
1232 return True
1213
1233
1214 elif key == QtCore.Qt.Key_Backspace:
1234 elif key == QtCore.Qt.Key_Backspace:
1215 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1235 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1216 QtCore.Qt.Key_PageUp,
1236 QtCore.Qt.Key_PageUp,
1217 QtCore.Qt.NoModifier)
1237 QtCore.Qt.NoModifier)
1218 QtGui.qApp.sendEvent(self._page_control, new_event)
1238 QtGui.qApp.sendEvent(self._page_control, new_event)
1219 return True
1239 return True
1220
1240
1221 return False
1241 return False
1222
1242
1223 def _format_as_columns(self, items, separator=' '):
1243 def _format_as_columns(self, items, separator=' '):
1224 """ Transform a list of strings into a single string with columns.
1244 """ Transform a list of strings into a single string with columns.
1225
1245
1226 Parameters
1246 Parameters
1227 ----------
1247 ----------
1228 items : sequence of strings
1248 items : sequence of strings
1229 The strings to process.
1249 The strings to process.
1230
1250
1231 separator : str, optional [default is two spaces]
1251 separator : str, optional [default is two spaces]
1232 The string that separates columns.
1252 The string that separates columns.
1233
1253
1234 Returns
1254 Returns
1235 -------
1255 -------
1236 The formatted string.
1256 The formatted string.
1237 """
1257 """
1238 # Note: this code is adapted from columnize 0.3.2.
1258 # Note: this code is adapted from columnize 0.3.2.
1239 # See http://code.google.com/p/pycolumnize/
1259 # See http://code.google.com/p/pycolumnize/
1240
1260
1241 # Calculate the number of characters available.
1261 # Calculate the number of characters available.
1242 width = self._control.viewport().width()
1262 width = self._control.viewport().width()
1243 char_width = QtGui.QFontMetrics(self.font).width(' ')
1263 char_width = QtGui.QFontMetrics(self.font).width(' ')
1244 displaywidth = max(10, (width / char_width) - 1)
1264 displaywidth = max(10, (width / char_width) - 1)
1245
1265
1246 # Some degenerate cases.
1266 # Some degenerate cases.
1247 size = len(items)
1267 size = len(items)
1248 if size == 0:
1268 if size == 0:
1249 return '\n'
1269 return '\n'
1250 elif size == 1:
1270 elif size == 1:
1251 return '%s\n' % items[0]
1271 return '%s\n' % items[0]
1252
1272
1253 # Try every row count from 1 upwards
1273 # Try every row count from 1 upwards
1254 array_index = lambda nrows, row, col: nrows*col + row
1274 array_index = lambda nrows, row, col: nrows*col + row
1255 for nrows in range(1, size):
1275 for nrows in range(1, size):
1256 ncols = (size + nrows - 1) // nrows
1276 ncols = (size + nrows - 1) // nrows
1257 colwidths = []
1277 colwidths = []
1258 totwidth = -len(separator)
1278 totwidth = -len(separator)
1259 for col in range(ncols):
1279 for col in range(ncols):
1260 # Get max column width for this column
1280 # Get max column width for this column
1261 colwidth = 0
1281 colwidth = 0
1262 for row in range(nrows):
1282 for row in range(nrows):
1263 i = array_index(nrows, row, col)
1283 i = array_index(nrows, row, col)
1264 if i >= size: break
1284 if i >= size: break
1265 x = items[i]
1285 x = items[i]
1266 colwidth = max(colwidth, len(x))
1286 colwidth = max(colwidth, len(x))
1267 colwidths.append(colwidth)
1287 colwidths.append(colwidth)
1268 totwidth += colwidth + len(separator)
1288 totwidth += colwidth + len(separator)
1269 if totwidth > displaywidth:
1289 if totwidth > displaywidth:
1270 break
1290 break
1271 if totwidth <= displaywidth:
1291 if totwidth <= displaywidth:
1272 break
1292 break
1273
1293
1274 # The smallest number of rows computed and the max widths for each
1294 # The smallest number of rows computed and the max widths for each
1275 # column has been obtained. Now we just have to format each of the rows.
1295 # column has been obtained. Now we just have to format each of the rows.
1276 string = ''
1296 string = ''
1277 for row in range(nrows):
1297 for row in range(nrows):
1278 texts = []
1298 texts = []
1279 for col in range(ncols):
1299 for col in range(ncols):
1280 i = row + nrows*col
1300 i = row + nrows*col
1281 if i >= size:
1301 if i >= size:
1282 texts.append('')
1302 texts.append('')
1283 else:
1303 else:
1284 texts.append(items[i])
1304 texts.append(items[i])
1285 while texts and not texts[-1]:
1305 while texts and not texts[-1]:
1286 del texts[-1]
1306 del texts[-1]
1287 for col in range(len(texts)):
1307 for col in range(len(texts)):
1288 texts[col] = texts[col].ljust(colwidths[col])
1308 texts[col] = texts[col].ljust(colwidths[col])
1289 string += '%s\n' % separator.join(texts)
1309 string += '%s\n' % separator.join(texts)
1290 return string
1310 return string
1291
1311
1292 def _get_block_plain_text(self, block):
1312 def _get_block_plain_text(self, block):
1293 """ Given a QTextBlock, return its unformatted text.
1313 """ Given a QTextBlock, return its unformatted text.
1294 """
1314 """
1295 cursor = QtGui.QTextCursor(block)
1315 cursor = QtGui.QTextCursor(block)
1296 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1316 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1297 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1317 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1298 QtGui.QTextCursor.KeepAnchor)
1318 QtGui.QTextCursor.KeepAnchor)
1299 return unicode(cursor.selection().toPlainText())
1319 return unicode(cursor.selection().toPlainText())
1300
1320
1301 def _get_cursor(self):
1321 def _get_cursor(self):
1302 """ Convenience method that returns a cursor for the current position.
1322 """ Convenience method that returns a cursor for the current position.
1303 """
1323 """
1304 return self._control.textCursor()
1324 return self._control.textCursor()
1305
1325
1306 def _get_end_cursor(self):
1326 def _get_end_cursor(self):
1307 """ Convenience method that returns a cursor for the last character.
1327 """ Convenience method that returns a cursor for the last character.
1308 """
1328 """
1309 cursor = self._control.textCursor()
1329 cursor = self._control.textCursor()
1310 cursor.movePosition(QtGui.QTextCursor.End)
1330 cursor.movePosition(QtGui.QTextCursor.End)
1311 return cursor
1331 return cursor
1312
1332
1313 def _get_input_buffer_cursor_column(self):
1333 def _get_input_buffer_cursor_column(self):
1314 """ Returns the column of the cursor in the input buffer, excluding the
1334 """ Returns the column of the cursor in the input buffer, excluding the
1315 contribution by the prompt, or -1 if there is no such column.
1335 contribution by the prompt, or -1 if there is no such column.
1316 """
1336 """
1317 prompt = self._get_input_buffer_cursor_prompt()
1337 prompt = self._get_input_buffer_cursor_prompt()
1318 if prompt is None:
1338 if prompt is None:
1319 return -1
1339 return -1
1320 else:
1340 else:
1321 cursor = self._control.textCursor()
1341 cursor = self._control.textCursor()
1322 return cursor.columnNumber() - len(prompt)
1342 return cursor.columnNumber() - len(prompt)
1323
1343
1324 def _get_input_buffer_cursor_line(self):
1344 def _get_input_buffer_cursor_line(self):
1325 """ Returns the text of the line of the input buffer that contains the
1345 """ Returns the text of the line of the input buffer that contains the
1326 cursor, or None if there is no such line.
1346 cursor, or None if there is no such line.
1327 """
1347 """
1328 prompt = self._get_input_buffer_cursor_prompt()
1348 prompt = self._get_input_buffer_cursor_prompt()
1329 if prompt is None:
1349 if prompt is None:
1330 return None
1350 return None
1331 else:
1351 else:
1332 cursor = self._control.textCursor()
1352 cursor = self._control.textCursor()
1333 text = self._get_block_plain_text(cursor.block())
1353 text = self._get_block_plain_text(cursor.block())
1334 return text[len(prompt):]
1354 return text[len(prompt):]
1335
1355
1336 def _get_input_buffer_cursor_prompt(self):
1356 def _get_input_buffer_cursor_prompt(self):
1337 """ Returns the (plain text) prompt for line of the input buffer that
1357 """ Returns the (plain text) prompt for line of the input buffer that
1338 contains the cursor, or None if there is no such line.
1358 contains the cursor, or None if there is no such line.
1339 """
1359 """
1340 if self._executing:
1360 if self._executing:
1341 return None
1361 return None
1342 cursor = self._control.textCursor()
1362 cursor = self._control.textCursor()
1343 if cursor.position() >= self._prompt_pos:
1363 if cursor.position() >= self._prompt_pos:
1344 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1364 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1345 return self._prompt
1365 return self._prompt
1346 else:
1366 else:
1347 return self._continuation_prompt
1367 return self._continuation_prompt
1348 else:
1368 else:
1349 return None
1369 return None
1350
1370
1351 def _get_prompt_cursor(self):
1371 def _get_prompt_cursor(self):
1352 """ Convenience method that returns a cursor for the prompt position.
1372 """ Convenience method that returns a cursor for the prompt position.
1353 """
1373 """
1354 cursor = self._control.textCursor()
1374 cursor = self._control.textCursor()
1355 cursor.setPosition(self._prompt_pos)
1375 cursor.setPosition(self._prompt_pos)
1356 return cursor
1376 return cursor
1357
1377
1358 def _get_selection_cursor(self, start, end):
1378 def _get_selection_cursor(self, start, end):
1359 """ Convenience method that returns a cursor with text selected between
1379 """ Convenience method that returns a cursor with text selected between
1360 the positions 'start' and 'end'.
1380 the positions 'start' and 'end'.
1361 """
1381 """
1362 cursor = self._control.textCursor()
1382 cursor = self._control.textCursor()
1363 cursor.setPosition(start)
1383 cursor.setPosition(start)
1364 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1384 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1365 return cursor
1385 return cursor
1366
1386
1367 def _get_word_start_cursor(self, position):
1387 def _get_word_start_cursor(self, position):
1368 """ Find the start of the word to the left the given position. If a
1388 """ Find the start of the word to the left the given position. If a
1369 sequence of non-word characters precedes the first word, skip over
1389 sequence of non-word characters precedes the first word, skip over
1370 them. (This emulates the behavior of bash, emacs, etc.)
1390 them. (This emulates the behavior of bash, emacs, etc.)
1371 """
1391 """
1372 document = self._control.document()
1392 document = self._control.document()
1373 position -= 1
1393 position -= 1
1374 while position >= self._prompt_pos and \
1394 while position >= self._prompt_pos and \
1375 not document.characterAt(position).isLetterOrNumber():
1395 not document.characterAt(position).isLetterOrNumber():
1376 position -= 1
1396 position -= 1
1377 while position >= self._prompt_pos and \
1397 while position >= self._prompt_pos and \
1378 document.characterAt(position).isLetterOrNumber():
1398 document.characterAt(position).isLetterOrNumber():
1379 position -= 1
1399 position -= 1
1380 cursor = self._control.textCursor()
1400 cursor = self._control.textCursor()
1381 cursor.setPosition(position + 1)
1401 cursor.setPosition(position + 1)
1382 return cursor
1402 return cursor
1383
1403
1384 def _get_word_end_cursor(self, position):
1404 def _get_word_end_cursor(self, position):
1385 """ Find the end of the word to the right the given position. If a
1405 """ Find the end of the word to the right the given position. If a
1386 sequence of non-word characters precedes the first word, skip over
1406 sequence of non-word characters precedes the first word, skip over
1387 them. (This emulates the behavior of bash, emacs, etc.)
1407 them. (This emulates the behavior of bash, emacs, etc.)
1388 """
1408 """
1389 document = self._control.document()
1409 document = self._control.document()
1390 end = self._get_end_cursor().position()
1410 end = self._get_end_cursor().position()
1391 while position < end and \
1411 while position < end and \
1392 not document.characterAt(position).isLetterOrNumber():
1412 not document.characterAt(position).isLetterOrNumber():
1393 position += 1
1413 position += 1
1394 while position < end and \
1414 while position < end and \
1395 document.characterAt(position).isLetterOrNumber():
1415 document.characterAt(position).isLetterOrNumber():
1396 position += 1
1416 position += 1
1397 cursor = self._control.textCursor()
1417 cursor = self._control.textCursor()
1398 cursor.setPosition(position)
1418 cursor.setPosition(position)
1399 return cursor
1419 return cursor
1400
1420
1401 def _insert_continuation_prompt(self, cursor):
1421 def _insert_continuation_prompt(self, cursor):
1402 """ Inserts new continuation prompt using the specified cursor.
1422 """ Inserts new continuation prompt using the specified cursor.
1403 """
1423 """
1404 if self._continuation_prompt_html is None:
1424 if self._continuation_prompt_html is None:
1405 self._insert_plain_text(cursor, self._continuation_prompt)
1425 self._insert_plain_text(cursor, self._continuation_prompt)
1406 else:
1426 else:
1407 self._continuation_prompt = self._insert_html_fetching_plain_text(
1427 self._continuation_prompt = self._insert_html_fetching_plain_text(
1408 cursor, self._continuation_prompt_html)
1428 cursor, self._continuation_prompt_html)
1409
1429
1410 def _insert_html(self, cursor, html):
1430 def _insert_html(self, cursor, html):
1411 """ Inserts HTML using the specified cursor in such a way that future
1431 """ Inserts HTML using the specified cursor in such a way that future
1412 formatting is unaffected.
1432 formatting is unaffected.
1413 """
1433 """
1414 cursor.beginEditBlock()
1434 cursor.beginEditBlock()
1415 cursor.insertHtml(html)
1435 cursor.insertHtml(html)
1416
1436
1417 # After inserting HTML, the text document "remembers" it's in "html
1437 # After inserting HTML, the text document "remembers" it's in "html
1418 # mode", which means that subsequent calls adding plain text will result
1438 # mode", which means that subsequent calls adding plain text will result
1419 # in unwanted formatting, lost tab characters, etc. The following code
1439 # in unwanted formatting, lost tab characters, etc. The following code
1420 # hacks around this behavior, which I consider to be a bug in Qt, by
1440 # hacks around this behavior, which I consider to be a bug in Qt, by
1421 # (crudely) resetting the document's style state.
1441 # (crudely) resetting the document's style state.
1422 cursor.movePosition(QtGui.QTextCursor.Left,
1442 cursor.movePosition(QtGui.QTextCursor.Left,
1423 QtGui.QTextCursor.KeepAnchor)
1443 QtGui.QTextCursor.KeepAnchor)
1424 if cursor.selection().toPlainText() == ' ':
1444 if cursor.selection().toPlainText() == ' ':
1425 cursor.removeSelectedText()
1445 cursor.removeSelectedText()
1426 else:
1446 else:
1427 cursor.movePosition(QtGui.QTextCursor.Right)
1447 cursor.movePosition(QtGui.QTextCursor.Right)
1428 cursor.insertText(' ', QtGui.QTextCharFormat())
1448 cursor.insertText(' ', QtGui.QTextCharFormat())
1429 cursor.endEditBlock()
1449 cursor.endEditBlock()
1430
1450
1431 def _insert_html_fetching_plain_text(self, cursor, html):
1451 def _insert_html_fetching_plain_text(self, cursor, html):
1432 """ Inserts HTML using the specified cursor, then returns its plain text
1452 """ Inserts HTML using the specified cursor, then returns its plain text
1433 version.
1453 version.
1434 """
1454 """
1435 cursor.beginEditBlock()
1455 cursor.beginEditBlock()
1436 cursor.removeSelectedText()
1456 cursor.removeSelectedText()
1437
1457
1438 start = cursor.position()
1458 start = cursor.position()
1439 self._insert_html(cursor, html)
1459 self._insert_html(cursor, html)
1440 end = cursor.position()
1460 end = cursor.position()
1441 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1461 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1442 text = unicode(cursor.selection().toPlainText())
1462 text = unicode(cursor.selection().toPlainText())
1443
1463
1444 cursor.setPosition(end)
1464 cursor.setPosition(end)
1445 cursor.endEditBlock()
1465 cursor.endEditBlock()
1446 return text
1466 return text
1447
1467
1448 def _insert_plain_text(self, cursor, text):
1468 def _insert_plain_text(self, cursor, text):
1449 """ Inserts plain text using the specified cursor, processing ANSI codes
1469 """ Inserts plain text using the specified cursor, processing ANSI codes
1450 if enabled.
1470 if enabled.
1451 """
1471 """
1452 cursor.beginEditBlock()
1472 cursor.beginEditBlock()
1453 if self.ansi_codes:
1473 if self.ansi_codes:
1454 for substring in self._ansi_processor.split_string(text):
1474 for substring in self._ansi_processor.split_string(text):
1455 for act in self._ansi_processor.actions:
1475 for act in self._ansi_processor.actions:
1456
1476
1457 # Unlike real terminal emulators, we don't distinguish
1477 # Unlike real terminal emulators, we don't distinguish
1458 # between the screen and the scrollback buffer. A screen
1478 # between the screen and the scrollback buffer. A screen
1459 # erase request clears everything.
1479 # erase request clears everything.
1460 if act.action == 'erase' and act.area == 'screen':
1480 if act.action == 'erase' and act.area == 'screen':
1461 cursor.select(QtGui.QTextCursor.Document)
1481 cursor.select(QtGui.QTextCursor.Document)
1462 cursor.removeSelectedText()
1482 cursor.removeSelectedText()
1463
1483
1464 # Simulate a form feed by scrolling just past the last line.
1484 # Simulate a form feed by scrolling just past the last line.
1465 elif act.action == 'scroll' and act.unit == 'page':
1485 elif act.action == 'scroll' and act.unit == 'page':
1466 cursor.insertText('\n')
1486 cursor.insertText('\n')
1467 cursor.endEditBlock()
1487 cursor.endEditBlock()
1468 self._set_top_cursor(cursor)
1488 self._set_top_cursor(cursor)
1469 cursor.joinPreviousEditBlock()
1489 cursor.joinPreviousEditBlock()
1470 cursor.deletePreviousChar()
1490 cursor.deletePreviousChar()
1471
1491
1472 format = self._ansi_processor.get_format()
1492 format = self._ansi_processor.get_format()
1473 cursor.insertText(substring, format)
1493 cursor.insertText(substring, format)
1474 else:
1494 else:
1475 cursor.insertText(text)
1495 cursor.insertText(text)
1476 cursor.endEditBlock()
1496 cursor.endEditBlock()
1477
1497
1478 def _insert_plain_text_into_buffer(self, cursor, text):
1498 def _insert_plain_text_into_buffer(self, cursor, text):
1479 """ Inserts text into the input buffer using the specified cursor (which
1499 """ Inserts text into the input buffer using the specified cursor (which
1480 must be in the input buffer), ensuring that continuation prompts are
1500 must be in the input buffer), ensuring that continuation prompts are
1481 inserted as necessary.
1501 inserted as necessary.
1482 """
1502 """
1483 lines = unicode(text).splitlines(True)
1503 lines = unicode(text).splitlines(True)
1484 if lines:
1504 if lines:
1485 cursor.beginEditBlock()
1505 cursor.beginEditBlock()
1486 cursor.insertText(lines[0])
1506 cursor.insertText(lines[0])
1487 for line in lines[1:]:
1507 for line in lines[1:]:
1488 if self._continuation_prompt_html is None:
1508 if self._continuation_prompt_html is None:
1489 cursor.insertText(self._continuation_prompt)
1509 cursor.insertText(self._continuation_prompt)
1490 else:
1510 else:
1491 self._continuation_prompt = \
1511 self._continuation_prompt = \
1492 self._insert_html_fetching_plain_text(
1512 self._insert_html_fetching_plain_text(
1493 cursor, self._continuation_prompt_html)
1513 cursor, self._continuation_prompt_html)
1494 cursor.insertText(line)
1514 cursor.insertText(line)
1495 cursor.endEditBlock()
1515 cursor.endEditBlock()
1496
1516
1497 def _in_buffer(self, position=None):
1517 def _in_buffer(self, position=None):
1498 """ Returns whether the current cursor (or, if specified, a position) is
1518 """ Returns whether the current cursor (or, if specified, a position) is
1499 inside the editing region.
1519 inside the editing region.
1500 """
1520 """
1501 cursor = self._control.textCursor()
1521 cursor = self._control.textCursor()
1502 if position is None:
1522 if position is None:
1503 position = cursor.position()
1523 position = cursor.position()
1504 else:
1524 else:
1505 cursor.setPosition(position)
1525 cursor.setPosition(position)
1506 line = cursor.blockNumber()
1526 line = cursor.blockNumber()
1507 prompt_line = self._get_prompt_cursor().blockNumber()
1527 prompt_line = self._get_prompt_cursor().blockNumber()
1508 if line == prompt_line:
1528 if line == prompt_line:
1509 return position >= self._prompt_pos
1529 return position >= self._prompt_pos
1510 elif line > prompt_line:
1530 elif line > prompt_line:
1511 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1531 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1512 prompt_pos = cursor.position() + len(self._continuation_prompt)
1532 prompt_pos = cursor.position() + len(self._continuation_prompt)
1513 return position >= prompt_pos
1533 return position >= prompt_pos
1514 return False
1534 return False
1515
1535
1516 def _keep_cursor_in_buffer(self):
1536 def _keep_cursor_in_buffer(self):
1517 """ Ensures that the cursor is inside the editing region. Returns
1537 """ Ensures that the cursor is inside the editing region. Returns
1518 whether the cursor was moved.
1538 whether the cursor was moved.
1519 """
1539 """
1520 moved = not self._in_buffer()
1540 moved = not self._in_buffer()
1521 if moved:
1541 if moved:
1522 cursor = self._control.textCursor()
1542 cursor = self._control.textCursor()
1523 cursor.movePosition(QtGui.QTextCursor.End)
1543 cursor.movePosition(QtGui.QTextCursor.End)
1524 self._control.setTextCursor(cursor)
1544 self._control.setTextCursor(cursor)
1525 return moved
1545 return moved
1526
1546
1527 def _keyboard_quit(self):
1547 def _keyboard_quit(self):
1528 """ Cancels the current editing task ala Ctrl-G in Emacs.
1548 """ Cancels the current editing task ala Ctrl-G in Emacs.
1529 """
1549 """
1530 if self._text_completing_pos:
1550 if self._text_completing_pos:
1531 self._cancel_text_completion()
1551 self._cancel_text_completion()
1532 else:
1552 else:
1533 self.input_buffer = ''
1553 self.input_buffer = ''
1534
1554
1535 def _page(self, text, html=False):
1555 def _page(self, text, html=False):
1536 """ Displays text using the pager if it exceeds the height of the
1556 """ Displays text using the pager if it exceeds the height of the
1537 viewport.
1557 viewport.
1538
1558
1539 Parameters:
1559 Parameters:
1540 -----------
1560 -----------
1541 html : bool, optional (default False)
1561 html : bool, optional (default False)
1542 If set, the text will be interpreted as HTML instead of plain text.
1562 If set, the text will be interpreted as HTML instead of plain text.
1543 """
1563 """
1544 line_height = QtGui.QFontMetrics(self.font).height()
1564 line_height = QtGui.QFontMetrics(self.font).height()
1545 minlines = self._control.viewport().height() / line_height
1565 minlines = self._control.viewport().height() / line_height
1546 if self.paging != 'none' and \
1566 if self.paging != 'none' and \
1547 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1567 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1548 if self.paging == 'custom':
1568 if self.paging == 'custom':
1549 self.custom_page_requested.emit(text)
1569 self.custom_page_requested.emit(text)
1550 else:
1570 else:
1551 self._page_control.clear()
1571 self._page_control.clear()
1552 cursor = self._page_control.textCursor()
1572 cursor = self._page_control.textCursor()
1553 if html:
1573 if html:
1554 self._insert_html(cursor, text)
1574 self._insert_html(cursor, text)
1555 else:
1575 else:
1556 self._insert_plain_text(cursor, text)
1576 self._insert_plain_text(cursor, text)
1557 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1577 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1558
1578
1559 self._page_control.viewport().resize(self._control.size())
1579 self._page_control.viewport().resize(self._control.size())
1560 if self._splitter:
1580 if self._splitter:
1561 self._page_control.show()
1581 self._page_control.show()
1562 self._page_control.setFocus()
1582 self._page_control.setFocus()
1563 else:
1583 else:
1564 self.layout().setCurrentWidget(self._page_control)
1584 self.layout().setCurrentWidget(self._page_control)
1565 elif html:
1585 elif html:
1566 self._append_plain_html(text)
1586 self._append_plain_html(text)
1567 else:
1587 else:
1568 self._append_plain_text(text)
1588 self._append_plain_text(text)
1569
1589
1570 def _prompt_finished(self):
1590 def _prompt_finished(self):
1571 """ Called immediately after a prompt is finished, i.e. when some input
1591 """ Called immediately after a prompt is finished, i.e. when some input
1572 will be processed and a new prompt displayed.
1592 will be processed and a new prompt displayed.
1573 """
1593 """
1574 # Flush all state from the input splitter so the next round of
1594 # Flush all state from the input splitter so the next round of
1575 # reading input starts with a clean buffer.
1595 # reading input starts with a clean buffer.
1576 self._input_splitter.reset()
1596 self._input_splitter.reset()
1577
1597
1578 self._control.setReadOnly(True)
1598 self._control.setReadOnly(True)
1579 self._prompt_finished_hook()
1599 self._prompt_finished_hook()
1580
1600
1581 def _prompt_started(self):
1601 def _prompt_started(self):
1582 """ Called immediately after a new prompt is displayed.
1602 """ Called immediately after a new prompt is displayed.
1583 """
1603 """
1584 # Temporarily disable the maximum block count to permit undo/redo and
1604 # Temporarily disable the maximum block count to permit undo/redo and
1585 # to ensure that the prompt position does not change due to truncation.
1605 # to ensure that the prompt position does not change due to truncation.
1586 self._control.document().setMaximumBlockCount(0)
1606 self._control.document().setMaximumBlockCount(0)
1587 self._control.setUndoRedoEnabled(True)
1607 self._control.setUndoRedoEnabled(True)
1588
1608
1589 self._control.setReadOnly(False)
1609 self._control.setReadOnly(False)
1590 self._control.moveCursor(QtGui.QTextCursor.End)
1610 self._control.moveCursor(QtGui.QTextCursor.End)
1591 self._executing = False
1611 self._executing = False
1592 self._prompt_started_hook()
1612 self._prompt_started_hook()
1593
1613
1594 def _readline(self, prompt='', callback=None):
1614 def _readline(self, prompt='', callback=None):
1595 """ Reads one line of input from the user.
1615 """ Reads one line of input from the user.
1596
1616
1597 Parameters
1617 Parameters
1598 ----------
1618 ----------
1599 prompt : str, optional
1619 prompt : str, optional
1600 The prompt to print before reading the line.
1620 The prompt to print before reading the line.
1601
1621
1602 callback : callable, optional
1622 callback : callable, optional
1603 A callback to execute with the read line. If not specified, input is
1623 A callback to execute with the read line. If not specified, input is
1604 read *synchronously* and this method does not return until it has
1624 read *synchronously* and this method does not return until it has
1605 been read.
1625 been read.
1606
1626
1607 Returns
1627 Returns
1608 -------
1628 -------
1609 If a callback is specified, returns nothing. Otherwise, returns the
1629 If a callback is specified, returns nothing. Otherwise, returns the
1610 input string with the trailing newline stripped.
1630 input string with the trailing newline stripped.
1611 """
1631 """
1612 if self._reading:
1632 if self._reading:
1613 raise RuntimeError('Cannot read a line. Widget is already reading.')
1633 raise RuntimeError('Cannot read a line. Widget is already reading.')
1614
1634
1615 if not callback and not self.isVisible():
1635 if not callback and not self.isVisible():
1616 # If the user cannot see the widget, this function cannot return.
1636 # If the user cannot see the widget, this function cannot return.
1617 raise RuntimeError('Cannot synchronously read a line if the widget '
1637 raise RuntimeError('Cannot synchronously read a line if the widget '
1618 'is not visible!')
1638 'is not visible!')
1619
1639
1620 self._reading = True
1640 self._reading = True
1621 self._show_prompt(prompt, newline=False)
1641 self._show_prompt(prompt, newline=False)
1622
1642
1623 if callback is None:
1643 if callback is None:
1624 self._reading_callback = None
1644 self._reading_callback = None
1625 while self._reading:
1645 while self._reading:
1626 QtCore.QCoreApplication.processEvents()
1646 QtCore.QCoreApplication.processEvents()
1627 return self.input_buffer.rstrip('\n')
1647 return self.input_buffer.rstrip('\n')
1628
1648
1629 else:
1649 else:
1630 self._reading_callback = lambda: \
1650 self._reading_callback = lambda: \
1631 callback(self.input_buffer.rstrip('\n'))
1651 callback(self.input_buffer.rstrip('\n'))
1632
1652
1633 def _set_continuation_prompt(self, prompt, html=False):
1653 def _set_continuation_prompt(self, prompt, html=False):
1634 """ Sets the continuation prompt.
1654 """ Sets the continuation prompt.
1635
1655
1636 Parameters
1656 Parameters
1637 ----------
1657 ----------
1638 prompt : str
1658 prompt : str
1639 The prompt to show when more input is needed.
1659 The prompt to show when more input is needed.
1640
1660
1641 html : bool, optional (default False)
1661 html : bool, optional (default False)
1642 If set, the prompt will be inserted as formatted HTML. Otherwise,
1662 If set, the prompt will be inserted as formatted HTML. Otherwise,
1643 the prompt will be treated as plain text, though ANSI color codes
1663 the prompt will be treated as plain text, though ANSI color codes
1644 will be handled.
1664 will be handled.
1645 """
1665 """
1646 if html:
1666 if html:
1647 self._continuation_prompt_html = prompt
1667 self._continuation_prompt_html = prompt
1648 else:
1668 else:
1649 self._continuation_prompt = prompt
1669 self._continuation_prompt = prompt
1650 self._continuation_prompt_html = None
1670 self._continuation_prompt_html = None
1651
1671
1652 def _set_cursor(self, cursor):
1672 def _set_cursor(self, cursor):
1653 """ Convenience method to set the current cursor.
1673 """ Convenience method to set the current cursor.
1654 """
1674 """
1655 self._control.setTextCursor(cursor)
1675 self._control.setTextCursor(cursor)
1656
1676
1657 def _set_top_cursor(self, cursor):
1677 def _set_top_cursor(self, cursor):
1658 """ Scrolls the viewport so that the specified cursor is at the top.
1678 """ Scrolls the viewport so that the specified cursor is at the top.
1659 """
1679 """
1660 scrollbar = self._control.verticalScrollBar()
1680 scrollbar = self._control.verticalScrollBar()
1661 scrollbar.setValue(scrollbar.maximum())
1681 scrollbar.setValue(scrollbar.maximum())
1662 original_cursor = self._control.textCursor()
1682 original_cursor = self._control.textCursor()
1663 self._control.setTextCursor(cursor)
1683 self._control.setTextCursor(cursor)
1664 self._control.ensureCursorVisible()
1684 self._control.ensureCursorVisible()
1665 self._control.setTextCursor(original_cursor)
1685 self._control.setTextCursor(original_cursor)
1666
1686
1667 def _show_prompt(self, prompt=None, html=False, newline=True):
1687 def _show_prompt(self, prompt=None, html=False, newline=True):
1668 """ Writes a new prompt at the end of the buffer.
1688 """ Writes a new prompt at the end of the buffer.
1669
1689
1670 Parameters
1690 Parameters
1671 ----------
1691 ----------
1672 prompt : str, optional
1692 prompt : str, optional
1673 The prompt to show. If not specified, the previous prompt is used.
1693 The prompt to show. If not specified, the previous prompt is used.
1674
1694
1675 html : bool, optional (default False)
1695 html : bool, optional (default False)
1676 Only relevant when a prompt is specified. If set, the prompt will
1696 Only relevant when a prompt is specified. If set, the prompt will
1677 be inserted as formatted HTML. Otherwise, the prompt will be treated
1697 be inserted as formatted HTML. Otherwise, the prompt will be treated
1678 as plain text, though ANSI color codes will be handled.
1698 as plain text, though ANSI color codes will be handled.
1679
1699
1680 newline : bool, optional (default True)
1700 newline : bool, optional (default True)
1681 If set, a new line will be written before showing the prompt if
1701 If set, a new line will be written before showing the prompt if
1682 there is not already a newline at the end of the buffer.
1702 there is not already a newline at the end of the buffer.
1683 """
1703 """
1684 # Insert a preliminary newline, if necessary.
1704 # Insert a preliminary newline, if necessary.
1685 if newline:
1705 if newline:
1686 cursor = self._get_end_cursor()
1706 cursor = self._get_end_cursor()
1687 if cursor.position() > 0:
1707 if cursor.position() > 0:
1688 cursor.movePosition(QtGui.QTextCursor.Left,
1708 cursor.movePosition(QtGui.QTextCursor.Left,
1689 QtGui.QTextCursor.KeepAnchor)
1709 QtGui.QTextCursor.KeepAnchor)
1690 if unicode(cursor.selection().toPlainText()) != '\n':
1710 if unicode(cursor.selection().toPlainText()) != '\n':
1691 self._append_plain_text('\n')
1711 self._append_plain_text('\n')
1692
1712
1693 # Write the prompt.
1713 # Write the prompt.
1694 self._append_plain_text(self._prompt_sep)
1714 self._append_plain_text(self._prompt_sep)
1695 if prompt is None:
1715 if prompt is None:
1696 if self._prompt_html is None:
1716 if self._prompt_html is None:
1697 self._append_plain_text(self._prompt)
1717 self._append_plain_text(self._prompt)
1698 else:
1718 else:
1699 self._append_html(self._prompt_html)
1719 self._append_html(self._prompt_html)
1700 else:
1720 else:
1701 if html:
1721 if html:
1702 self._prompt = self._append_html_fetching_plain_text(prompt)
1722 self._prompt = self._append_html_fetching_plain_text(prompt)
1703 self._prompt_html = prompt
1723 self._prompt_html = prompt
1704 else:
1724 else:
1705 self._append_plain_text(prompt)
1725 self._append_plain_text(prompt)
1706 self._prompt = prompt
1726 self._prompt = prompt
1707 self._prompt_html = None
1727 self._prompt_html = None
1708
1728
1709 self._prompt_pos = self._get_end_cursor().position()
1729 self._prompt_pos = self._get_end_cursor().position()
1710 self._prompt_started()
1730 self._prompt_started()
1711
1731
1712 #------ Signal handlers ----------------------------------------------------
1732 #------ Signal handlers ----------------------------------------------------
1713
1733
1714 def _adjust_scrollbars(self):
1734 def _adjust_scrollbars(self):
1715 """ Expands the vertical scrollbar beyond the range set by Qt.
1735 """ Expands the vertical scrollbar beyond the range set by Qt.
1716 """
1736 """
1717 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1737 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1718 # and qtextedit.cpp.
1738 # and qtextedit.cpp.
1719 document = self._control.document()
1739 document = self._control.document()
1720 scrollbar = self._control.verticalScrollBar()
1740 scrollbar = self._control.verticalScrollBar()
1721 viewport_height = self._control.viewport().height()
1741 viewport_height = self._control.viewport().height()
1722 if isinstance(self._control, QtGui.QPlainTextEdit):
1742 if isinstance(self._control, QtGui.QPlainTextEdit):
1723 maximum = max(0, document.lineCount() - 1)
1743 maximum = max(0, document.lineCount() - 1)
1724 step = viewport_height / self._control.fontMetrics().lineSpacing()
1744 step = viewport_height / self._control.fontMetrics().lineSpacing()
1725 else:
1745 else:
1726 # QTextEdit does not do line-based layout and blocks will not in
1746 # QTextEdit does not do line-based layout and blocks will not in
1727 # general have the same height. Therefore it does not make sense to
1747 # general have the same height. Therefore it does not make sense to
1728 # attempt to scroll in line height increments.
1748 # attempt to scroll in line height increments.
1729 maximum = document.size().height()
1749 maximum = document.size().height()
1730 step = viewport_height
1750 step = viewport_height
1731 diff = maximum - scrollbar.maximum()
1751 diff = maximum - scrollbar.maximum()
1732 scrollbar.setRange(0, maximum)
1752 scrollbar.setRange(0, maximum)
1733 scrollbar.setPageStep(step)
1753 scrollbar.setPageStep(step)
1734 # Compensate for undesirable scrolling that occurs automatically due to
1754 # Compensate for undesirable scrolling that occurs automatically due to
1735 # maximumBlockCount() text truncation.
1755 # maximumBlockCount() text truncation.
1736 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1756 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1737 scrollbar.setValue(scrollbar.value() + diff)
1757 scrollbar.setValue(scrollbar.value() + diff)
1738
1758
1739 def _cursor_position_changed(self):
1759 def _cursor_position_changed(self):
1740 """ Clears the temporary buffer based on the cursor position.
1760 """ Clears the temporary buffer based on the cursor position.
1741 """
1761 """
1742 if self._text_completing_pos:
1762 if self._text_completing_pos:
1743 document = self._control.document()
1763 document = self._control.document()
1744 if self._text_completing_pos < document.characterCount():
1764 if self._text_completing_pos < document.characterCount():
1745 cursor = self._control.textCursor()
1765 cursor = self._control.textCursor()
1746 pos = cursor.position()
1766 pos = cursor.position()
1747 text_cursor = self._control.textCursor()
1767 text_cursor = self._control.textCursor()
1748 text_cursor.setPosition(self._text_completing_pos)
1768 text_cursor.setPosition(self._text_completing_pos)
1749 if pos < self._text_completing_pos or \
1769 if pos < self._text_completing_pos or \
1750 cursor.blockNumber() > text_cursor.blockNumber():
1770 cursor.blockNumber() > text_cursor.blockNumber():
1751 self._clear_temporary_buffer()
1771 self._clear_temporary_buffer()
1752 self._text_completing_pos = 0
1772 self._text_completing_pos = 0
1753 else:
1773 else:
1754 self._clear_temporary_buffer()
1774 self._clear_temporary_buffer()
1755 self._text_completing_pos = 0
1775 self._text_completing_pos = 0
1756
1776
1757 def _custom_context_menu_requested(self, pos):
1777 def _custom_context_menu_requested(self, pos):
1758 """ Shows a context menu at the given QPoint (in widget coordinates).
1778 """ Shows a context menu at the given QPoint (in widget coordinates).
1759 """
1779 """
1760 menu = self._context_menu_make(pos)
1780 menu = self._context_menu_make(pos)
1761 menu.exec_(self._control.mapToGlobal(pos))
1781 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,177 +1,192 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
6 from ipython_widget import IPythonWidget
6 from ipython_widget import IPythonWidget
7
7
8
8
9 class RichIPythonWidget(IPythonWidget):
9 class RichIPythonWidget(IPythonWidget):
10 """ An IPythonWidget that supports rich text, including lists, images, and
10 """ An IPythonWidget that supports rich text, including lists, images, and
11 tables. Note that raw performance will be reduced compared to the plain
11 tables. Note that raw performance will be reduced compared to the plain
12 text version.
12 text version.
13 """
13 """
14
14
15 # RichIPythonWidget protected class variables.
15 # RichIPythonWidget protected class variables.
16 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
16 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
17 _svg_text_format_property = 1
17 _svg_text_format_property = 1
18
18
19 #---------------------------------------------------------------------------
19 #---------------------------------------------------------------------------
20 # 'object' interface
20 # 'object' interface
21 #---------------------------------------------------------------------------
21 #---------------------------------------------------------------------------
22
22
23 def __init__(self, *args, **kw):
23 def __init__(self, *args, **kw):
24 """ Create a RichIPythonWidget.
24 """ Create a RichIPythonWidget.
25 """
25 """
26 kw['kind'] = 'rich'
26 kw['kind'] = 'rich'
27 super(RichIPythonWidget, self).__init__(*args, **kw)
27 super(RichIPythonWidget, self).__init__(*args, **kw)
28 # Dictionary for resolving Qt names to images when
28 # Dictionary for resolving Qt names to images when
29 # generating XHTML output
29 # generating XHTML output
30 self._name_to_svg = {}
30 self._name_to_svg = {}
31
31
32 #---------------------------------------------------------------------------
32 #---------------------------------------------------------------------------
33 # 'ConsoleWidget' protected interface
33 # 'ConsoleWidget' protected interface
34 #---------------------------------------------------------------------------
34 #---------------------------------------------------------------------------
35
35
36 def _context_menu_make(self, pos):
36 def _context_menu_make(self, pos):
37 """ Reimplemented to return a custom context menu for images.
37 """ Reimplemented to return a custom context menu for images.
38 """
38 """
39 format = self._control.cursorForPosition(pos).charFormat()
39 format = self._control.cursorForPosition(pos).charFormat()
40 name = format.stringProperty(QtGui.QTextFormat.ImageName)
40 name = format.stringProperty(QtGui.QTextFormat.ImageName)
41 if name.isEmpty():
41 if name.isEmpty():
42 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
42 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
43 else:
43 else:
44 menu = QtGui.QMenu()
44 menu = QtGui.QMenu()
45
45
46 menu.addAction('Copy Image', lambda: self._copy_image(name))
46 menu.addAction('Copy Image', lambda: self._copy_image(name))
47 menu.addAction('Save Image As...', lambda: self._save_image(name))
47 menu.addAction('Save Image As...', lambda: self._save_image(name))
48 menu.addSeparator()
48 menu.addSeparator()
49
49
50 svg = format.stringProperty(self._svg_text_format_property)
50 svg = format.stringProperty(self._svg_text_format_property)
51 if not svg.isEmpty():
51 if not svg.isEmpty():
52 menu.addSeparator()
52 menu.addSeparator()
53 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
53 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
54 menu.addAction('Save SVG As...',
54 menu.addAction('Save SVG As...',
55 lambda: save_svg(svg, self._control))
55 lambda: save_svg(svg, self._control))
56 return menu
56 return menu
57
57
58 #---------------------------------------------------------------------------
58 #---------------------------------------------------------------------------
59 # 'FrontendWidget' protected interface
59 # 'FrontendWidget' protected interface
60 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
61
61
62 def _process_execute_payload(self, item):
62 def _process_execute_payload(self, item):
63 """ Reimplemented to handle matplotlib plot payloads.
63 """ Reimplemented to handle matplotlib plot payloads.
64 """
64 """
65 if item['source'] == self._payload_source_plot:
65 if item['source'] == self._payload_source_plot:
66 if item['format'] == 'svg':
66 if item['format'] == 'svg':
67 svg = item['data']
67 svg = item['data']
68 try:
68 try:
69 image = svg_to_image(svg)
69 image = svg_to_image(svg)
70 except ValueError:
70 except ValueError:
71 self._append_plain_text('Received invalid plot data.')
71 self._append_plain_text('Received invalid plot data.')
72 else:
72 else:
73 format = self._add_image(image)
73 format = self._add_image(image)
74 self._name_to_svg[str(format.name())] = svg
74 self._name_to_svg[str(format.name())] = svg
75 format.setProperty(self._svg_text_format_property, svg)
75 format.setProperty(self._svg_text_format_property, svg)
76 cursor = self._get_end_cursor()
76 cursor = self._get_end_cursor()
77 cursor.insertBlock()
77 cursor.insertBlock()
78 cursor.insertImage(format)
78 cursor.insertImage(format)
79 cursor.insertBlock()
79 cursor.insertBlock()
80 return True
80 return True
81 else:
81 else:
82 # Add other plot formats here!
82 # Add other plot formats here!
83 return False
83 return False
84 else:
84 else:
85 return super(RichIPythonWidget, self)._process_execute_payload(item)
85 return super(RichIPythonWidget, self)._process_execute_payload(item)
86
86
87 #---------------------------------------------------------------------------
87 #---------------------------------------------------------------------------
88 # 'RichIPythonWidget' protected interface
88 # 'RichIPythonWidget' protected interface
89 #---------------------------------------------------------------------------
89 #---------------------------------------------------------------------------
90
90
91 def _add_image(self, image):
91 def _add_image(self, image):
92 """ Adds the specified QImage to the document and returns a
92 """ Adds the specified QImage to the document and returns a
93 QTextImageFormat that references it.
93 QTextImageFormat that references it.
94 """
94 """
95 document = self._control.document()
95 document = self._control.document()
96 name = QtCore.QString.number(image.cacheKey())
96 name = QtCore.QString.number(image.cacheKey())
97 document.addResource(QtGui.QTextDocument.ImageResource,
97 document.addResource(QtGui.QTextDocument.ImageResource,
98 QtCore.QUrl(name), image)
98 QtCore.QUrl(name), image)
99 format = QtGui.QTextImageFormat()
99 format = QtGui.QTextImageFormat()
100 format.setName(name)
100 format.setName(name)
101 return format
101 return format
102
102
103 def _copy_image(self, name):
103 def _copy_image(self, name):
104 """ Copies the ImageResource with 'name' to the clipboard.
104 """ Copies the ImageResource with 'name' to the clipboard.
105 """
105 """
106 image = self._get_image(name)
106 image = self._get_image(name)
107 QtGui.QApplication.clipboard().setImage(image)
107 QtGui.QApplication.clipboard().setImage(image)
108
108
109 def _get_image(self, name):
109 def _get_image(self, name):
110 """ Returns the QImage stored as the ImageResource with 'name'.
110 """ Returns the QImage stored as the ImageResource with 'name'.
111 """
111 """
112 document = self._control.document()
112 document = self._control.document()
113 variant = document.resource(QtGui.QTextDocument.ImageResource,
113 variant = document.resource(QtGui.QTextDocument.ImageResource,
114 QtCore.QUrl(name))
114 QtCore.QUrl(name))
115 return variant.toPyObject()
115 return variant.toPyObject()
116
116
117 def _save_image(self, name, format='PNG'):
117 def _save_image(self, name, format='PNG'):
118 """ Shows a save dialog for the ImageResource with 'name'.
118 """ Shows a save dialog for the ImageResource with 'name'.
119 """
119 """
120 dialog = QtGui.QFileDialog(self._control, 'Save Image')
120 dialog = QtGui.QFileDialog(self._control, 'Save Image')
121 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
121 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
122 dialog.setDefaultSuffix(format.lower())
122 dialog.setDefaultSuffix(format.lower())
123 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
123 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
124 if dialog.exec_():
124 if dialog.exec_():
125 filename = dialog.selectedFiles()[0]
125 filename = dialog.selectedFiles()[0]
126 image = self._get_image(name)
126 image = self._get_image(name)
127 image.save(filename, format)
127 image.save(filename, format)
128
128
129 def image_tag(self, match, path = None, format = "PNG"):
129 def image_tag(self, match, path = None, format = "png"):
130 """ Given an re.match object matching an image name in an HTML dump,
130 """ Return (X)HTML mark-up for the image-tag given by match.
131 return an appropriate substitution string for the image tag
131
132 (e.g., link, embedded image, ...). As a side effect, files may
132 Parameters
133 be generated in the directory given by path."""
133 ----------
134 match : re.SRE_Match
135 A match to an HTML image tag as exported by Qt, with
136 match.group("Name") containing the matched image ID.
137
138 path : string|None, optional [default None]
139 If not None, specifies a path to which supporting files
140 may be written (e.g., for linked images).
141 If None, all images are to be included inline.
142
143 format : "png"|"svg", optional [default "png"]
144 Format for returned or referenced images.
145
146 Subclasses supporting image display should override this
147 method.
148 """
134
149
135 if(format == "png"):
150 if(format == "png"):
136 try:
151 try:
137 image = self._get_image(match.group("name"))
152 image = self._get_image(match.group("name"))
138 except KeyError:
153 except KeyError:
139 return "<b>Couldn't find image %s</b>" % match.group("name")
154 return "<b>Couldn't find image %s</b>" % match.group("name")
140
155
141 if(path is not None):
156 if(path is not None):
142 relpath = path[path.rfind("/")+1:]
157 relpath = path[path.rfind("/")+1:]
143 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
158 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
144 "PNG")):
159 "PNG")):
145 return '<img src="%s/qt_img%s.png">' % (relpath,
160 return '<img src="%s/qt_img%s.png">' % (relpath,
146 match.group("name"))
161 match.group("name"))
147 else:
162 else:
148 return "<b>Couldn't save image!</b>"
163 return "<b>Couldn't save image!</b>"
149 else:
164 else:
150 ba = QtCore.QByteArray()
165 ba = QtCore.QByteArray()
151 buffer_ = QtCore.QBuffer(ba)
166 buffer_ = QtCore.QBuffer(ba)
152 buffer_.open(QtCore.QIODevice.WriteOnly)
167 buffer_.open(QtCore.QIODevice.WriteOnly)
153 image.save(buffer_, "PNG")
168 image.save(buffer_, "PNG")
154 buffer_.close()
169 buffer_.close()
155 import re
170 import re
156 return '<img src="data:image/png;base64,\n%s\n" />' % (
171 return '<img src="data:image/png;base64,\n%s\n" />' % (
157 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
172 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
158
173
159 elif(format == "svg"):
174 elif(format == "svg"):
160 try:
175 try:
161 svg = str(self._name_to_svg[match.group("name")])
176 svg = str(self._name_to_svg[match.group("name")])
162 except KeyError:
177 except KeyError:
163 return "<b>Couldn't find image %s</b>" % match.group("name")
178 return "<b>Couldn't find image %s</b>" % match.group("name")
164
179
165 # Not currently checking path, because it's tricky to find a
180 # Not currently checking path, because it's tricky to find a
166 # cross-browser way to embed external SVG images (e.g., via
181 # cross-browser way to embed external SVG images (e.g., via
167 # object or embed tags).
182 # object or embed tags).
168
183
169 # Chop stand-alone header from matplotlib SVG
184 # Chop stand-alone header from matplotlib SVG
170 offset = svg.find("<svg")
185 offset = svg.find("<svg")
171 assert(offset > -1)
186 assert(offset > -1)
172
187
173 return svg[offset:]
188 return svg[offset:]
174
189
175 else:
190 else:
176 return '<b>Unrecognized image format</b>'
191 return '<b>Unrecognized image format</b>'
177
192
General Comments 0
You need to be logged in to leave comments. Login now