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