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