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