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