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