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