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