##// END OF EJS Templates
Applied lowercase_with_underscores naming convention
Mark Voorhies -
Show More
@@ -1,1761 +1,1761 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 exportHtmlInline(self, parent = None):
510 def export_html_inline(self, parent = None):
511 self.exportHtml(parent, inline = True)
511 self.export_html(parent, inline = True)
512
512
513 def exportHtml(self, parent = None, inline = False):
513 def export_html(self, parent = None, inline = False):
514 """ Export the contents of the ConsoleWidget as an HTML file.
514 """ Export the contents of the ConsoleWidget as an HTML file.
515
515
516 If inline == True, include images as inline PNGs. Otherwise,
516 If inline == True, include images as inline PNGs. Otherwise,
517 include them as links to external PNG files, mimicking the
517 include them as links to external PNG files, mimicking the
518 Firefox's "Web Page, complete" behavior.
518 Firefox's "Web Page, complete" behavior.
519 """
519 """
520 dialog = QtGui.QFileDialog(parent, 'Save HTML Document')
520 dialog = QtGui.QFileDialog(parent, 'Save HTML Document')
521 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
521 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
522 dialog.setDefaultSuffix('htm')
522 dialog.setDefaultSuffix('htm')
523 dialog.setNameFilter('HTML document (*.htm)')
523 dialog.setNameFilter('HTML document (*.htm)')
524 if dialog.exec_():
524 if dialog.exec_():
525 filename = str(dialog.selectedFiles()[0])
525 filename = str(dialog.selectedFiles()[0])
526 if(inline):
526 if(inline):
527 path = None
527 path = None
528 else:
528 else:
529 offset = filename.rfind(".")
529 offset = filename.rfind(".")
530 if(offset > 0):
530 if(offset > 0):
531 path = filename[:offset]+"_files"
531 path = filename[:offset]+"_files"
532 else:
532 else:
533 path = filename+"_files"
533 path = filename+"_files"
534 import os
534 import os
535 try:
535 try:
536 os.mkdir(path)
536 os.mkdir(path)
537 except OSError:
537 except OSError:
538 # TODO: check that this is an "already exists" error
538 # TODO: check that this is an "already exists" error
539 pass
539 pass
540
540
541 f = open(filename, 'w')
541 f = open(filename, 'w')
542 try:
542 try:
543 # N.B. this is overly restrictive, but Qt's output is
543 # N.B. this is overly restrictive, but Qt's output is
544 # predictable...
544 # predictable...
545 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
545 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
546 f.write(img_re.sub(
546 f.write(img_re.sub(
547 lambda x: self.imagetag(x, path = path, format = "PNG"),
547 lambda x: self.image_tag(x, path = path, format = "PNG"),
548 str(self._control.toHtml().toUtf8())))
548 str(self._control.toHtml().toUtf8())))
549 finally:
549 finally:
550 f.close()
550 f.close()
551 return filename
551 return filename
552 return None
552 return None
553
553
554 def exportXhtml(self, parent = None):
554 def export_xhtml(self, parent = None):
555 """ Export the contents of the ConsoleWidget as an XHTML file
555 """ Export the contents of the ConsoleWidget as an XHTML file
556 with figures as inline SVG.
556 with figures as inline SVG.
557 """
557 """
558 dialog = QtGui.QFileDialog(parent, 'Save XHTML Document')
558 dialog = QtGui.QFileDialog(parent, 'Save XHTML Document')
559 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
559 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
560 dialog.setDefaultSuffix('xml')
560 dialog.setDefaultSuffix('xml')
561 dialog.setNameFilter('XHTML document (*.xml)')
561 dialog.setNameFilter('XHTML document (*.xml)')
562 if dialog.exec_():
562 if dialog.exec_():
563 filename = str(dialog.selectedFiles()[0])
563 filename = str(dialog.selectedFiles()[0])
564 f = open(filename, 'w')
564 f = open(filename, 'w')
565 try:
565 try:
566 # N.B. this is overly restrictive, but Qt's output is
566 # N.B. this is overly restrictive, but Qt's output is
567 # predictable...
567 # predictable...
568 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
568 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
569 html = str(self._control.toHtml().toUtf8())
569 html = str(self._control.toHtml().toUtf8())
570 # Hack to make xhtml header -- note that we are not doing
570 # Hack to make xhtml header -- note that we are not doing
571 # any check for valid xml
571 # any check for valid xml
572 offset = html.find("<html>")
572 offset = html.find("<html>")
573 assert(offset > -1)
573 assert(offset > -1)
574 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
574 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
575 html[offset+6:])
575 html[offset+6:])
576 f.write(img_re.sub(
576 f.write(img_re.sub(
577 lambda x: self.imagetag(x, path = None, format = "SVG"),
577 lambda x: self.image_tag(x, path = None, format = "SVG"),
578 html))
578 html))
579 finally:
579 finally:
580 f.close()
580 f.close()
581 return filename
581 return filename
582 return None
582 return None
583
583
584 def imagetag(self, match, path = None):
584 def image_tag(self, match, path = None):
585 """ Given an re.match object matching an image name in an HTML export,
585 """ Given an re.match object matching an image name in an HTML export,
586 return an appropriate substitution string for the image tag
586 return an appropriate substitution string for the image tag
587 (e.g., link, embedded image, ...). As a side effect, files may
587 (e.g., link, embedded image, ...). As a side effect, files may
588 be generated in the directory given by path."""
588 be generated in the directory given by path."""
589
589
590 # Default case -- not enough information to generate tag
590 # Default case -- not enough information to generate tag
591 return ""
591 return ""
592
592
593 def prompt_to_top(self):
593 def prompt_to_top(self):
594 """ Moves the prompt to the top of the viewport.
594 """ Moves the prompt to the top of the viewport.
595 """
595 """
596 if not self._executing:
596 if not self._executing:
597 prompt_cursor = self._get_prompt_cursor()
597 prompt_cursor = self._get_prompt_cursor()
598 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
598 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
599 self._set_cursor(prompt_cursor)
599 self._set_cursor(prompt_cursor)
600 self._set_top_cursor(prompt_cursor)
600 self._set_top_cursor(prompt_cursor)
601
601
602 def redo(self):
602 def redo(self):
603 """ Redo the last operation. If there is no operation to redo, nothing
603 """ Redo the last operation. If there is no operation to redo, nothing
604 happens.
604 happens.
605 """
605 """
606 self._control.redo()
606 self._control.redo()
607
607
608 def reset_font(self):
608 def reset_font(self):
609 """ Sets the font to the default fixed-width font for this platform.
609 """ Sets the font to the default fixed-width font for this platform.
610 """
610 """
611 if sys.platform == 'win32':
611 if sys.platform == 'win32':
612 # Consolas ships with Vista/Win7, fallback to Courier if needed
612 # Consolas ships with Vista/Win7, fallback to Courier if needed
613 family, fallback = 'Consolas', 'Courier'
613 family, fallback = 'Consolas', 'Courier'
614 elif sys.platform == 'darwin':
614 elif sys.platform == 'darwin':
615 # OSX always has Monaco, no need for a fallback
615 # OSX always has Monaco, no need for a fallback
616 family, fallback = 'Monaco', None
616 family, fallback = 'Monaco', None
617 else:
617 else:
618 # FIXME: remove Consolas as a default on Linux once our font
618 # FIXME: remove Consolas as a default on Linux once our font
619 # selections are configurable by the user.
619 # selections are configurable by the user.
620 family, fallback = 'Consolas', 'Monospace'
620 family, fallback = 'Consolas', 'Monospace'
621 font = get_font(family, fallback)
621 font = get_font(family, fallback)
622 font.setPointSize(QtGui.qApp.font().pointSize())
622 font.setPointSize(QtGui.qApp.font().pointSize())
623 font.setStyleHint(QtGui.QFont.TypeWriter)
623 font.setStyleHint(QtGui.QFont.TypeWriter)
624 self._set_font(font)
624 self._set_font(font)
625
625
626 def change_font_size(self, delta):
626 def change_font_size(self, delta):
627 """Change the font size by the specified amount (in points).
627 """Change the font size by the specified amount (in points).
628 """
628 """
629 font = self.font
629 font = self.font
630 font.setPointSize(font.pointSize() + delta)
630 font.setPointSize(font.pointSize() + delta)
631 self._set_font(font)
631 self._set_font(font)
632
632
633 def select_all(self):
633 def select_all(self):
634 """ Selects all the text in the buffer.
634 """ Selects all the text in the buffer.
635 """
635 """
636 self._control.selectAll()
636 self._control.selectAll()
637
637
638 def _get_tab_width(self):
638 def _get_tab_width(self):
639 """ The width (in terms of space characters) for tab characters.
639 """ The width (in terms of space characters) for tab characters.
640 """
640 """
641 return self._tab_width
641 return self._tab_width
642
642
643 def _set_tab_width(self, tab_width):
643 def _set_tab_width(self, tab_width):
644 """ Sets the width (in terms of space characters) for tab characters.
644 """ Sets the width (in terms of space characters) for tab characters.
645 """
645 """
646 font_metrics = QtGui.QFontMetrics(self.font)
646 font_metrics = QtGui.QFontMetrics(self.font)
647 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
647 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
648
648
649 self._tab_width = tab_width
649 self._tab_width = tab_width
650
650
651 tab_width = property(_get_tab_width, _set_tab_width)
651 tab_width = property(_get_tab_width, _set_tab_width)
652
652
653 def undo(self):
653 def undo(self):
654 """ Undo the last operation. If there is no operation to undo, nothing
654 """ Undo the last operation. If there is no operation to undo, nothing
655 happens.
655 happens.
656 """
656 """
657 self._control.undo()
657 self._control.undo()
658
658
659 #---------------------------------------------------------------------------
659 #---------------------------------------------------------------------------
660 # 'ConsoleWidget' abstract interface
660 # 'ConsoleWidget' abstract interface
661 #---------------------------------------------------------------------------
661 #---------------------------------------------------------------------------
662
662
663 def _is_complete(self, source, interactive):
663 def _is_complete(self, source, interactive):
664 """ Returns whether 'source' can be executed. When triggered by an
664 """ Returns whether 'source' can be executed. When triggered by an
665 Enter/Return key press, 'interactive' is True; otherwise, it is
665 Enter/Return key press, 'interactive' is True; otherwise, it is
666 False.
666 False.
667 """
667 """
668 raise NotImplementedError
668 raise NotImplementedError
669
669
670 def _execute(self, source, hidden):
670 def _execute(self, source, hidden):
671 """ Execute 'source'. If 'hidden', do not show any output.
671 """ Execute 'source'. If 'hidden', do not show any output.
672 """
672 """
673 raise NotImplementedError
673 raise NotImplementedError
674
674
675 def _prompt_started_hook(self):
675 def _prompt_started_hook(self):
676 """ Called immediately after a new prompt is displayed.
676 """ Called immediately after a new prompt is displayed.
677 """
677 """
678 pass
678 pass
679
679
680 def _prompt_finished_hook(self):
680 def _prompt_finished_hook(self):
681 """ Called immediately after a prompt is finished, i.e. when some input
681 """ Called immediately after a prompt is finished, i.e. when some input
682 will be processed and a new prompt displayed.
682 will be processed and a new prompt displayed.
683 """
683 """
684 pass
684 pass
685
685
686 def _up_pressed(self):
686 def _up_pressed(self):
687 """ Called when the up key is pressed. Returns whether to continue
687 """ Called when the up key is pressed. Returns whether to continue
688 processing the event.
688 processing the event.
689 """
689 """
690 return True
690 return True
691
691
692 def _down_pressed(self):
692 def _down_pressed(self):
693 """ Called when the down key is pressed. Returns whether to continue
693 """ Called when the down key is pressed. Returns whether to continue
694 processing the event.
694 processing the event.
695 """
695 """
696 return True
696 return True
697
697
698 def _tab_pressed(self):
698 def _tab_pressed(self):
699 """ Called when the tab key is pressed. Returns whether to continue
699 """ Called when the tab key is pressed. Returns whether to continue
700 processing the event.
700 processing the event.
701 """
701 """
702 return False
702 return False
703
703
704 #--------------------------------------------------------------------------
704 #--------------------------------------------------------------------------
705 # 'ConsoleWidget' protected interface
705 # 'ConsoleWidget' protected interface
706 #--------------------------------------------------------------------------
706 #--------------------------------------------------------------------------
707
707
708 def _append_html(self, html):
708 def _append_html(self, html):
709 """ Appends html at the end of the console buffer.
709 """ Appends html at the end of the console buffer.
710 """
710 """
711 cursor = self._get_end_cursor()
711 cursor = self._get_end_cursor()
712 self._insert_html(cursor, html)
712 self._insert_html(cursor, html)
713
713
714 def _append_html_fetching_plain_text(self, html):
714 def _append_html_fetching_plain_text(self, html):
715 """ Appends 'html', then returns the plain text version of it.
715 """ Appends 'html', then returns the plain text version of it.
716 """
716 """
717 cursor = self._get_end_cursor()
717 cursor = self._get_end_cursor()
718 return self._insert_html_fetching_plain_text(cursor, html)
718 return self._insert_html_fetching_plain_text(cursor, html)
719
719
720 def _append_plain_text(self, text):
720 def _append_plain_text(self, text):
721 """ Appends plain text at the end of the console buffer, processing
721 """ Appends plain text at the end of the console buffer, processing
722 ANSI codes if enabled.
722 ANSI codes if enabled.
723 """
723 """
724 cursor = self._get_end_cursor()
724 cursor = self._get_end_cursor()
725 self._insert_plain_text(cursor, text)
725 self._insert_plain_text(cursor, text)
726
726
727 def _append_plain_text_keeping_prompt(self, text):
727 def _append_plain_text_keeping_prompt(self, text):
728 """ Writes 'text' after the current prompt, then restores the old prompt
728 """ Writes 'text' after the current prompt, then restores the old prompt
729 with its old input buffer.
729 with its old input buffer.
730 """
730 """
731 input_buffer = self.input_buffer
731 input_buffer = self.input_buffer
732 self._append_plain_text('\n')
732 self._append_plain_text('\n')
733 self._prompt_finished()
733 self._prompt_finished()
734
734
735 self._append_plain_text(text)
735 self._append_plain_text(text)
736 self._show_prompt()
736 self._show_prompt()
737 self.input_buffer = input_buffer
737 self.input_buffer = input_buffer
738
738
739 def _cancel_text_completion(self):
739 def _cancel_text_completion(self):
740 """ If text completion is progress, cancel it.
740 """ If text completion is progress, cancel it.
741 """
741 """
742 if self._text_completing_pos:
742 if self._text_completing_pos:
743 self._clear_temporary_buffer()
743 self._clear_temporary_buffer()
744 self._text_completing_pos = 0
744 self._text_completing_pos = 0
745
745
746 def _clear_temporary_buffer(self):
746 def _clear_temporary_buffer(self):
747 """ Clears the "temporary text" buffer, i.e. all the text following
747 """ Clears the "temporary text" buffer, i.e. all the text following
748 the prompt region.
748 the prompt region.
749 """
749 """
750 # Select and remove all text below the input buffer.
750 # Select and remove all text below the input buffer.
751 cursor = self._get_prompt_cursor()
751 cursor = self._get_prompt_cursor()
752 prompt = self._continuation_prompt.lstrip()
752 prompt = self._continuation_prompt.lstrip()
753 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
753 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
754 temp_cursor = QtGui.QTextCursor(cursor)
754 temp_cursor = QtGui.QTextCursor(cursor)
755 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
755 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
756 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
756 text = unicode(temp_cursor.selection().toPlainText()).lstrip()
757 if not text.startswith(prompt):
757 if not text.startswith(prompt):
758 break
758 break
759 else:
759 else:
760 # We've reached the end of the input buffer and no text follows.
760 # We've reached the end of the input buffer and no text follows.
761 return
761 return
762 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
762 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
763 cursor.movePosition(QtGui.QTextCursor.End,
763 cursor.movePosition(QtGui.QTextCursor.End,
764 QtGui.QTextCursor.KeepAnchor)
764 QtGui.QTextCursor.KeepAnchor)
765 cursor.removeSelectedText()
765 cursor.removeSelectedText()
766
766
767 # After doing this, we have no choice but to clear the undo/redo
767 # 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
768 # history. Otherwise, the text is not "temporary" at all, because it
769 # can be recalled with undo/redo. Unfortunately, Qt does not expose
769 # can be recalled with undo/redo. Unfortunately, Qt does not expose
770 # fine-grained control to the undo/redo system.
770 # fine-grained control to the undo/redo system.
771 if self._control.isUndoRedoEnabled():
771 if self._control.isUndoRedoEnabled():
772 self._control.setUndoRedoEnabled(False)
772 self._control.setUndoRedoEnabled(False)
773 self._control.setUndoRedoEnabled(True)
773 self._control.setUndoRedoEnabled(True)
774
774
775 def _complete_with_items(self, cursor, items):
775 def _complete_with_items(self, cursor, items):
776 """ Performs completion with 'items' at the specified cursor location.
776 """ Performs completion with 'items' at the specified cursor location.
777 """
777 """
778 self._cancel_text_completion()
778 self._cancel_text_completion()
779
779
780 if len(items) == 1:
780 if len(items) == 1:
781 cursor.setPosition(self._control.textCursor().position(),
781 cursor.setPosition(self._control.textCursor().position(),
782 QtGui.QTextCursor.KeepAnchor)
782 QtGui.QTextCursor.KeepAnchor)
783 cursor.insertText(items[0])
783 cursor.insertText(items[0])
784
784
785 elif len(items) > 1:
785 elif len(items) > 1:
786 current_pos = self._control.textCursor().position()
786 current_pos = self._control.textCursor().position()
787 prefix = commonprefix(items)
787 prefix = commonprefix(items)
788 if prefix:
788 if prefix:
789 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
789 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
790 cursor.insertText(prefix)
790 cursor.insertText(prefix)
791 current_pos = cursor.position()
791 current_pos = cursor.position()
792
792
793 if self.gui_completion:
793 if self.gui_completion:
794 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
794 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
795 self._completion_widget.show_items(cursor, items)
795 self._completion_widget.show_items(cursor, items)
796 else:
796 else:
797 cursor.beginEditBlock()
797 cursor.beginEditBlock()
798 self._append_plain_text('\n')
798 self._append_plain_text('\n')
799 self._page(self._format_as_columns(items))
799 self._page(self._format_as_columns(items))
800 cursor.endEditBlock()
800 cursor.endEditBlock()
801
801
802 cursor.setPosition(current_pos)
802 cursor.setPosition(current_pos)
803 self._control.moveCursor(QtGui.QTextCursor.End)
803 self._control.moveCursor(QtGui.QTextCursor.End)
804 self._control.setTextCursor(cursor)
804 self._control.setTextCursor(cursor)
805 self._text_completing_pos = current_pos
805 self._text_completing_pos = current_pos
806
806
807 def _context_menu_make(self, pos):
807 def _context_menu_make(self, pos):
808 """ Creates a context menu for the given QPoint (in widget coordinates).
808 """ Creates a context menu for the given QPoint (in widget coordinates).
809 """
809 """
810 menu = QtGui.QMenu()
810 menu = QtGui.QMenu()
811
811
812 cut_action = menu.addAction('Cut', self.cut)
812 cut_action = menu.addAction('Cut', self.cut)
813 cut_action.setEnabled(self.can_cut())
813 cut_action.setEnabled(self.can_cut())
814 cut_action.setShortcut(QtGui.QKeySequence.Cut)
814 cut_action.setShortcut(QtGui.QKeySequence.Cut)
815
815
816 copy_action = menu.addAction('Copy', self.copy)
816 copy_action = menu.addAction('Copy', self.copy)
817 copy_action.setEnabled(self.can_copy())
817 copy_action.setEnabled(self.can_copy())
818 copy_action.setShortcut(QtGui.QKeySequence.Copy)
818 copy_action.setShortcut(QtGui.QKeySequence.Copy)
819
819
820 paste_action = menu.addAction('Paste', self.paste)
820 paste_action = menu.addAction('Paste', self.paste)
821 paste_action.setEnabled(self.can_paste())
821 paste_action.setEnabled(self.can_paste())
822 paste_action.setShortcut(QtGui.QKeySequence.Paste)
822 paste_action.setShortcut(QtGui.QKeySequence.Paste)
823
823
824 menu.addSeparator()
824 menu.addSeparator()
825 menu.addAction('Select All', self.select_all)
825 menu.addAction('Select All', self.select_all)
826
826
827 menu.addSeparator()
827 menu.addSeparator()
828 print_action = menu.addAction('Print', self.print_)
828 print_action = menu.addAction('Print', self.print_)
829 print_action.setEnabled(True)
829 print_action.setEnabled(True)
830 html_action = menu.addAction('Export HTML (external PNGs)',
830 html_action = menu.addAction('Export HTML (external PNGs)',
831 self.exportHtml)
831 self.export_html)
832 html_action.setEnabled(True)
832 html_action.setEnabled(True)
833 html_inline_action = menu.addAction('Export HTML (inline PNGs)',
833 html_inline_action = menu.addAction('Export HTML (inline PNGs)',
834 self.exportHtmlInline)
834 self.export_html_inline)
835 html_inline_action.setEnabled(True)
835 html_inline_action.setEnabled(True)
836 xhtml_action = menu.addAction('Export XHTML (inline SVGs)',
836 xhtml_action = menu.addAction('Export XHTML (inline SVGs)',
837 self.exportXhtml)
837 self.export_xhtml)
838 xhtml_action.setEnabled(True)
838 xhtml_action.setEnabled(True)
839 return menu
839 return menu
840
840
841 def _control_key_down(self, modifiers, include_command=True):
841 def _control_key_down(self, modifiers, include_command=True):
842 """ Given a KeyboardModifiers flags object, return whether the Control
842 """ Given a KeyboardModifiers flags object, return whether the Control
843 key is down.
843 key is down.
844
844
845 Parameters:
845 Parameters:
846 -----------
846 -----------
847 include_command : bool, optional (default True)
847 include_command : bool, optional (default True)
848 Whether to treat the Command key as a (mutually exclusive) synonym
848 Whether to treat the Command key as a (mutually exclusive) synonym
849 for Control when in Mac OS.
849 for Control when in Mac OS.
850 """
850 """
851 # Note that on Mac OS, ControlModifier corresponds to the Command key
851 # Note that on Mac OS, ControlModifier corresponds to the Command key
852 # while MetaModifier corresponds to the Control key.
852 # while MetaModifier corresponds to the Control key.
853 if sys.platform == 'darwin':
853 if sys.platform == 'darwin':
854 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
854 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
855 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
855 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
856 else:
856 else:
857 return bool(modifiers & QtCore.Qt.ControlModifier)
857 return bool(modifiers & QtCore.Qt.ControlModifier)
858
858
859 def _create_control(self):
859 def _create_control(self):
860 """ Creates and connects the underlying text widget.
860 """ Creates and connects the underlying text widget.
861 """
861 """
862 # Create the underlying control.
862 # Create the underlying control.
863 if self.kind == 'plain':
863 if self.kind == 'plain':
864 control = QtGui.QPlainTextEdit()
864 control = QtGui.QPlainTextEdit()
865 elif self.kind == 'rich':
865 elif self.kind == 'rich':
866 control = QtGui.QTextEdit()
866 control = QtGui.QTextEdit()
867 control.setAcceptRichText(False)
867 control.setAcceptRichText(False)
868
868
869 # Install event filters. The filter on the viewport is needed for
869 # Install event filters. The filter on the viewport is needed for
870 # mouse events and drag events.
870 # mouse events and drag events.
871 control.installEventFilter(self)
871 control.installEventFilter(self)
872 control.viewport().installEventFilter(self)
872 control.viewport().installEventFilter(self)
873
873
874 # Connect signals.
874 # Connect signals.
875 control.cursorPositionChanged.connect(self._cursor_position_changed)
875 control.cursorPositionChanged.connect(self._cursor_position_changed)
876 control.customContextMenuRequested.connect(
876 control.customContextMenuRequested.connect(
877 self._custom_context_menu_requested)
877 self._custom_context_menu_requested)
878 control.copyAvailable.connect(self.copy_available)
878 control.copyAvailable.connect(self.copy_available)
879 control.redoAvailable.connect(self.redo_available)
879 control.redoAvailable.connect(self.redo_available)
880 control.undoAvailable.connect(self.undo_available)
880 control.undoAvailable.connect(self.undo_available)
881
881
882 # Hijack the document size change signal to prevent Qt from adjusting
882 # Hijack the document size change signal to prevent Qt from adjusting
883 # the viewport's scrollbar. We are relying on an implementation detail
883 # the viewport's scrollbar. We are relying on an implementation detail
884 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
884 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
885 # this functionality we cannot create a nice terminal interface.
885 # this functionality we cannot create a nice terminal interface.
886 layout = control.document().documentLayout()
886 layout = control.document().documentLayout()
887 layout.documentSizeChanged.disconnect()
887 layout.documentSizeChanged.disconnect()
888 layout.documentSizeChanged.connect(self._adjust_scrollbars)
888 layout.documentSizeChanged.connect(self._adjust_scrollbars)
889
889
890 # Configure the control.
890 # Configure the control.
891 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
891 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
892 control.setReadOnly(True)
892 control.setReadOnly(True)
893 control.setUndoRedoEnabled(False)
893 control.setUndoRedoEnabled(False)
894 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
894 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
895 return control
895 return control
896
896
897 def _create_page_control(self):
897 def _create_page_control(self):
898 """ Creates and connects the underlying paging widget.
898 """ Creates and connects the underlying paging widget.
899 """
899 """
900 if self.kind == 'plain':
900 if self.kind == 'plain':
901 control = QtGui.QPlainTextEdit()
901 control = QtGui.QPlainTextEdit()
902 elif self.kind == 'rich':
902 elif self.kind == 'rich':
903 control = QtGui.QTextEdit()
903 control = QtGui.QTextEdit()
904 control.installEventFilter(self)
904 control.installEventFilter(self)
905 control.setReadOnly(True)
905 control.setReadOnly(True)
906 control.setUndoRedoEnabled(False)
906 control.setUndoRedoEnabled(False)
907 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
907 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
908 return control
908 return control
909
909
910 def _event_filter_console_keypress(self, event):
910 def _event_filter_console_keypress(self, event):
911 """ Filter key events for the underlying text widget to create a
911 """ Filter key events for the underlying text widget to create a
912 console-like interface.
912 console-like interface.
913 """
913 """
914 intercepted = False
914 intercepted = False
915 cursor = self._control.textCursor()
915 cursor = self._control.textCursor()
916 position = cursor.position()
916 position = cursor.position()
917 key = event.key()
917 key = event.key()
918 ctrl_down = self._control_key_down(event.modifiers())
918 ctrl_down = self._control_key_down(event.modifiers())
919 alt_down = event.modifiers() & QtCore.Qt.AltModifier
919 alt_down = event.modifiers() & QtCore.Qt.AltModifier
920 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
920 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
921
921
922 #------ Special sequences ----------------------------------------------
922 #------ Special sequences ----------------------------------------------
923
923
924 if event.matches(QtGui.QKeySequence.Copy):
924 if event.matches(QtGui.QKeySequence.Copy):
925 self.copy()
925 self.copy()
926 intercepted = True
926 intercepted = True
927
927
928 elif event.matches(QtGui.QKeySequence.Cut):
928 elif event.matches(QtGui.QKeySequence.Cut):
929 self.cut()
929 self.cut()
930 intercepted = True
930 intercepted = True
931
931
932 elif event.matches(QtGui.QKeySequence.Paste):
932 elif event.matches(QtGui.QKeySequence.Paste):
933 self.paste()
933 self.paste()
934 intercepted = True
934 intercepted = True
935
935
936 #------ Special modifier logic -----------------------------------------
936 #------ Special modifier logic -----------------------------------------
937
937
938 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
938 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
939 intercepted = True
939 intercepted = True
940
940
941 # Special handling when tab completing in text mode.
941 # Special handling when tab completing in text mode.
942 self._cancel_text_completion()
942 self._cancel_text_completion()
943
943
944 if self._in_buffer(position):
944 if self._in_buffer(position):
945 if self._reading:
945 if self._reading:
946 self._append_plain_text('\n')
946 self._append_plain_text('\n')
947 self._reading = False
947 self._reading = False
948 if self._reading_callback:
948 if self._reading_callback:
949 self._reading_callback()
949 self._reading_callback()
950
950
951 # If the input buffer is a single line or there is only
951 # If the input buffer is a single line or there is only
952 # whitespace after the cursor, execute. Otherwise, split the
952 # whitespace after the cursor, execute. Otherwise, split the
953 # line with a continuation prompt.
953 # line with a continuation prompt.
954 elif not self._executing:
954 elif not self._executing:
955 cursor.movePosition(QtGui.QTextCursor.End,
955 cursor.movePosition(QtGui.QTextCursor.End,
956 QtGui.QTextCursor.KeepAnchor)
956 QtGui.QTextCursor.KeepAnchor)
957 at_end = cursor.selectedText().trimmed().isEmpty()
957 at_end = cursor.selectedText().trimmed().isEmpty()
958 single_line = (self._get_end_cursor().blockNumber() ==
958 single_line = (self._get_end_cursor().blockNumber() ==
959 self._get_prompt_cursor().blockNumber())
959 self._get_prompt_cursor().blockNumber())
960 if (at_end or shift_down or single_line) and not ctrl_down:
960 if (at_end or shift_down or single_line) and not ctrl_down:
961 self.execute(interactive = not shift_down)
961 self.execute(interactive = not shift_down)
962 else:
962 else:
963 # Do this inside an edit block for clean undo/redo.
963 # Do this inside an edit block for clean undo/redo.
964 cursor.beginEditBlock()
964 cursor.beginEditBlock()
965 cursor.setPosition(position)
965 cursor.setPosition(position)
966 cursor.insertText('\n')
966 cursor.insertText('\n')
967 self._insert_continuation_prompt(cursor)
967 self._insert_continuation_prompt(cursor)
968 cursor.endEditBlock()
968 cursor.endEditBlock()
969
969
970 # Ensure that the whole input buffer is visible.
970 # Ensure that the whole input buffer is visible.
971 # FIXME: This will not be usable if the input buffer is
971 # FIXME: This will not be usable if the input buffer is
972 # taller than the console widget.
972 # taller than the console widget.
973 self._control.moveCursor(QtGui.QTextCursor.End)
973 self._control.moveCursor(QtGui.QTextCursor.End)
974 self._control.setTextCursor(cursor)
974 self._control.setTextCursor(cursor)
975
975
976 #------ Control/Cmd modifier -------------------------------------------
976 #------ Control/Cmd modifier -------------------------------------------
977
977
978 elif ctrl_down:
978 elif ctrl_down:
979 if key == QtCore.Qt.Key_G:
979 if key == QtCore.Qt.Key_G:
980 self._keyboard_quit()
980 self._keyboard_quit()
981 intercepted = True
981 intercepted = True
982
982
983 elif key == QtCore.Qt.Key_K:
983 elif key == QtCore.Qt.Key_K:
984 if self._in_buffer(position):
984 if self._in_buffer(position):
985 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
985 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
986 QtGui.QTextCursor.KeepAnchor)
986 QtGui.QTextCursor.KeepAnchor)
987 if not cursor.hasSelection():
987 if not cursor.hasSelection():
988 # Line deletion (remove continuation prompt)
988 # Line deletion (remove continuation prompt)
989 cursor.movePosition(QtGui.QTextCursor.NextBlock,
989 cursor.movePosition(QtGui.QTextCursor.NextBlock,
990 QtGui.QTextCursor.KeepAnchor)
990 QtGui.QTextCursor.KeepAnchor)
991 cursor.movePosition(QtGui.QTextCursor.Right,
991 cursor.movePosition(QtGui.QTextCursor.Right,
992 QtGui.QTextCursor.KeepAnchor,
992 QtGui.QTextCursor.KeepAnchor,
993 len(self._continuation_prompt))
993 len(self._continuation_prompt))
994 cursor.removeSelectedText()
994 cursor.removeSelectedText()
995 intercepted = True
995 intercepted = True
996
996
997 elif key == QtCore.Qt.Key_L:
997 elif key == QtCore.Qt.Key_L:
998 self.prompt_to_top()
998 self.prompt_to_top()
999 intercepted = True
999 intercepted = True
1000
1000
1001 elif key == QtCore.Qt.Key_O:
1001 elif key == QtCore.Qt.Key_O:
1002 if self._page_control and self._page_control.isVisible():
1002 if self._page_control and self._page_control.isVisible():
1003 self._page_control.setFocus()
1003 self._page_control.setFocus()
1004 intercepted = True
1004 intercepted = True
1005
1005
1006 elif key == QtCore.Qt.Key_Y:
1006 elif key == QtCore.Qt.Key_Y:
1007 self.paste()
1007 self.paste()
1008 intercepted = True
1008 intercepted = True
1009
1009
1010 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1010 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1011 intercepted = True
1011 intercepted = True
1012
1012
1013 elif key == QtCore.Qt.Key_Plus:
1013 elif key == QtCore.Qt.Key_Plus:
1014 self.change_font_size(1)
1014 self.change_font_size(1)
1015 intercepted = True
1015 intercepted = True
1016
1016
1017 elif key == QtCore.Qt.Key_Minus:
1017 elif key == QtCore.Qt.Key_Minus:
1018 self.change_font_size(-1)
1018 self.change_font_size(-1)
1019 intercepted = True
1019 intercepted = True
1020
1020
1021 #------ Alt modifier ---------------------------------------------------
1021 #------ Alt modifier ---------------------------------------------------
1022
1022
1023 elif alt_down:
1023 elif alt_down:
1024 if key == QtCore.Qt.Key_B:
1024 if key == QtCore.Qt.Key_B:
1025 self._set_cursor(self._get_word_start_cursor(position))
1025 self._set_cursor(self._get_word_start_cursor(position))
1026 intercepted = True
1026 intercepted = True
1027
1027
1028 elif key == QtCore.Qt.Key_F:
1028 elif key == QtCore.Qt.Key_F:
1029 self._set_cursor(self._get_word_end_cursor(position))
1029 self._set_cursor(self._get_word_end_cursor(position))
1030 intercepted = True
1030 intercepted = True
1031
1031
1032 elif key == QtCore.Qt.Key_Backspace:
1032 elif key == QtCore.Qt.Key_Backspace:
1033 cursor = self._get_word_start_cursor(position)
1033 cursor = self._get_word_start_cursor(position)
1034 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1034 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1035 cursor.removeSelectedText()
1035 cursor.removeSelectedText()
1036 intercepted = True
1036 intercepted = True
1037
1037
1038 elif key == QtCore.Qt.Key_D:
1038 elif key == QtCore.Qt.Key_D:
1039 cursor = self._get_word_end_cursor(position)
1039 cursor = self._get_word_end_cursor(position)
1040 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1040 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1041 cursor.removeSelectedText()
1041 cursor.removeSelectedText()
1042 intercepted = True
1042 intercepted = True
1043
1043
1044 elif key == QtCore.Qt.Key_Delete:
1044 elif key == QtCore.Qt.Key_Delete:
1045 intercepted = True
1045 intercepted = True
1046
1046
1047 elif key == QtCore.Qt.Key_Greater:
1047 elif key == QtCore.Qt.Key_Greater:
1048 self._control.moveCursor(QtGui.QTextCursor.End)
1048 self._control.moveCursor(QtGui.QTextCursor.End)
1049 intercepted = True
1049 intercepted = True
1050
1050
1051 elif key == QtCore.Qt.Key_Less:
1051 elif key == QtCore.Qt.Key_Less:
1052 self._control.setTextCursor(self._get_prompt_cursor())
1052 self._control.setTextCursor(self._get_prompt_cursor())
1053 intercepted = True
1053 intercepted = True
1054
1054
1055 #------ No modifiers ---------------------------------------------------
1055 #------ No modifiers ---------------------------------------------------
1056
1056
1057 else:
1057 else:
1058 if shift_down:
1058 if shift_down:
1059 anchormode=QtGui.QTextCursor.KeepAnchor
1059 anchormode=QtGui.QTextCursor.KeepAnchor
1060 else:
1060 else:
1061 anchormode=QtGui.QTextCursor.MoveAnchor
1061 anchormode=QtGui.QTextCursor.MoveAnchor
1062
1062
1063 if key == QtCore.Qt.Key_Escape:
1063 if key == QtCore.Qt.Key_Escape:
1064 self._keyboard_quit()
1064 self._keyboard_quit()
1065 intercepted = True
1065 intercepted = True
1066
1066
1067 elif key == QtCore.Qt.Key_Up:
1067 elif key == QtCore.Qt.Key_Up:
1068 if self._reading or not self._up_pressed():
1068 if self._reading or not self._up_pressed():
1069 intercepted = True
1069 intercepted = True
1070 else:
1070 else:
1071 prompt_line = self._get_prompt_cursor().blockNumber()
1071 prompt_line = self._get_prompt_cursor().blockNumber()
1072 intercepted = cursor.blockNumber() <= prompt_line
1072 intercepted = cursor.blockNumber() <= prompt_line
1073
1073
1074 elif key == QtCore.Qt.Key_Down:
1074 elif key == QtCore.Qt.Key_Down:
1075 if self._reading or not self._down_pressed():
1075 if self._reading or not self._down_pressed():
1076 intercepted = True
1076 intercepted = True
1077 else:
1077 else:
1078 end_line = self._get_end_cursor().blockNumber()
1078 end_line = self._get_end_cursor().blockNumber()
1079 intercepted = cursor.blockNumber() == end_line
1079 intercepted = cursor.blockNumber() == end_line
1080
1080
1081 elif key == QtCore.Qt.Key_Tab:
1081 elif key == QtCore.Qt.Key_Tab:
1082 if not self._reading:
1082 if not self._reading:
1083 intercepted = not self._tab_pressed()
1083 intercepted = not self._tab_pressed()
1084
1084
1085 elif key == QtCore.Qt.Key_Left:
1085 elif key == QtCore.Qt.Key_Left:
1086
1086
1087 # Move to the previous line
1087 # Move to the previous line
1088 line, col = cursor.blockNumber(), cursor.columnNumber()
1088 line, col = cursor.blockNumber(), cursor.columnNumber()
1089 if line > self._get_prompt_cursor().blockNumber() and \
1089 if line > self._get_prompt_cursor().blockNumber() and \
1090 col == len(self._continuation_prompt):
1090 col == len(self._continuation_prompt):
1091 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1091 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1092 mode=anchormode)
1092 mode=anchormode)
1093 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1093 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1094 mode=anchormode)
1094 mode=anchormode)
1095 intercepted = True
1095 intercepted = True
1096
1096
1097 # Regular left movement
1097 # Regular left movement
1098 else:
1098 else:
1099 intercepted = not self._in_buffer(position - 1)
1099 intercepted = not self._in_buffer(position - 1)
1100
1100
1101 elif key == QtCore.Qt.Key_Right:
1101 elif key == QtCore.Qt.Key_Right:
1102 original_block_number = cursor.blockNumber()
1102 original_block_number = cursor.blockNumber()
1103 cursor.movePosition(QtGui.QTextCursor.Right,
1103 cursor.movePosition(QtGui.QTextCursor.Right,
1104 mode=anchormode)
1104 mode=anchormode)
1105 if cursor.blockNumber() != original_block_number:
1105 if cursor.blockNumber() != original_block_number:
1106 cursor.movePosition(QtGui.QTextCursor.Right,
1106 cursor.movePosition(QtGui.QTextCursor.Right,
1107 n=len(self._continuation_prompt),
1107 n=len(self._continuation_prompt),
1108 mode=anchormode)
1108 mode=anchormode)
1109 self._set_cursor(cursor)
1109 self._set_cursor(cursor)
1110 intercepted = True
1110 intercepted = True
1111
1111
1112 elif key == QtCore.Qt.Key_Home:
1112 elif key == QtCore.Qt.Key_Home:
1113 start_line = cursor.blockNumber()
1113 start_line = cursor.blockNumber()
1114 if start_line == self._get_prompt_cursor().blockNumber():
1114 if start_line == self._get_prompt_cursor().blockNumber():
1115 start_pos = self._prompt_pos
1115 start_pos = self._prompt_pos
1116 else:
1116 else:
1117 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1117 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1118 QtGui.QTextCursor.KeepAnchor)
1118 QtGui.QTextCursor.KeepAnchor)
1119 start_pos = cursor.position()
1119 start_pos = cursor.position()
1120 start_pos += len(self._continuation_prompt)
1120 start_pos += len(self._continuation_prompt)
1121 cursor.setPosition(position)
1121 cursor.setPosition(position)
1122 if shift_down and self._in_buffer(position):
1122 if shift_down and self._in_buffer(position):
1123 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1123 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1124 else:
1124 else:
1125 cursor.setPosition(start_pos)
1125 cursor.setPosition(start_pos)
1126 self._set_cursor(cursor)
1126 self._set_cursor(cursor)
1127 intercepted = True
1127 intercepted = True
1128
1128
1129 elif key == QtCore.Qt.Key_Backspace:
1129 elif key == QtCore.Qt.Key_Backspace:
1130
1130
1131 # Line deletion (remove continuation prompt)
1131 # Line deletion (remove continuation prompt)
1132 line, col = cursor.blockNumber(), cursor.columnNumber()
1132 line, col = cursor.blockNumber(), cursor.columnNumber()
1133 if not self._reading and \
1133 if not self._reading and \
1134 col == len(self._continuation_prompt) and \
1134 col == len(self._continuation_prompt) and \
1135 line > self._get_prompt_cursor().blockNumber():
1135 line > self._get_prompt_cursor().blockNumber():
1136 cursor.beginEditBlock()
1136 cursor.beginEditBlock()
1137 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1137 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1138 QtGui.QTextCursor.KeepAnchor)
1138 QtGui.QTextCursor.KeepAnchor)
1139 cursor.removeSelectedText()
1139 cursor.removeSelectedText()
1140 cursor.deletePreviousChar()
1140 cursor.deletePreviousChar()
1141 cursor.endEditBlock()
1141 cursor.endEditBlock()
1142 intercepted = True
1142 intercepted = True
1143
1143
1144 # Regular backwards deletion
1144 # Regular backwards deletion
1145 else:
1145 else:
1146 anchor = cursor.anchor()
1146 anchor = cursor.anchor()
1147 if anchor == position:
1147 if anchor == position:
1148 intercepted = not self._in_buffer(position - 1)
1148 intercepted = not self._in_buffer(position - 1)
1149 else:
1149 else:
1150 intercepted = not self._in_buffer(min(anchor, position))
1150 intercepted = not self._in_buffer(min(anchor, position))
1151
1151
1152 elif key == QtCore.Qt.Key_Delete:
1152 elif key == QtCore.Qt.Key_Delete:
1153
1153
1154 # Line deletion (remove continuation prompt)
1154 # Line deletion (remove continuation prompt)
1155 if not self._reading and self._in_buffer(position) and \
1155 if not self._reading and self._in_buffer(position) and \
1156 cursor.atBlockEnd() and not cursor.hasSelection():
1156 cursor.atBlockEnd() and not cursor.hasSelection():
1157 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1157 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1158 QtGui.QTextCursor.KeepAnchor)
1158 QtGui.QTextCursor.KeepAnchor)
1159 cursor.movePosition(QtGui.QTextCursor.Right,
1159 cursor.movePosition(QtGui.QTextCursor.Right,
1160 QtGui.QTextCursor.KeepAnchor,
1160 QtGui.QTextCursor.KeepAnchor,
1161 len(self._continuation_prompt))
1161 len(self._continuation_prompt))
1162 cursor.removeSelectedText()
1162 cursor.removeSelectedText()
1163 intercepted = True
1163 intercepted = True
1164
1164
1165 # Regular forwards deletion:
1165 # Regular forwards deletion:
1166 else:
1166 else:
1167 anchor = cursor.anchor()
1167 anchor = cursor.anchor()
1168 intercepted = (not self._in_buffer(anchor) or
1168 intercepted = (not self._in_buffer(anchor) or
1169 not self._in_buffer(position))
1169 not self._in_buffer(position))
1170
1170
1171 # Don't move the cursor if control is down to allow copy-paste using
1171 # Don't move the cursor if control is down to allow copy-paste using
1172 # the keyboard in any part of the buffer.
1172 # the keyboard in any part of the buffer.
1173 if not ctrl_down:
1173 if not ctrl_down:
1174 self._keep_cursor_in_buffer()
1174 self._keep_cursor_in_buffer()
1175
1175
1176 return intercepted
1176 return intercepted
1177
1177
1178 def _event_filter_page_keypress(self, event):
1178 def _event_filter_page_keypress(self, event):
1179 """ Filter key events for the paging widget to create console-like
1179 """ Filter key events for the paging widget to create console-like
1180 interface.
1180 interface.
1181 """
1181 """
1182 key = event.key()
1182 key = event.key()
1183 ctrl_down = self._control_key_down(event.modifiers())
1183 ctrl_down = self._control_key_down(event.modifiers())
1184 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1184 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1185
1185
1186 if ctrl_down:
1186 if ctrl_down:
1187 if key == QtCore.Qt.Key_O:
1187 if key == QtCore.Qt.Key_O:
1188 self._control.setFocus()
1188 self._control.setFocus()
1189 intercept = True
1189 intercept = True
1190
1190
1191 elif alt_down:
1191 elif alt_down:
1192 if key == QtCore.Qt.Key_Greater:
1192 if key == QtCore.Qt.Key_Greater:
1193 self._page_control.moveCursor(QtGui.QTextCursor.End)
1193 self._page_control.moveCursor(QtGui.QTextCursor.End)
1194 intercepted = True
1194 intercepted = True
1195
1195
1196 elif key == QtCore.Qt.Key_Less:
1196 elif key == QtCore.Qt.Key_Less:
1197 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1197 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1198 intercepted = True
1198 intercepted = True
1199
1199
1200 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1200 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1201 if self._splitter:
1201 if self._splitter:
1202 self._page_control.hide()
1202 self._page_control.hide()
1203 else:
1203 else:
1204 self.layout().setCurrentWidget(self._control)
1204 self.layout().setCurrentWidget(self._control)
1205 return True
1205 return True
1206
1206
1207 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1207 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1208 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1208 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1209 QtCore.Qt.Key_PageDown,
1209 QtCore.Qt.Key_PageDown,
1210 QtCore.Qt.NoModifier)
1210 QtCore.Qt.NoModifier)
1211 QtGui.qApp.sendEvent(self._page_control, new_event)
1211 QtGui.qApp.sendEvent(self._page_control, new_event)
1212 return True
1212 return True
1213
1213
1214 elif key == QtCore.Qt.Key_Backspace:
1214 elif key == QtCore.Qt.Key_Backspace:
1215 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1215 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1216 QtCore.Qt.Key_PageUp,
1216 QtCore.Qt.Key_PageUp,
1217 QtCore.Qt.NoModifier)
1217 QtCore.Qt.NoModifier)
1218 QtGui.qApp.sendEvent(self._page_control, new_event)
1218 QtGui.qApp.sendEvent(self._page_control, new_event)
1219 return True
1219 return True
1220
1220
1221 return False
1221 return False
1222
1222
1223 def _format_as_columns(self, items, separator=' '):
1223 def _format_as_columns(self, items, separator=' '):
1224 """ Transform a list of strings into a single string with columns.
1224 """ Transform a list of strings into a single string with columns.
1225
1225
1226 Parameters
1226 Parameters
1227 ----------
1227 ----------
1228 items : sequence of strings
1228 items : sequence of strings
1229 The strings to process.
1229 The strings to process.
1230
1230
1231 separator : str, optional [default is two spaces]
1231 separator : str, optional [default is two spaces]
1232 The string that separates columns.
1232 The string that separates columns.
1233
1233
1234 Returns
1234 Returns
1235 -------
1235 -------
1236 The formatted string.
1236 The formatted string.
1237 """
1237 """
1238 # Note: this code is adapted from columnize 0.3.2.
1238 # Note: this code is adapted from columnize 0.3.2.
1239 # See http://code.google.com/p/pycolumnize/
1239 # See http://code.google.com/p/pycolumnize/
1240
1240
1241 # Calculate the number of characters available.
1241 # Calculate the number of characters available.
1242 width = self._control.viewport().width()
1242 width = self._control.viewport().width()
1243 char_width = QtGui.QFontMetrics(self.font).width(' ')
1243 char_width = QtGui.QFontMetrics(self.font).width(' ')
1244 displaywidth = max(10, (width / char_width) - 1)
1244 displaywidth = max(10, (width / char_width) - 1)
1245
1245
1246 # Some degenerate cases.
1246 # Some degenerate cases.
1247 size = len(items)
1247 size = len(items)
1248 if size == 0:
1248 if size == 0:
1249 return '\n'
1249 return '\n'
1250 elif size == 1:
1250 elif size == 1:
1251 return '%s\n' % items[0]
1251 return '%s\n' % items[0]
1252
1252
1253 # Try every row count from 1 upwards
1253 # Try every row count from 1 upwards
1254 array_index = lambda nrows, row, col: nrows*col + row
1254 array_index = lambda nrows, row, col: nrows*col + row
1255 for nrows in range(1, size):
1255 for nrows in range(1, size):
1256 ncols = (size + nrows - 1) // nrows
1256 ncols = (size + nrows - 1) // nrows
1257 colwidths = []
1257 colwidths = []
1258 totwidth = -len(separator)
1258 totwidth = -len(separator)
1259 for col in range(ncols):
1259 for col in range(ncols):
1260 # Get max column width for this column
1260 # Get max column width for this column
1261 colwidth = 0
1261 colwidth = 0
1262 for row in range(nrows):
1262 for row in range(nrows):
1263 i = array_index(nrows, row, col)
1263 i = array_index(nrows, row, col)
1264 if i >= size: break
1264 if i >= size: break
1265 x = items[i]
1265 x = items[i]
1266 colwidth = max(colwidth, len(x))
1266 colwidth = max(colwidth, len(x))
1267 colwidths.append(colwidth)
1267 colwidths.append(colwidth)
1268 totwidth += colwidth + len(separator)
1268 totwidth += colwidth + len(separator)
1269 if totwidth > displaywidth:
1269 if totwidth > displaywidth:
1270 break
1270 break
1271 if totwidth <= displaywidth:
1271 if totwidth <= displaywidth:
1272 break
1272 break
1273
1273
1274 # The smallest number of rows computed and the max widths for each
1274 # 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.
1275 # column has been obtained. Now we just have to format each of the rows.
1276 string = ''
1276 string = ''
1277 for row in range(nrows):
1277 for row in range(nrows):
1278 texts = []
1278 texts = []
1279 for col in range(ncols):
1279 for col in range(ncols):
1280 i = row + nrows*col
1280 i = row + nrows*col
1281 if i >= size:
1281 if i >= size:
1282 texts.append('')
1282 texts.append('')
1283 else:
1283 else:
1284 texts.append(items[i])
1284 texts.append(items[i])
1285 while texts and not texts[-1]:
1285 while texts and not texts[-1]:
1286 del texts[-1]
1286 del texts[-1]
1287 for col in range(len(texts)):
1287 for col in range(len(texts)):
1288 texts[col] = texts[col].ljust(colwidths[col])
1288 texts[col] = texts[col].ljust(colwidths[col])
1289 string += '%s\n' % separator.join(texts)
1289 string += '%s\n' % separator.join(texts)
1290 return string
1290 return string
1291
1291
1292 def _get_block_plain_text(self, block):
1292 def _get_block_plain_text(self, block):
1293 """ Given a QTextBlock, return its unformatted text.
1293 """ Given a QTextBlock, return its unformatted text.
1294 """
1294 """
1295 cursor = QtGui.QTextCursor(block)
1295 cursor = QtGui.QTextCursor(block)
1296 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1296 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1297 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1297 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1298 QtGui.QTextCursor.KeepAnchor)
1298 QtGui.QTextCursor.KeepAnchor)
1299 return unicode(cursor.selection().toPlainText())
1299 return unicode(cursor.selection().toPlainText())
1300
1300
1301 def _get_cursor(self):
1301 def _get_cursor(self):
1302 """ Convenience method that returns a cursor for the current position.
1302 """ Convenience method that returns a cursor for the current position.
1303 """
1303 """
1304 return self._control.textCursor()
1304 return self._control.textCursor()
1305
1305
1306 def _get_end_cursor(self):
1306 def _get_end_cursor(self):
1307 """ Convenience method that returns a cursor for the last character.
1307 """ Convenience method that returns a cursor for the last character.
1308 """
1308 """
1309 cursor = self._control.textCursor()
1309 cursor = self._control.textCursor()
1310 cursor.movePosition(QtGui.QTextCursor.End)
1310 cursor.movePosition(QtGui.QTextCursor.End)
1311 return cursor
1311 return cursor
1312
1312
1313 def _get_input_buffer_cursor_column(self):
1313 def _get_input_buffer_cursor_column(self):
1314 """ Returns the column of the cursor in the input buffer, excluding the
1314 """ 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.
1315 contribution by the prompt, or -1 if there is no such column.
1316 """
1316 """
1317 prompt = self._get_input_buffer_cursor_prompt()
1317 prompt = self._get_input_buffer_cursor_prompt()
1318 if prompt is None:
1318 if prompt is None:
1319 return -1
1319 return -1
1320 else:
1320 else:
1321 cursor = self._control.textCursor()
1321 cursor = self._control.textCursor()
1322 return cursor.columnNumber() - len(prompt)
1322 return cursor.columnNumber() - len(prompt)
1323
1323
1324 def _get_input_buffer_cursor_line(self):
1324 def _get_input_buffer_cursor_line(self):
1325 """ Returns the text of the line of the input buffer that contains the
1325 """ Returns the text of the line of the input buffer that contains the
1326 cursor, or None if there is no such line.
1326 cursor, or None if there is no such line.
1327 """
1327 """
1328 prompt = self._get_input_buffer_cursor_prompt()
1328 prompt = self._get_input_buffer_cursor_prompt()
1329 if prompt is None:
1329 if prompt is None:
1330 return None
1330 return None
1331 else:
1331 else:
1332 cursor = self._control.textCursor()
1332 cursor = self._control.textCursor()
1333 text = self._get_block_plain_text(cursor.block())
1333 text = self._get_block_plain_text(cursor.block())
1334 return text[len(prompt):]
1334 return text[len(prompt):]
1335
1335
1336 def _get_input_buffer_cursor_prompt(self):
1336 def _get_input_buffer_cursor_prompt(self):
1337 """ Returns the (plain text) prompt for line of the input buffer that
1337 """ Returns the (plain text) prompt for line of the input buffer that
1338 contains the cursor, or None if there is no such line.
1338 contains the cursor, or None if there is no such line.
1339 """
1339 """
1340 if self._executing:
1340 if self._executing:
1341 return None
1341 return None
1342 cursor = self._control.textCursor()
1342 cursor = self._control.textCursor()
1343 if cursor.position() >= self._prompt_pos:
1343 if cursor.position() >= self._prompt_pos:
1344 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1344 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1345 return self._prompt
1345 return self._prompt
1346 else:
1346 else:
1347 return self._continuation_prompt
1347 return self._continuation_prompt
1348 else:
1348 else:
1349 return None
1349 return None
1350
1350
1351 def _get_prompt_cursor(self):
1351 def _get_prompt_cursor(self):
1352 """ Convenience method that returns a cursor for the prompt position.
1352 """ Convenience method that returns a cursor for the prompt position.
1353 """
1353 """
1354 cursor = self._control.textCursor()
1354 cursor = self._control.textCursor()
1355 cursor.setPosition(self._prompt_pos)
1355 cursor.setPosition(self._prompt_pos)
1356 return cursor
1356 return cursor
1357
1357
1358 def _get_selection_cursor(self, start, end):
1358 def _get_selection_cursor(self, start, end):
1359 """ Convenience method that returns a cursor with text selected between
1359 """ Convenience method that returns a cursor with text selected between
1360 the positions 'start' and 'end'.
1360 the positions 'start' and 'end'.
1361 """
1361 """
1362 cursor = self._control.textCursor()
1362 cursor = self._control.textCursor()
1363 cursor.setPosition(start)
1363 cursor.setPosition(start)
1364 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1364 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1365 return cursor
1365 return cursor
1366
1366
1367 def _get_word_start_cursor(self, position):
1367 def _get_word_start_cursor(self, position):
1368 """ Find the start of the word to the left the given position. If a
1368 """ 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
1369 sequence of non-word characters precedes the first word, skip over
1370 them. (This emulates the behavior of bash, emacs, etc.)
1370 them. (This emulates the behavior of bash, emacs, etc.)
1371 """
1371 """
1372 document = self._control.document()
1372 document = self._control.document()
1373 position -= 1
1373 position -= 1
1374 while position >= self._prompt_pos and \
1374 while position >= self._prompt_pos and \
1375 not document.characterAt(position).isLetterOrNumber():
1375 not document.characterAt(position).isLetterOrNumber():
1376 position -= 1
1376 position -= 1
1377 while position >= self._prompt_pos and \
1377 while position >= self._prompt_pos and \
1378 document.characterAt(position).isLetterOrNumber():
1378 document.characterAt(position).isLetterOrNumber():
1379 position -= 1
1379 position -= 1
1380 cursor = self._control.textCursor()
1380 cursor = self._control.textCursor()
1381 cursor.setPosition(position + 1)
1381 cursor.setPosition(position + 1)
1382 return cursor
1382 return cursor
1383
1383
1384 def _get_word_end_cursor(self, position):
1384 def _get_word_end_cursor(self, position):
1385 """ Find the end of the word to the right the given position. If a
1385 """ 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
1386 sequence of non-word characters precedes the first word, skip over
1387 them. (This emulates the behavior of bash, emacs, etc.)
1387 them. (This emulates the behavior of bash, emacs, etc.)
1388 """
1388 """
1389 document = self._control.document()
1389 document = self._control.document()
1390 end = self._get_end_cursor().position()
1390 end = self._get_end_cursor().position()
1391 while position < end and \
1391 while position < end and \
1392 not document.characterAt(position).isLetterOrNumber():
1392 not document.characterAt(position).isLetterOrNumber():
1393 position += 1
1393 position += 1
1394 while position < end and \
1394 while position < end and \
1395 document.characterAt(position).isLetterOrNumber():
1395 document.characterAt(position).isLetterOrNumber():
1396 position += 1
1396 position += 1
1397 cursor = self._control.textCursor()
1397 cursor = self._control.textCursor()
1398 cursor.setPosition(position)
1398 cursor.setPosition(position)
1399 return cursor
1399 return cursor
1400
1400
1401 def _insert_continuation_prompt(self, cursor):
1401 def _insert_continuation_prompt(self, cursor):
1402 """ Inserts new continuation prompt using the specified cursor.
1402 """ Inserts new continuation prompt using the specified cursor.
1403 """
1403 """
1404 if self._continuation_prompt_html is None:
1404 if self._continuation_prompt_html is None:
1405 self._insert_plain_text(cursor, self._continuation_prompt)
1405 self._insert_plain_text(cursor, self._continuation_prompt)
1406 else:
1406 else:
1407 self._continuation_prompt = self._insert_html_fetching_plain_text(
1407 self._continuation_prompt = self._insert_html_fetching_plain_text(
1408 cursor, self._continuation_prompt_html)
1408 cursor, self._continuation_prompt_html)
1409
1409
1410 def _insert_html(self, cursor, html):
1410 def _insert_html(self, cursor, html):
1411 """ Inserts HTML using the specified cursor in such a way that future
1411 """ Inserts HTML using the specified cursor in such a way that future
1412 formatting is unaffected.
1412 formatting is unaffected.
1413 """
1413 """
1414 cursor.beginEditBlock()
1414 cursor.beginEditBlock()
1415 cursor.insertHtml(html)
1415 cursor.insertHtml(html)
1416
1416
1417 # After inserting HTML, the text document "remembers" it's in "html
1417 # After inserting HTML, the text document "remembers" it's in "html
1418 # mode", which means that subsequent calls adding plain text will result
1418 # mode", which means that subsequent calls adding plain text will result
1419 # in unwanted formatting, lost tab characters, etc. The following code
1419 # 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
1420 # hacks around this behavior, which I consider to be a bug in Qt, by
1421 # (crudely) resetting the document's style state.
1421 # (crudely) resetting the document's style state.
1422 cursor.movePosition(QtGui.QTextCursor.Left,
1422 cursor.movePosition(QtGui.QTextCursor.Left,
1423 QtGui.QTextCursor.KeepAnchor)
1423 QtGui.QTextCursor.KeepAnchor)
1424 if cursor.selection().toPlainText() == ' ':
1424 if cursor.selection().toPlainText() == ' ':
1425 cursor.removeSelectedText()
1425 cursor.removeSelectedText()
1426 else:
1426 else:
1427 cursor.movePosition(QtGui.QTextCursor.Right)
1427 cursor.movePosition(QtGui.QTextCursor.Right)
1428 cursor.insertText(' ', QtGui.QTextCharFormat())
1428 cursor.insertText(' ', QtGui.QTextCharFormat())
1429 cursor.endEditBlock()
1429 cursor.endEditBlock()
1430
1430
1431 def _insert_html_fetching_plain_text(self, cursor, html):
1431 def _insert_html_fetching_plain_text(self, cursor, html):
1432 """ Inserts HTML using the specified cursor, then returns its plain text
1432 """ Inserts HTML using the specified cursor, then returns its plain text
1433 version.
1433 version.
1434 """
1434 """
1435 cursor.beginEditBlock()
1435 cursor.beginEditBlock()
1436 cursor.removeSelectedText()
1436 cursor.removeSelectedText()
1437
1437
1438 start = cursor.position()
1438 start = cursor.position()
1439 self._insert_html(cursor, html)
1439 self._insert_html(cursor, html)
1440 end = cursor.position()
1440 end = cursor.position()
1441 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1441 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1442 text = unicode(cursor.selection().toPlainText())
1442 text = unicode(cursor.selection().toPlainText())
1443
1443
1444 cursor.setPosition(end)
1444 cursor.setPosition(end)
1445 cursor.endEditBlock()
1445 cursor.endEditBlock()
1446 return text
1446 return text
1447
1447
1448 def _insert_plain_text(self, cursor, text):
1448 def _insert_plain_text(self, cursor, text):
1449 """ Inserts plain text using the specified cursor, processing ANSI codes
1449 """ Inserts plain text using the specified cursor, processing ANSI codes
1450 if enabled.
1450 if enabled.
1451 """
1451 """
1452 cursor.beginEditBlock()
1452 cursor.beginEditBlock()
1453 if self.ansi_codes:
1453 if self.ansi_codes:
1454 for substring in self._ansi_processor.split_string(text):
1454 for substring in self._ansi_processor.split_string(text):
1455 for act in self._ansi_processor.actions:
1455 for act in self._ansi_processor.actions:
1456
1456
1457 # Unlike real terminal emulators, we don't distinguish
1457 # Unlike real terminal emulators, we don't distinguish
1458 # between the screen and the scrollback buffer. A screen
1458 # between the screen and the scrollback buffer. A screen
1459 # erase request clears everything.
1459 # erase request clears everything.
1460 if act.action == 'erase' and act.area == 'screen':
1460 if act.action == 'erase' and act.area == 'screen':
1461 cursor.select(QtGui.QTextCursor.Document)
1461 cursor.select(QtGui.QTextCursor.Document)
1462 cursor.removeSelectedText()
1462 cursor.removeSelectedText()
1463
1463
1464 # Simulate a form feed by scrolling just past the last line.
1464 # Simulate a form feed by scrolling just past the last line.
1465 elif act.action == 'scroll' and act.unit == 'page':
1465 elif act.action == 'scroll' and act.unit == 'page':
1466 cursor.insertText('\n')
1466 cursor.insertText('\n')
1467 cursor.endEditBlock()
1467 cursor.endEditBlock()
1468 self._set_top_cursor(cursor)
1468 self._set_top_cursor(cursor)
1469 cursor.joinPreviousEditBlock()
1469 cursor.joinPreviousEditBlock()
1470 cursor.deletePreviousChar()
1470 cursor.deletePreviousChar()
1471
1471
1472 format = self._ansi_processor.get_format()
1472 format = self._ansi_processor.get_format()
1473 cursor.insertText(substring, format)
1473 cursor.insertText(substring, format)
1474 else:
1474 else:
1475 cursor.insertText(text)
1475 cursor.insertText(text)
1476 cursor.endEditBlock()
1476 cursor.endEditBlock()
1477
1477
1478 def _insert_plain_text_into_buffer(self, cursor, text):
1478 def _insert_plain_text_into_buffer(self, cursor, text):
1479 """ Inserts text into the input buffer using the specified cursor (which
1479 """ Inserts text into the input buffer using the specified cursor (which
1480 must be in the input buffer), ensuring that continuation prompts are
1480 must be in the input buffer), ensuring that continuation prompts are
1481 inserted as necessary.
1481 inserted as necessary.
1482 """
1482 """
1483 lines = unicode(text).splitlines(True)
1483 lines = unicode(text).splitlines(True)
1484 if lines:
1484 if lines:
1485 cursor.beginEditBlock()
1485 cursor.beginEditBlock()
1486 cursor.insertText(lines[0])
1486 cursor.insertText(lines[0])
1487 for line in lines[1:]:
1487 for line in lines[1:]:
1488 if self._continuation_prompt_html is None:
1488 if self._continuation_prompt_html is None:
1489 cursor.insertText(self._continuation_prompt)
1489 cursor.insertText(self._continuation_prompt)
1490 else:
1490 else:
1491 self._continuation_prompt = \
1491 self._continuation_prompt = \
1492 self._insert_html_fetching_plain_text(
1492 self._insert_html_fetching_plain_text(
1493 cursor, self._continuation_prompt_html)
1493 cursor, self._continuation_prompt_html)
1494 cursor.insertText(line)
1494 cursor.insertText(line)
1495 cursor.endEditBlock()
1495 cursor.endEditBlock()
1496
1496
1497 def _in_buffer(self, position=None):
1497 def _in_buffer(self, position=None):
1498 """ Returns whether the current cursor (or, if specified, a position) is
1498 """ Returns whether the current cursor (or, if specified, a position) is
1499 inside the editing region.
1499 inside the editing region.
1500 """
1500 """
1501 cursor = self._control.textCursor()
1501 cursor = self._control.textCursor()
1502 if position is None:
1502 if position is None:
1503 position = cursor.position()
1503 position = cursor.position()
1504 else:
1504 else:
1505 cursor.setPosition(position)
1505 cursor.setPosition(position)
1506 line = cursor.blockNumber()
1506 line = cursor.blockNumber()
1507 prompt_line = self._get_prompt_cursor().blockNumber()
1507 prompt_line = self._get_prompt_cursor().blockNumber()
1508 if line == prompt_line:
1508 if line == prompt_line:
1509 return position >= self._prompt_pos
1509 return position >= self._prompt_pos
1510 elif line > prompt_line:
1510 elif line > prompt_line:
1511 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1511 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1512 prompt_pos = cursor.position() + len(self._continuation_prompt)
1512 prompt_pos = cursor.position() + len(self._continuation_prompt)
1513 return position >= prompt_pos
1513 return position >= prompt_pos
1514 return False
1514 return False
1515
1515
1516 def _keep_cursor_in_buffer(self):
1516 def _keep_cursor_in_buffer(self):
1517 """ Ensures that the cursor is inside the editing region. Returns
1517 """ Ensures that the cursor is inside the editing region. Returns
1518 whether the cursor was moved.
1518 whether the cursor was moved.
1519 """
1519 """
1520 moved = not self._in_buffer()
1520 moved = not self._in_buffer()
1521 if moved:
1521 if moved:
1522 cursor = self._control.textCursor()
1522 cursor = self._control.textCursor()
1523 cursor.movePosition(QtGui.QTextCursor.End)
1523 cursor.movePosition(QtGui.QTextCursor.End)
1524 self._control.setTextCursor(cursor)
1524 self._control.setTextCursor(cursor)
1525 return moved
1525 return moved
1526
1526
1527 def _keyboard_quit(self):
1527 def _keyboard_quit(self):
1528 """ Cancels the current editing task ala Ctrl-G in Emacs.
1528 """ Cancels the current editing task ala Ctrl-G in Emacs.
1529 """
1529 """
1530 if self._text_completing_pos:
1530 if self._text_completing_pos:
1531 self._cancel_text_completion()
1531 self._cancel_text_completion()
1532 else:
1532 else:
1533 self.input_buffer = ''
1533 self.input_buffer = ''
1534
1534
1535 def _page(self, text, html=False):
1535 def _page(self, text, html=False):
1536 """ Displays text using the pager if it exceeds the height of the
1536 """ Displays text using the pager if it exceeds the height of the
1537 viewport.
1537 viewport.
1538
1538
1539 Parameters:
1539 Parameters:
1540 -----------
1540 -----------
1541 html : bool, optional (default False)
1541 html : bool, optional (default False)
1542 If set, the text will be interpreted as HTML instead of plain text.
1542 If set, the text will be interpreted as HTML instead of plain text.
1543 """
1543 """
1544 line_height = QtGui.QFontMetrics(self.font).height()
1544 line_height = QtGui.QFontMetrics(self.font).height()
1545 minlines = self._control.viewport().height() / line_height
1545 minlines = self._control.viewport().height() / line_height
1546 if self.paging != 'none' and \
1546 if self.paging != 'none' and \
1547 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1547 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1548 if self.paging == 'custom':
1548 if self.paging == 'custom':
1549 self.custom_page_requested.emit(text)
1549 self.custom_page_requested.emit(text)
1550 else:
1550 else:
1551 self._page_control.clear()
1551 self._page_control.clear()
1552 cursor = self._page_control.textCursor()
1552 cursor = self._page_control.textCursor()
1553 if html:
1553 if html:
1554 self._insert_html(cursor, text)
1554 self._insert_html(cursor, text)
1555 else:
1555 else:
1556 self._insert_plain_text(cursor, text)
1556 self._insert_plain_text(cursor, text)
1557 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1557 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1558
1558
1559 self._page_control.viewport().resize(self._control.size())
1559 self._page_control.viewport().resize(self._control.size())
1560 if self._splitter:
1560 if self._splitter:
1561 self._page_control.show()
1561 self._page_control.show()
1562 self._page_control.setFocus()
1562 self._page_control.setFocus()
1563 else:
1563 else:
1564 self.layout().setCurrentWidget(self._page_control)
1564 self.layout().setCurrentWidget(self._page_control)
1565 elif html:
1565 elif html:
1566 self._append_plain_html(text)
1566 self._append_plain_html(text)
1567 else:
1567 else:
1568 self._append_plain_text(text)
1568 self._append_plain_text(text)
1569
1569
1570 def _prompt_finished(self):
1570 def _prompt_finished(self):
1571 """ Called immediately after a prompt is finished, i.e. when some input
1571 """ Called immediately after a prompt is finished, i.e. when some input
1572 will be processed and a new prompt displayed.
1572 will be processed and a new prompt displayed.
1573 """
1573 """
1574 # Flush all state from the input splitter so the next round of
1574 # Flush all state from the input splitter so the next round of
1575 # reading input starts with a clean buffer.
1575 # reading input starts with a clean buffer.
1576 self._input_splitter.reset()
1576 self._input_splitter.reset()
1577
1577
1578 self._control.setReadOnly(True)
1578 self._control.setReadOnly(True)
1579 self._prompt_finished_hook()
1579 self._prompt_finished_hook()
1580
1580
1581 def _prompt_started(self):
1581 def _prompt_started(self):
1582 """ Called immediately after a new prompt is displayed.
1582 """ Called immediately after a new prompt is displayed.
1583 """
1583 """
1584 # Temporarily disable the maximum block count to permit undo/redo and
1584 # Temporarily disable the maximum block count to permit undo/redo and
1585 # to ensure that the prompt position does not change due to truncation.
1585 # to ensure that the prompt position does not change due to truncation.
1586 self._control.document().setMaximumBlockCount(0)
1586 self._control.document().setMaximumBlockCount(0)
1587 self._control.setUndoRedoEnabled(True)
1587 self._control.setUndoRedoEnabled(True)
1588
1588
1589 self._control.setReadOnly(False)
1589 self._control.setReadOnly(False)
1590 self._control.moveCursor(QtGui.QTextCursor.End)
1590 self._control.moveCursor(QtGui.QTextCursor.End)
1591 self._executing = False
1591 self._executing = False
1592 self._prompt_started_hook()
1592 self._prompt_started_hook()
1593
1593
1594 def _readline(self, prompt='', callback=None):
1594 def _readline(self, prompt='', callback=None):
1595 """ Reads one line of input from the user.
1595 """ Reads one line of input from the user.
1596
1596
1597 Parameters
1597 Parameters
1598 ----------
1598 ----------
1599 prompt : str, optional
1599 prompt : str, optional
1600 The prompt to print before reading the line.
1600 The prompt to print before reading the line.
1601
1601
1602 callback : callable, optional
1602 callback : callable, optional
1603 A callback to execute with the read line. If not specified, input is
1603 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
1604 read *synchronously* and this method does not return until it has
1605 been read.
1605 been read.
1606
1606
1607 Returns
1607 Returns
1608 -------
1608 -------
1609 If a callback is specified, returns nothing. Otherwise, returns the
1609 If a callback is specified, returns nothing. Otherwise, returns the
1610 input string with the trailing newline stripped.
1610 input string with the trailing newline stripped.
1611 """
1611 """
1612 if self._reading:
1612 if self._reading:
1613 raise RuntimeError('Cannot read a line. Widget is already reading.')
1613 raise RuntimeError('Cannot read a line. Widget is already reading.')
1614
1614
1615 if not callback and not self.isVisible():
1615 if not callback and not self.isVisible():
1616 # If the user cannot see the widget, this function cannot return.
1616 # If the user cannot see the widget, this function cannot return.
1617 raise RuntimeError('Cannot synchronously read a line if the widget '
1617 raise RuntimeError('Cannot synchronously read a line if the widget '
1618 'is not visible!')
1618 'is not visible!')
1619
1619
1620 self._reading = True
1620 self._reading = True
1621 self._show_prompt(prompt, newline=False)
1621 self._show_prompt(prompt, newline=False)
1622
1622
1623 if callback is None:
1623 if callback is None:
1624 self._reading_callback = None
1624 self._reading_callback = None
1625 while self._reading:
1625 while self._reading:
1626 QtCore.QCoreApplication.processEvents()
1626 QtCore.QCoreApplication.processEvents()
1627 return self.input_buffer.rstrip('\n')
1627 return self.input_buffer.rstrip('\n')
1628
1628
1629 else:
1629 else:
1630 self._reading_callback = lambda: \
1630 self._reading_callback = lambda: \
1631 callback(self.input_buffer.rstrip('\n'))
1631 callback(self.input_buffer.rstrip('\n'))
1632
1632
1633 def _set_continuation_prompt(self, prompt, html=False):
1633 def _set_continuation_prompt(self, prompt, html=False):
1634 """ Sets the continuation prompt.
1634 """ Sets the continuation prompt.
1635
1635
1636 Parameters
1636 Parameters
1637 ----------
1637 ----------
1638 prompt : str
1638 prompt : str
1639 The prompt to show when more input is needed.
1639 The prompt to show when more input is needed.
1640
1640
1641 html : bool, optional (default False)
1641 html : bool, optional (default False)
1642 If set, the prompt will be inserted as formatted HTML. Otherwise,
1642 If set, the prompt will be inserted as formatted HTML. Otherwise,
1643 the prompt will be treated as plain text, though ANSI color codes
1643 the prompt will be treated as plain text, though ANSI color codes
1644 will be handled.
1644 will be handled.
1645 """
1645 """
1646 if html:
1646 if html:
1647 self._continuation_prompt_html = prompt
1647 self._continuation_prompt_html = prompt
1648 else:
1648 else:
1649 self._continuation_prompt = prompt
1649 self._continuation_prompt = prompt
1650 self._continuation_prompt_html = None
1650 self._continuation_prompt_html = None
1651
1651
1652 def _set_cursor(self, cursor):
1652 def _set_cursor(self, cursor):
1653 """ Convenience method to set the current cursor.
1653 """ Convenience method to set the current cursor.
1654 """
1654 """
1655 self._control.setTextCursor(cursor)
1655 self._control.setTextCursor(cursor)
1656
1656
1657 def _set_top_cursor(self, cursor):
1657 def _set_top_cursor(self, cursor):
1658 """ Scrolls the viewport so that the specified cursor is at the top.
1658 """ Scrolls the viewport so that the specified cursor is at the top.
1659 """
1659 """
1660 scrollbar = self._control.verticalScrollBar()
1660 scrollbar = self._control.verticalScrollBar()
1661 scrollbar.setValue(scrollbar.maximum())
1661 scrollbar.setValue(scrollbar.maximum())
1662 original_cursor = self._control.textCursor()
1662 original_cursor = self._control.textCursor()
1663 self._control.setTextCursor(cursor)
1663 self._control.setTextCursor(cursor)
1664 self._control.ensureCursorVisible()
1664 self._control.ensureCursorVisible()
1665 self._control.setTextCursor(original_cursor)
1665 self._control.setTextCursor(original_cursor)
1666
1666
1667 def _show_prompt(self, prompt=None, html=False, newline=True):
1667 def _show_prompt(self, prompt=None, html=False, newline=True):
1668 """ Writes a new prompt at the end of the buffer.
1668 """ Writes a new prompt at the end of the buffer.
1669
1669
1670 Parameters
1670 Parameters
1671 ----------
1671 ----------
1672 prompt : str, optional
1672 prompt : str, optional
1673 The prompt to show. If not specified, the previous prompt is used.
1673 The prompt to show. If not specified, the previous prompt is used.
1674
1674
1675 html : bool, optional (default False)
1675 html : bool, optional (default False)
1676 Only relevant when a prompt is specified. If set, the prompt will
1676 Only relevant when a prompt is specified. If set, the prompt will
1677 be inserted as formatted HTML. Otherwise, the prompt will be treated
1677 be inserted as formatted HTML. Otherwise, the prompt will be treated
1678 as plain text, though ANSI color codes will be handled.
1678 as plain text, though ANSI color codes will be handled.
1679
1679
1680 newline : bool, optional (default True)
1680 newline : bool, optional (default True)
1681 If set, a new line will be written before showing the prompt if
1681 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.
1682 there is not already a newline at the end of the buffer.
1683 """
1683 """
1684 # Insert a preliminary newline, if necessary.
1684 # Insert a preliminary newline, if necessary.
1685 if newline:
1685 if newline:
1686 cursor = self._get_end_cursor()
1686 cursor = self._get_end_cursor()
1687 if cursor.position() > 0:
1687 if cursor.position() > 0:
1688 cursor.movePosition(QtGui.QTextCursor.Left,
1688 cursor.movePosition(QtGui.QTextCursor.Left,
1689 QtGui.QTextCursor.KeepAnchor)
1689 QtGui.QTextCursor.KeepAnchor)
1690 if unicode(cursor.selection().toPlainText()) != '\n':
1690 if unicode(cursor.selection().toPlainText()) != '\n':
1691 self._append_plain_text('\n')
1691 self._append_plain_text('\n')
1692
1692
1693 # Write the prompt.
1693 # Write the prompt.
1694 self._append_plain_text(self._prompt_sep)
1694 self._append_plain_text(self._prompt_sep)
1695 if prompt is None:
1695 if prompt is None:
1696 if self._prompt_html is None:
1696 if self._prompt_html is None:
1697 self._append_plain_text(self._prompt)
1697 self._append_plain_text(self._prompt)
1698 else:
1698 else:
1699 self._append_html(self._prompt_html)
1699 self._append_html(self._prompt_html)
1700 else:
1700 else:
1701 if html:
1701 if html:
1702 self._prompt = self._append_html_fetching_plain_text(prompt)
1702 self._prompt = self._append_html_fetching_plain_text(prompt)
1703 self._prompt_html = prompt
1703 self._prompt_html = prompt
1704 else:
1704 else:
1705 self._append_plain_text(prompt)
1705 self._append_plain_text(prompt)
1706 self._prompt = prompt
1706 self._prompt = prompt
1707 self._prompt_html = None
1707 self._prompt_html = None
1708
1708
1709 self._prompt_pos = self._get_end_cursor().position()
1709 self._prompt_pos = self._get_end_cursor().position()
1710 self._prompt_started()
1710 self._prompt_started()
1711
1711
1712 #------ Signal handlers ----------------------------------------------------
1712 #------ Signal handlers ----------------------------------------------------
1713
1713
1714 def _adjust_scrollbars(self):
1714 def _adjust_scrollbars(self):
1715 """ Expands the vertical scrollbar beyond the range set by Qt.
1715 """ Expands the vertical scrollbar beyond the range set by Qt.
1716 """
1716 """
1717 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1717 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1718 # and qtextedit.cpp.
1718 # and qtextedit.cpp.
1719 document = self._control.document()
1719 document = self._control.document()
1720 scrollbar = self._control.verticalScrollBar()
1720 scrollbar = self._control.verticalScrollBar()
1721 viewport_height = self._control.viewport().height()
1721 viewport_height = self._control.viewport().height()
1722 if isinstance(self._control, QtGui.QPlainTextEdit):
1722 if isinstance(self._control, QtGui.QPlainTextEdit):
1723 maximum = max(0, document.lineCount() - 1)
1723 maximum = max(0, document.lineCount() - 1)
1724 step = viewport_height / self._control.fontMetrics().lineSpacing()
1724 step = viewport_height / self._control.fontMetrics().lineSpacing()
1725 else:
1725 else:
1726 # QTextEdit does not do line-based layout and blocks will not in
1726 # 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
1727 # general have the same height. Therefore it does not make sense to
1728 # attempt to scroll in line height increments.
1728 # attempt to scroll in line height increments.
1729 maximum = document.size().height()
1729 maximum = document.size().height()
1730 step = viewport_height
1730 step = viewport_height
1731 diff = maximum - scrollbar.maximum()
1731 diff = maximum - scrollbar.maximum()
1732 scrollbar.setRange(0, maximum)
1732 scrollbar.setRange(0, maximum)
1733 scrollbar.setPageStep(step)
1733 scrollbar.setPageStep(step)
1734 # Compensate for undesirable scrolling that occurs automatically due to
1734 # Compensate for undesirable scrolling that occurs automatically due to
1735 # maximumBlockCount() text truncation.
1735 # maximumBlockCount() text truncation.
1736 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1736 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1737 scrollbar.setValue(scrollbar.value() + diff)
1737 scrollbar.setValue(scrollbar.value() + diff)
1738
1738
1739 def _cursor_position_changed(self):
1739 def _cursor_position_changed(self):
1740 """ Clears the temporary buffer based on the cursor position.
1740 """ Clears the temporary buffer based on the cursor position.
1741 """
1741 """
1742 if self._text_completing_pos:
1742 if self._text_completing_pos:
1743 document = self._control.document()
1743 document = self._control.document()
1744 if self._text_completing_pos < document.characterCount():
1744 if self._text_completing_pos < document.characterCount():
1745 cursor = self._control.textCursor()
1745 cursor = self._control.textCursor()
1746 pos = cursor.position()
1746 pos = cursor.position()
1747 text_cursor = self._control.textCursor()
1747 text_cursor = self._control.textCursor()
1748 text_cursor.setPosition(self._text_completing_pos)
1748 text_cursor.setPosition(self._text_completing_pos)
1749 if pos < self._text_completing_pos or \
1749 if pos < self._text_completing_pos or \
1750 cursor.blockNumber() > text_cursor.blockNumber():
1750 cursor.blockNumber() > text_cursor.blockNumber():
1751 self._clear_temporary_buffer()
1751 self._clear_temporary_buffer()
1752 self._text_completing_pos = 0
1752 self._text_completing_pos = 0
1753 else:
1753 else:
1754 self._clear_temporary_buffer()
1754 self._clear_temporary_buffer()
1755 self._text_completing_pos = 0
1755 self._text_completing_pos = 0
1756
1756
1757 def _custom_context_menu_requested(self, pos):
1757 def _custom_context_menu_requested(self, pos):
1758 """ Shows a context menu at the given QPoint (in widget coordinates).
1758 """ Shows a context menu at the given QPoint (in widget coordinates).
1759 """
1759 """
1760 menu = self._context_menu_make(pos)
1760 menu = self._context_menu_make(pos)
1761 menu.exec_(self._control.mapToGlobal(pos))
1761 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,177 +1,177 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._name2svg = {}
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._name2svg[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 imagetag(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 """ Given an re.match object matching an image name in an HTML dump,
131 return an appropriate substitution string for the image tag
131 return an appropriate substitution string for the image tag
132 (e.g., link, embedded image, ...). As a side effect, files may
132 (e.g., link, embedded image, ...). As a side effect, files may
133 be generated in the directory given by path."""
133 be generated in the directory given by path."""
134
134
135 if(format == "PNG"):
135 if(format == "PNG"):
136 try:
136 try:
137 image = self._get_image(match.group("name"))
137 image = self._get_image(match.group("name"))
138 except KeyError:
138 except KeyError:
139 return "<b>Couldn't find image %s</b>" % match.group("name")
139 return "<b>Couldn't find image %s</b>" % match.group("name")
140
140
141 if(path is not None):
141 if(path is not None):
142 relpath = path[path.rfind("/")+1:]
142 relpath = path[path.rfind("/")+1:]
143 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
143 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
144 "PNG")):
144 "PNG")):
145 return '<img src="%s/qt_img%s.png">' % (relpath,
145 return '<img src="%s/qt_img%s.png">' % (relpath,
146 match.group("name"))
146 match.group("name"))
147 else:
147 else:
148 return "<b>Couldn't save image!</b>"
148 return "<b>Couldn't save image!</b>"
149 else:
149 else:
150 ba = QtCore.QByteArray()
150 ba = QtCore.QByteArray()
151 buffer_ = QtCore.QBuffer(ba)
151 buffer_ = QtCore.QBuffer(ba)
152 buffer_.open(QtCore.QIODevice.WriteOnly)
152 buffer_.open(QtCore.QIODevice.WriteOnly)
153 image.save(buffer_, "PNG")
153 image.save(buffer_, "PNG")
154 buffer_.close()
154 buffer_.close()
155 import re
155 import re
156 return '<img src="data:image/png;base64,\n%s\n" />' % (
156 return '<img src="data:image/png;base64,\n%s\n" />' % (
157 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
157 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
158
158
159 elif(format == "SVG"):
159 elif(format == "SVG"):
160 try:
160 try:
161 svg = str(self._name2svg[match.group("name")])
161 svg = str(self._name_to_svg[match.group("name")])
162 except KeyError:
162 except KeyError:
163 return "<b>Couldn't find image %s</b>" % match.group("name")
163 return "<b>Couldn't find image %s</b>" % match.group("name")
164
164
165 # Not currently checking path, because it's tricky to find a
165 # Not currently checking path, because it's tricky to find a
166 # cross-browser way to embed external SVG images (e.g., via
166 # cross-browser way to embed external SVG images (e.g., via
167 # object or embed tags).
167 # object or embed tags).
168
168
169 # Chop stand-alone header from matplotlib SVG
169 # Chop stand-alone header from matplotlib SVG
170 offset = svg.find("<svg")
170 offset = svg.find("<svg")
171 assert(offset > -1)
171 assert(offset > -1)
172
172
173 return svg[offset:]
173 return svg[offset:]
174
174
175 else:
175 else:
176 return '<b>Unrecognized image format</b>'
176 return '<b>Unrecognized image format</b>'
177
177
General Comments 0
You need to be logged in to leave comments. Login now