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