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