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