##// END OF EJS Templates
* Added a pager with several different options to ConsoleWidget....
epatters -
Show More
@@ -1,1096 +1,1229 b''
1 # Standard library imports
1 # Standard library imports
2 import sys
2 import sys
3 from textwrap import dedent
3 from textwrap import dedent
4
4
5 # System library imports
5 # System library imports
6 from PyQt4 import QtCore, QtGui
6 from PyQt4 import QtCore, QtGui
7
7
8 # Local imports
8 # Local imports
9 from ansi_code_processor import QtAnsiCodeProcessor
9 from ansi_code_processor import QtAnsiCodeProcessor
10 from completion_widget import CompletionWidget
10 from completion_widget import CompletionWidget
11
11
12
12
13 class ConsoleWidget(QtGui.QWidget):
13 class ConsoleWidget(QtGui.QWidget):
14 """ Base class for console-type widgets. This class is mainly concerned with
14 """ An abstract base class for console-type widgets. This class has
15 dealing with the prompt, keeping the cursor inside the editing line, and
15 functionality for:
16 handling ANSI escape sequences.
16
17 * Maintaining a prompt and editing region
18 * Providing the traditional Unix-style console keyboard shortcuts
19 * Performing tab completion
20 * Paging text
21 * Handling ANSI escape codes
22
23 ConsoleWidget also provides a number of utility methods that will be
24 convenient to implementors of a console-style widget.
17 """
25 """
18
26
19 # Whether to process ANSI escape codes.
27 # Whether to process ANSI escape codes.
20 ansi_codes = True
28 ansi_codes = True
21
29
22 # The maximum number of lines of text before truncation.
30 # The maximum number of lines of text before truncation.
23 buffer_size = 500
31 buffer_size = 500
24
32
25 # Whether to use a list widget or plain text output for tab completion.
33 # Whether to use a list widget or plain text output for tab completion.
26 gui_completion = True
34 gui_completion = True
27
35
28 # Whether to override ShortcutEvents for the keybindings defined by this
36 # Whether to override ShortcutEvents for the keybindings defined by this
29 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
37 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
30 # priority (when it has focus) over, e.g., window-level menu shortcuts.
38 # priority (when it has focus) over, e.g., window-level menu shortcuts.
31 override_shortcuts = False
39 override_shortcuts = False
32
40
33 # Signals that indicate ConsoleWidget state.
41 # Signals that indicate ConsoleWidget state.
34 copy_available = QtCore.pyqtSignal(bool)
42 copy_available = QtCore.pyqtSignal(bool)
35 redo_available = QtCore.pyqtSignal(bool)
43 redo_available = QtCore.pyqtSignal(bool)
36 undo_available = QtCore.pyqtSignal(bool)
44 undo_available = QtCore.pyqtSignal(bool)
37
45
46 # Signal emitted when paging is needed and the paging style has been
47 # specified as 'custom'.
48 custom_page_requested = QtCore.pyqtSignal(QtCore.QString)
49
38 # Protected class variables.
50 # Protected class variables.
39 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
51 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
40 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
52 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
41 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
53 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
42 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
54 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
43 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
55 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
44 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
56 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
45 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
57 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
46 _shortcuts = set(_ctrl_down_remap.keys() +
58 _shortcuts = set(_ctrl_down_remap.keys() +
47 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
59 [ QtCore.Qt.Key_C, QtCore.Qt.Key_V ])
48
60
49 #---------------------------------------------------------------------------
61 #---------------------------------------------------------------------------
50 # 'QObject' interface
62 # 'QObject' interface
51 #---------------------------------------------------------------------------
63 #---------------------------------------------------------------------------
52
64
53 def __init__(self, kind='plain', parent=None):
65 def __init__(self, kind='plain', paging='inside', parent=None):
54 """ Create a ConsoleWidget.
66 """ Create a ConsoleWidget.
55
67
56 Parameters
68 Parameters
57 ----------
69 ----------
58 kind : str, optional [default 'plain']
70 kind : str, optional [default 'plain']
59 The type of text widget to use. Valid values are 'plain', which
71 The type of underlying text widget to use. Valid values are 'plain',
60 specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
72 which specifies a QPlainTextEdit, and 'rich', which specifies a
73 QTextEdit.
74
75 paging : str, optional [default 'inside']
76 The type of paging to use. Valid values are:
77 'inside' : The widget pages like a traditional terminal pager.
78 'hsplit' : When paging is requested, the widget is split
79 horizontally. The top pane contains the console,
80 and the bottom pane contains the paged text.
81 'vsplit' : Similar to 'hsplit', except that a vertical splitter
82 used.
83 'custom' : No action is taken by the widget beyond emitting a
84 'custom_page_requested(QString)' signal.
85 'none' : The text is written directly to the console.
61
86
62 parent : QWidget, optional [default None]
87 parent : QWidget, optional [default None]
63 The parent for this widget.
88 The parent for this widget.
64 """
89 """
65 super(ConsoleWidget, self).__init__(parent)
90 super(ConsoleWidget, self).__init__(parent)
66
91
67 # Create and set the underlying text widget.
92 # Create the layout and underlying text widget.
68 layout = QtGui.QVBoxLayout(self)
93 layout = QtGui.QStackedLayout(self)
69 layout.setMargin(0)
94 layout.setMargin(0)
70 self._control = self._create_control(kind)
95 self._control = self._create_control(kind)
71 layout.addWidget(self._control)
96 self._page_control = None
97 self._splitter = None
98 if paging in ('hsplit', 'vsplit'):
99 self._splitter = QtGui.QSplitter()
100 if paging == 'hsplit':
101 self._splitter.setOrientation(QtCore.Qt.Horizontal)
102 else:
103 self._splitter.setOrientation(QtCore.Qt.Vertical)
104 self._splitter.addWidget(self._control)
105 layout.addWidget(self._splitter)
106 else:
107 layout.addWidget(self._control)
108
109 # Create the paging widget, if necessary.
110 self._page_style = paging
111 if paging in ('inside', 'hsplit', 'vsplit'):
112 self._page_control = self._create_page_control()
113 if self._splitter:
114 self._page_control.hide()
115 self._splitter.addWidget(self._page_control)
116 else:
117 layout.addWidget(self._page_control)
118 elif paging not in ('custom', 'none'):
119 raise ValueError('Paging style %s unknown.' % repr(paging))
72
120
73 # Initialize protected variables. Some variables contain useful state
121 # Initialize protected variables. Some variables contain useful state
74 # information for subclasses; they should be considered read-only.
122 # information for subclasses; they should be considered read-only.
75 self._ansi_processor = QtAnsiCodeProcessor()
123 self._ansi_processor = QtAnsiCodeProcessor()
76 self._completion_widget = CompletionWidget(self._control)
124 self._completion_widget = CompletionWidget(self._control)
77 self._continuation_prompt = '> '
125 self._continuation_prompt = '> '
78 self._continuation_prompt_html = None
126 self._continuation_prompt_html = None
79 self._executing = False
127 self._executing = False
80 self._prompt = ''
128 self._prompt = ''
81 self._prompt_html = None
129 self._prompt_html = None
82 self._prompt_pos = 0
130 self._prompt_pos = 0
83 self._reading = False
131 self._reading = False
84 self._reading_callback = None
132 self._reading_callback = None
85 self._tab_width = 8
133 self._tab_width = 8
86
134
87 # Set a monospaced font.
135 # Set a monospaced font.
88 self.reset_font()
136 self.reset_font()
89
137
90 def eventFilter(self, obj, event):
138 def eventFilter(self, obj, event):
91 """ Reimplemented to ensure a console-like behavior in the underlying
139 """ Reimplemented to ensure a console-like behavior in the underlying
92 text widget.
140 text widget.
93 """
141 """
94 # Re-map keys for all filtered widgets.
142 # Re-map keys for all filtered widgets.
95 etype = event.type()
143 etype = event.type()
96 if etype == QtCore.QEvent.KeyPress and \
144 if etype == QtCore.QEvent.KeyPress and \
97 self._control_key_down(event.modifiers()) and \
145 self._control_key_down(event.modifiers()) and \
98 event.key() in self._ctrl_down_remap:
146 event.key() in self._ctrl_down_remap:
99 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
147 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
100 self._ctrl_down_remap[event.key()],
148 self._ctrl_down_remap[event.key()],
101 QtCore.Qt.NoModifier)
149 QtCore.Qt.NoModifier)
102 QtGui.qApp.sendEvent(obj, new_event)
150 QtGui.qApp.sendEvent(obj, new_event)
103 return True
151 return True
104
152
105 # Override shortucts for all filtered widgets. Note that on Mac OS it is
153 # Override shortucts for all filtered widgets. Note that on Mac OS it is
106 # always unnecessary to override shortcuts, hence the check below (users
154 # always unnecessary to override shortcuts, hence the check below (users
107 # should just use the Control key instead of the Command key).
155 # should just use the Control key instead of the Command key).
108 elif etype == QtCore.QEvent.ShortcutOverride and \
156 elif etype == QtCore.QEvent.ShortcutOverride and \
109 sys.platform != 'darwin' and \
157 sys.platform != 'darwin' and \
110 self._control_key_down(event.modifiers()) and \
158 self._control_key_down(event.modifiers()) and \
111 event.key() in self._shortcuts:
159 event.key() in self._shortcuts:
112 event.accept()
160 event.accept()
113 return False
161 return False
114
162
115 elif obj == self._control:
163 elif obj == self._control:
116 # Disable moving text by drag and drop.
164 # Disable moving text by drag and drop.
117 if etype == QtCore.QEvent.DragMove:
165 if etype == QtCore.QEvent.DragMove:
118 return True
166 return True
119
167
120 elif etype == QtCore.QEvent.KeyPress:
168 elif etype == QtCore.QEvent.KeyPress:
121 return self._event_filter_keypress(event)
169 return self._event_filter_console_keypress(event)
170
171 elif obj == self._page_control:
172 if etype == QtCore.QEvent.KeyPress:
173 return self._event_filter_page_keypress(event)
122
174
123 return super(ConsoleWidget, self).eventFilter(obj, event)
175 return super(ConsoleWidget, self).eventFilter(obj, event)
124
176
125 #---------------------------------------------------------------------------
177 #---------------------------------------------------------------------------
178 # 'QWidget' interface
179 #---------------------------------------------------------------------------
180
181 def sizeHint(self):
182 """ Reimplemented to suggest a size that is 80 characters wide and
183 25 lines high.
184 """
185 style = self.style()
186 opt = QtGui.QStyleOptionHeader()
187 font_metrics = QtGui.QFontMetrics(self.font)
188 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth, opt, self)
189
190 width = font_metrics.width(' ') * 80
191 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent, opt, self)
192 if self._page_style == 'hsplit':
193 width = width * 2 + splitwidth
194
195 height = font_metrics.height() * 25
196 if self._page_style == 'vsplit':
197 height = height * 2 + splitwidth
198
199 return QtCore.QSize(width, height)
200
201 #---------------------------------------------------------------------------
126 # 'ConsoleWidget' public interface
202 # 'ConsoleWidget' public interface
127 #---------------------------------------------------------------------------
203 #---------------------------------------------------------------------------
128
204
129 def can_paste(self):
205 def can_paste(self):
130 """ Returns whether text can be pasted from the clipboard.
206 """ Returns whether text can be pasted from the clipboard.
131 """
207 """
132 # Accept only text that can be ASCII encoded.
208 # Accept only text that can be ASCII encoded.
133 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
209 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
134 text = QtGui.QApplication.clipboard().text()
210 text = QtGui.QApplication.clipboard().text()
135 if not text.isEmpty():
211 if not text.isEmpty():
136 try:
212 try:
137 str(text)
213 str(text)
138 return True
214 return True
139 except UnicodeEncodeError:
215 except UnicodeEncodeError:
140 pass
216 pass
141 return False
217 return False
142
218
143 def clear(self, keep_input=False):
219 def clear(self, keep_input=False):
144 """ Clear the console, then write a new prompt. If 'keep_input' is set,
220 """ Clear the console, then write a new prompt. If 'keep_input' is set,
145 restores the old input buffer when the new prompt is written.
221 restores the old input buffer when the new prompt is written.
146 """
222 """
147 self._control.clear()
223 self._control.clear()
148 if keep_input:
224 if keep_input:
149 input_buffer = self.input_buffer
225 input_buffer = self.input_buffer
150 self._show_prompt()
226 self._show_prompt()
151 if keep_input:
227 if keep_input:
152 self.input_buffer = input_buffer
228 self.input_buffer = input_buffer
153
229
154 def copy(self):
230 def copy(self):
155 """ Copy the current selected text to the clipboard.
231 """ Copy the current selected text to the clipboard.
156 """
232 """
157 self._control.copy()
233 self._control.copy()
158
234
159 def execute(self, source=None, hidden=False, interactive=False):
235 def execute(self, source=None, hidden=False, interactive=False):
160 """ Executes source or the input buffer, possibly prompting for more
236 """ Executes source or the input buffer, possibly prompting for more
161 input.
237 input.
162
238
163 Parameters:
239 Parameters:
164 -----------
240 -----------
165 source : str, optional
241 source : str, optional
166
242
167 The source to execute. If not specified, the input buffer will be
243 The source to execute. If not specified, the input buffer will be
168 used. If specified and 'hidden' is False, the input buffer will be
244 used. If specified and 'hidden' is False, the input buffer will be
169 replaced with the source before execution.
245 replaced with the source before execution.
170
246
171 hidden : bool, optional (default False)
247 hidden : bool, optional (default False)
172
248
173 If set, no output will be shown and the prompt will not be modified.
249 If set, no output will be shown and the prompt will not be modified.
174 In other words, it will be completely invisible to the user that
250 In other words, it will be completely invisible to the user that
175 an execution has occurred.
251 an execution has occurred.
176
252
177 interactive : bool, optional (default False)
253 interactive : bool, optional (default False)
178
254
179 Whether the console is to treat the source as having been manually
255 Whether the console is to treat the source as having been manually
180 entered by the user. The effect of this parameter depends on the
256 entered by the user. The effect of this parameter depends on the
181 subclass implementation.
257 subclass implementation.
182
258
183 Raises:
259 Raises:
184 -------
260 -------
185 RuntimeError
261 RuntimeError
186 If incomplete input is given and 'hidden' is True. In this case,
262 If incomplete input is given and 'hidden' is True. In this case,
187 it is not possible to prompt for more input.
263 it is not possible to prompt for more input.
188
264
189 Returns:
265 Returns:
190 --------
266 --------
191 A boolean indicating whether the source was executed.
267 A boolean indicating whether the source was executed.
192 """
268 """
193 if not hidden:
269 if not hidden:
194 if source is not None:
270 if source is not None:
195 self.input_buffer = source
271 self.input_buffer = source
196
272
197 self._append_plain_text('\n')
273 self._append_plain_text('\n')
198 self._executing_input_buffer = self.input_buffer
274 self._executing_input_buffer = self.input_buffer
199 self._executing = True
275 self._executing = True
200 self._prompt_finished()
276 self._prompt_finished()
201
277
202 real_source = self.input_buffer if source is None else source
278 real_source = self.input_buffer if source is None else source
203 complete = self._is_complete(real_source, interactive)
279 complete = self._is_complete(real_source, interactive)
204 if complete:
280 if complete:
205 if not hidden:
281 if not hidden:
206 # The maximum block count is only in effect during execution.
282 # The maximum block count is only in effect during execution.
207 # This ensures that _prompt_pos does not become invalid due to
283 # This ensures that _prompt_pos does not become invalid due to
208 # text truncation.
284 # text truncation.
209 self._control.document().setMaximumBlockCount(self.buffer_size)
285 self._control.document().setMaximumBlockCount(self.buffer_size)
210 self._execute(real_source, hidden)
286 self._execute(real_source, hidden)
211 elif hidden:
287 elif hidden:
212 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
288 raise RuntimeError('Incomplete noninteractive input: "%s"' % source)
213 else:
289 else:
214 self._show_continuation_prompt()
290 self._show_continuation_prompt()
215
291
216 return complete
292 return complete
217
293
218 def _get_input_buffer(self):
294 def _get_input_buffer(self):
219 """ The text that the user has entered entered at the current prompt.
295 """ The text that the user has entered entered at the current prompt.
220 """
296 """
221 # If we're executing, the input buffer may not even exist anymore due to
297 # If we're executing, the input buffer may not even exist anymore due to
222 # the limit imposed by 'buffer_size'. Therefore, we store it.
298 # the limit imposed by 'buffer_size'. Therefore, we store it.
223 if self._executing:
299 if self._executing:
224 return self._executing_input_buffer
300 return self._executing_input_buffer
225
301
226 cursor = self._get_end_cursor()
302 cursor = self._get_end_cursor()
227 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
303 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
228 input_buffer = str(cursor.selection().toPlainText())
304 input_buffer = str(cursor.selection().toPlainText())
229
305
230 # Strip out continuation prompts.
306 # Strip out continuation prompts.
231 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
307 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
232
308
233 def _set_input_buffer(self, string):
309 def _set_input_buffer(self, string):
234 """ Replaces the text in the input buffer with 'string'.
310 """ Replaces the text in the input buffer with 'string'.
235 """
311 """
236 # For now, it is an error to modify the input buffer during execution.
312 # For now, it is an error to modify the input buffer during execution.
237 if self._executing:
313 if self._executing:
238 raise RuntimeError("Cannot change input buffer during execution.")
314 raise RuntimeError("Cannot change input buffer during execution.")
239
315
240 # Remove old text.
316 # Remove old text.
241 cursor = self._get_end_cursor()
317 cursor = self._get_end_cursor()
242 cursor.beginEditBlock()
318 cursor.beginEditBlock()
243 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
319 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
244 cursor.removeSelectedText()
320 cursor.removeSelectedText()
245
321
246 # Insert new text with continuation prompts.
322 # Insert new text with continuation prompts.
247 lines = string.splitlines(True)
323 lines = string.splitlines(True)
248 if lines:
324 if lines:
249 self._append_plain_text(lines[0])
325 self._append_plain_text(lines[0])
250 for i in xrange(1, len(lines)):
326 for i in xrange(1, len(lines)):
251 if self._continuation_prompt_html is None:
327 if self._continuation_prompt_html is None:
252 self._append_plain_text(self._continuation_prompt)
328 self._append_plain_text(self._continuation_prompt)
253 else:
329 else:
254 self._append_html(self._continuation_prompt_html)
330 self._append_html(self._continuation_prompt_html)
255 self._append_plain_text(lines[i])
331 self._append_plain_text(lines[i])
256 cursor.endEditBlock()
332 cursor.endEditBlock()
257 self._control.moveCursor(QtGui.QTextCursor.End)
333 self._control.moveCursor(QtGui.QTextCursor.End)
258
334
259 input_buffer = property(_get_input_buffer, _set_input_buffer)
335 input_buffer = property(_get_input_buffer, _set_input_buffer)
260
336
261 def _get_font(self):
337 def _get_font(self):
262 """ The base font being used by the ConsoleWidget.
338 """ The base font being used by the ConsoleWidget.
263 """
339 """
264 return self._control.document().defaultFont()
340 return self._control.document().defaultFont()
265
341
266 def _set_font(self, font):
342 def _set_font(self, font):
267 """ Sets the base font for the ConsoleWidget to the specified QFont.
343 """ Sets the base font for the ConsoleWidget to the specified QFont.
268 """
344 """
269 font_metrics = QtGui.QFontMetrics(font)
345 font_metrics = QtGui.QFontMetrics(font)
270 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
346 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
271
347
272 self._completion_widget.setFont(font)
348 self._completion_widget.setFont(font)
273 self._control.document().setDefaultFont(font)
349 self._control.document().setDefaultFont(font)
274
350
275 font = property(_get_font, _set_font)
351 font = property(_get_font, _set_font)
276
352
277 def paste(self):
353 def paste(self):
278 """ Paste the contents of the clipboard into the input region.
354 """ Paste the contents of the clipboard into the input region.
279 """
355 """
280 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
356 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
281 try:
357 try:
282 text = str(QtGui.QApplication.clipboard().text())
358 text = str(QtGui.QApplication.clipboard().text())
283 except UnicodeEncodeError:
359 except UnicodeEncodeError:
284 pass
360 pass
285 else:
361 else:
286 self._insert_into_buffer(dedent(text))
362 self._insert_into_buffer(dedent(text))
287
363
288 def print_(self, printer):
364 def print_(self, printer):
289 """ Print the contents of the ConsoleWidget to the specified QPrinter.
365 """ Print the contents of the ConsoleWidget to the specified QPrinter.
290 """
366 """
291 self._control.print_(printer)
367 self._control.print_(printer)
292
368
293 def redo(self):
369 def redo(self):
294 """ Redo the last operation. If there is no operation to redo, nothing
370 """ Redo the last operation. If there is no operation to redo, nothing
295 happens.
371 happens.
296 """
372 """
297 self._control.redo()
373 self._control.redo()
298
374
299 def reset_font(self):
375 def reset_font(self):
300 """ Sets the font to the default fixed-width font for this platform.
376 """ Sets the font to the default fixed-width font for this platform.
301 """
377 """
302 if sys.platform == 'win32':
378 if sys.platform == 'win32':
303 name = 'Courier'
379 name = 'Courier'
304 elif sys.platform == 'darwin':
380 elif sys.platform == 'darwin':
305 name = 'Monaco'
381 name = 'Monaco'
306 else:
382 else:
307 name = 'Monospace'
383 name = 'Monospace'
308 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
384 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
309 font.setStyleHint(QtGui.QFont.TypeWriter)
385 font.setStyleHint(QtGui.QFont.TypeWriter)
310 self._set_font(font)
386 self._set_font(font)
311
387
312 def select_all(self):
388 def select_all(self):
313 """ Selects all the text in the buffer.
389 """ Selects all the text in the buffer.
314 """
390 """
315 self._control.selectAll()
391 self._control.selectAll()
316
392
317 def _get_tab_width(self):
393 def _get_tab_width(self):
318 """ The width (in terms of space characters) for tab characters.
394 """ The width (in terms of space characters) for tab characters.
319 """
395 """
320 return self._tab_width
396 return self._tab_width
321
397
322 def _set_tab_width(self, tab_width):
398 def _set_tab_width(self, tab_width):
323 """ Sets the width (in terms of space characters) for tab characters.
399 """ Sets the width (in terms of space characters) for tab characters.
324 """
400 """
325 font_metrics = QtGui.QFontMetrics(self.font)
401 font_metrics = QtGui.QFontMetrics(self.font)
326 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
402 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
327
403
328 self._tab_width = tab_width
404 self._tab_width = tab_width
329
405
330 tab_width = property(_get_tab_width, _set_tab_width)
406 tab_width = property(_get_tab_width, _set_tab_width)
331
407
332 def undo(self):
408 def undo(self):
333 """ Undo the last operation. If there is no operation to undo, nothing
409 """ Undo the last operation. If there is no operation to undo, nothing
334 happens.
410 happens.
335 """
411 """
336 self._control.undo()
412 self._control.undo()
337
413
338 #---------------------------------------------------------------------------
414 #---------------------------------------------------------------------------
339 # 'ConsoleWidget' abstract interface
415 # 'ConsoleWidget' abstract interface
340 #---------------------------------------------------------------------------
416 #---------------------------------------------------------------------------
341
417
342 def _is_complete(self, source, interactive):
418 def _is_complete(self, source, interactive):
343 """ Returns whether 'source' can be executed. When triggered by an
419 """ Returns whether 'source' can be executed. When triggered by an
344 Enter/Return key press, 'interactive' is True; otherwise, it is
420 Enter/Return key press, 'interactive' is True; otherwise, it is
345 False.
421 False.
346 """
422 """
347 raise NotImplementedError
423 raise NotImplementedError
348
424
349 def _execute(self, source, hidden):
425 def _execute(self, source, hidden):
350 """ Execute 'source'. If 'hidden', do not show any output.
426 """ Execute 'source'. If 'hidden', do not show any output.
351 """
427 """
352 raise NotImplementedError
428 raise NotImplementedError
353
429
354 def _execute_interrupt(self):
430 def _execute_interrupt(self):
355 """ Attempts to stop execution. Returns whether this method has an
431 """ Attempts to stop execution. Returns whether this method has an
356 implementation.
432 implementation.
357 """
433 """
358 return False
434 return False
359
435
360 def _prompt_started_hook(self):
436 def _prompt_started_hook(self):
361 """ Called immediately after a new prompt is displayed.
437 """ Called immediately after a new prompt is displayed.
362 """
438 """
363 pass
439 pass
364
440
365 def _prompt_finished_hook(self):
441 def _prompt_finished_hook(self):
366 """ Called immediately after a prompt is finished, i.e. when some input
442 """ Called immediately after a prompt is finished, i.e. when some input
367 will be processed and a new prompt displayed.
443 will be processed and a new prompt displayed.
368 """
444 """
369 pass
445 pass
370
446
371 def _up_pressed(self):
447 def _up_pressed(self):
372 """ Called when the up key is pressed. Returns whether to continue
448 """ Called when the up key is pressed. Returns whether to continue
373 processing the event.
449 processing the event.
374 """
450 """
375 return True
451 return True
376
452
377 def _down_pressed(self):
453 def _down_pressed(self):
378 """ Called when the down key is pressed. Returns whether to continue
454 """ Called when the down key is pressed. Returns whether to continue
379 processing the event.
455 processing the event.
380 """
456 """
381 return True
457 return True
382
458
383 def _tab_pressed(self):
459 def _tab_pressed(self):
384 """ Called when the tab key is pressed. Returns whether to continue
460 """ Called when the tab key is pressed. Returns whether to continue
385 processing the event.
461 processing the event.
386 """
462 """
387 return False
463 return False
388
464
389 #--------------------------------------------------------------------------
465 #--------------------------------------------------------------------------
390 # 'ConsoleWidget' protected interface
466 # 'ConsoleWidget' protected interface
391 #--------------------------------------------------------------------------
467 #--------------------------------------------------------------------------
392
468
393 def _append_html(self, html):
469 def _append_html(self, html):
394 """ Appends html at the end of the console buffer.
470 """ Appends html at the end of the console buffer.
395 """
471 """
396 cursor = self._get_end_cursor()
472 cursor = self._get_end_cursor()
397 self._insert_html(cursor, html)
473 self._insert_html(cursor, html)
398
474
399 def _append_html_fetching_plain_text(self, html):
475 def _append_html_fetching_plain_text(self, html):
400 """ Appends 'html', then returns the plain text version of it.
476 """ Appends 'html', then returns the plain text version of it.
401 """
477 """
402 anchor = self._get_end_cursor().position()
478 anchor = self._get_end_cursor().position()
403 self._append_html(html)
479 self._append_html(html)
404 cursor = self._get_end_cursor()
480 cursor = self._get_end_cursor()
405 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
481 cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
406 return str(cursor.selection().toPlainText())
482 return str(cursor.selection().toPlainText())
407
483
408 def _append_plain_text(self, text):
484 def _append_plain_text(self, text):
409 """ Appends plain text at the end of the console buffer, processing
485 """ Appends plain text at the end of the console buffer, processing
410 ANSI codes if enabled.
486 ANSI codes if enabled.
411 """
487 """
412 cursor = self._get_end_cursor()
488 cursor = self._get_end_cursor()
413 cursor.beginEditBlock()
489 self._insert_plain_text(cursor, text)
414 if self.ansi_codes:
415 for substring in self._ansi_processor.split_string(text):
416 format = self._ansi_processor.get_format()
417 cursor.insertText(substring, format)
418 else:
419 cursor.insertText(text)
420 cursor.endEditBlock()
421
490
422 def _append_plain_text_keeping_prompt(self, text):
491 def _append_plain_text_keeping_prompt(self, text):
423 """ Writes 'text' after the current prompt, then restores the old prompt
492 """ Writes 'text' after the current prompt, then restores the old prompt
424 with its old input buffer.
493 with its old input buffer.
425 """
494 """
426 input_buffer = self.input_buffer
495 input_buffer = self.input_buffer
427 self._append_plain_text('\n')
496 self._append_plain_text('\n')
428 self._prompt_finished()
497 self._prompt_finished()
429
498
430 self._append_plain_text(text)
499 self._append_plain_text(text)
431 self._show_prompt()
500 self._show_prompt()
432 self.input_buffer = input_buffer
501 self.input_buffer = input_buffer
433
502
434 def _complete_with_items(self, cursor, items):
503 def _complete_with_items(self, cursor, items):
435 """ Performs completion with 'items' at the specified cursor location.
504 """ Performs completion with 'items' at the specified cursor location.
436 """
505 """
437 if len(items) == 1:
506 if len(items) == 1:
438 cursor.setPosition(self._control.textCursor().position(),
507 cursor.setPosition(self._control.textCursor().position(),
439 QtGui.QTextCursor.KeepAnchor)
508 QtGui.QTextCursor.KeepAnchor)
440 cursor.insertText(items[0])
509 cursor.insertText(items[0])
441 elif len(items) > 1:
510 elif len(items) > 1:
442 if self.gui_completion:
511 if self.gui_completion:
443 self._completion_widget.show_items(cursor, items)
512 self._completion_widget.show_items(cursor, items)
444 else:
513 else:
445 text = self._format_as_columns(items)
514 text = self._format_as_columns(items)
446 self._append_plain_text_keeping_prompt(text)
515 self._append_plain_text_keeping_prompt(text)
447
516
448 def _control_key_down(self, modifiers):
517 def _control_key_down(self, modifiers):
449 """ Given a KeyboardModifiers flags object, return whether the Control
518 """ Given a KeyboardModifiers flags object, return whether the Control
450 key is down (on Mac OS, treat the Command key as a synonym for
519 key is down (on Mac OS, treat the Command key as a synonym for
451 Control).
520 Control).
452 """
521 """
453 down = bool(modifiers & QtCore.Qt.ControlModifier)
522 down = bool(modifiers & QtCore.Qt.ControlModifier)
454
523
455 # Note: on Mac OS, ControlModifier corresponds to the Command key while
524 # Note: on Mac OS, ControlModifier corresponds to the Command key while
456 # MetaModifier corresponds to the Control key.
525 # MetaModifier corresponds to the Control key.
457 if sys.platform == 'darwin':
526 if sys.platform == 'darwin':
458 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
527 down = down ^ bool(modifiers & QtCore.Qt.MetaModifier)
459
528
460 return down
529 return down
461
530
462 def _create_control(self, kind):
531 def _create_control(self, kind):
463 """ Creates and connects the underlying text widget.
532 """ Creates and connects the underlying text widget.
464 """
533 """
465 if kind == 'plain':
534 if kind == 'plain':
466 control = QtGui.QPlainTextEdit()
535 control = QtGui.QPlainTextEdit()
467 elif kind == 'rich':
536 elif kind == 'rich':
468 control = QtGui.QTextEdit()
537 control = QtGui.QTextEdit()
469 control.setAcceptRichText(False)
538 control.setAcceptRichText(False)
470 else:
539 else:
471 raise ValueError("Kind %s unknown." % repr(kind))
540 raise ValueError("Kind %s unknown." % repr(kind))
472 control.installEventFilter(self)
541 control.installEventFilter(self)
473 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
542 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
474 control.customContextMenuRequested.connect(self._show_context_menu)
543 control.customContextMenuRequested.connect(self._show_context_menu)
475 control.copyAvailable.connect(self.copy_available)
544 control.copyAvailable.connect(self.copy_available)
476 control.redoAvailable.connect(self.redo_available)
545 control.redoAvailable.connect(self.redo_available)
477 control.undoAvailable.connect(self.undo_available)
546 control.undoAvailable.connect(self.undo_available)
478 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
547 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
479 return control
548 return control
480
549
481 def _event_filter_keypress(self, event):
550 def _create_page_control(self):
551 """ Creates and connects the underlying paging widget.
552 """
553 control = QtGui.QPlainTextEdit()
554 control.installEventFilter(self)
555 control.setReadOnly(True)
556 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
557 return control
558
559 def _event_filter_console_keypress(self, event):
482 """ Filter key events for the underlying text widget to create a
560 """ Filter key events for the underlying text widget to create a
483 console-like interface.
561 console-like interface.
484 """
562 """
485 intercepted = False
563 intercepted = False
486 cursor = self._control.textCursor()
564 cursor = self._control.textCursor()
487 position = cursor.position()
565 position = cursor.position()
488 key = event.key()
566 key = event.key()
489 ctrl_down = self._control_key_down(event.modifiers())
567 ctrl_down = self._control_key_down(event.modifiers())
490 alt_down = event.modifiers() & QtCore.Qt.AltModifier
568 alt_down = event.modifiers() & QtCore.Qt.AltModifier
491 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
569 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
492
570
493 if event.matches(QtGui.QKeySequence.Paste):
571 if event.matches(QtGui.QKeySequence.Paste):
494 # Call our paste instead of the underlying text widget's.
572 # Call our paste instead of the underlying text widget's.
495 self.paste()
573 self.paste()
496 intercepted = True
574 intercepted = True
497
575
498 elif ctrl_down:
576 elif ctrl_down:
499 if key == QtCore.Qt.Key_C:
577 if key == QtCore.Qt.Key_C:
500 intercepted = self._executing and self._execute_interrupt()
578 intercepted = self._executing and self._execute_interrupt()
501
579
502 elif key == QtCore.Qt.Key_K:
580 elif key == QtCore.Qt.Key_K:
503 if self._in_buffer(position):
581 if self._in_buffer(position):
504 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
582 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
505 QtGui.QTextCursor.KeepAnchor)
583 QtGui.QTextCursor.KeepAnchor)
506 cursor.removeSelectedText()
584 cursor.removeSelectedText()
507 intercepted = True
585 intercepted = True
508
586
509 elif key == QtCore.Qt.Key_X:
587 elif key == QtCore.Qt.Key_X:
510 intercepted = True
588 intercepted = True
511
589
512 elif key == QtCore.Qt.Key_Y:
590 elif key == QtCore.Qt.Key_Y:
513 self.paste()
591 self.paste()
514 intercepted = True
592 intercepted = True
515
593
516 elif alt_down:
594 elif alt_down:
517 if key == QtCore.Qt.Key_B:
595 if key == QtCore.Qt.Key_B:
518 self._set_cursor(self._get_word_start_cursor(position))
596 self._set_cursor(self._get_word_start_cursor(position))
519 intercepted = True
597 intercepted = True
520
598
521 elif key == QtCore.Qt.Key_F:
599 elif key == QtCore.Qt.Key_F:
522 self._set_cursor(self._get_word_end_cursor(position))
600 self._set_cursor(self._get_word_end_cursor(position))
523 intercepted = True
601 intercepted = True
524
602
525 elif key == QtCore.Qt.Key_Backspace:
603 elif key == QtCore.Qt.Key_Backspace:
526 cursor = self._get_word_start_cursor(position)
604 cursor = self._get_word_start_cursor(position)
527 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
605 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
528 cursor.removeSelectedText()
606 cursor.removeSelectedText()
529 intercepted = True
607 intercepted = True
530
608
531 elif key == QtCore.Qt.Key_D:
609 elif key == QtCore.Qt.Key_D:
532 cursor = self._get_word_end_cursor(position)
610 cursor = self._get_word_end_cursor(position)
533 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
611 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
534 cursor.removeSelectedText()
612 cursor.removeSelectedText()
535 intercepted = True
613 intercepted = True
536
614
537 else:
615 else:
538 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
616 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
539 if self._reading:
617 if self._reading:
540 self._append_plain_text('\n')
618 self._append_plain_text('\n')
541 self._reading = False
619 self._reading = False
542 if self._reading_callback:
620 if self._reading_callback:
543 self._reading_callback()
621 self._reading_callback()
544 elif not self._executing:
622 elif not self._executing:
545 self.execute(interactive=True)
623 self.execute(interactive=True)
546 intercepted = True
624 intercepted = True
547
625
548 elif key == QtCore.Qt.Key_Up:
626 elif key == QtCore.Qt.Key_Up:
549 if self._reading or not self._up_pressed():
627 if self._reading or not self._up_pressed():
550 intercepted = True
628 intercepted = True
551 else:
629 else:
552 prompt_line = self._get_prompt_cursor().blockNumber()
630 prompt_line = self._get_prompt_cursor().blockNumber()
553 intercepted = cursor.blockNumber() <= prompt_line
631 intercepted = cursor.blockNumber() <= prompt_line
554
632
555 elif key == QtCore.Qt.Key_Down:
633 elif key == QtCore.Qt.Key_Down:
556 if self._reading or not self._down_pressed():
634 if self._reading or not self._down_pressed():
557 intercepted = True
635 intercepted = True
558 else:
636 else:
559 end_line = self._get_end_cursor().blockNumber()
637 end_line = self._get_end_cursor().blockNumber()
560 intercepted = cursor.blockNumber() == end_line
638 intercepted = cursor.blockNumber() == end_line
561
639
562 elif key == QtCore.Qt.Key_Tab:
640 elif key == QtCore.Qt.Key_Tab:
563 if self._reading:
641 if self._reading:
564 intercepted = False
642 intercepted = False
565 else:
643 else:
566 intercepted = not self._tab_pressed()
644 intercepted = not self._tab_pressed()
567
645
568 elif key == QtCore.Qt.Key_Left:
646 elif key == QtCore.Qt.Key_Left:
569 intercepted = not self._in_buffer(position - 1)
647 intercepted = not self._in_buffer(position - 1)
570
648
571 elif key == QtCore.Qt.Key_Home:
649 elif key == QtCore.Qt.Key_Home:
572 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
650 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
573 start_line = cursor.blockNumber()
651 start_line = cursor.blockNumber()
574 if start_line == self._get_prompt_cursor().blockNumber():
652 if start_line == self._get_prompt_cursor().blockNumber():
575 start_pos = self._prompt_pos
653 start_pos = self._prompt_pos
576 else:
654 else:
577 start_pos = cursor.position()
655 start_pos = cursor.position()
578 start_pos += len(self._continuation_prompt)
656 start_pos += len(self._continuation_prompt)
579 if shift_down and self._in_buffer(position):
657 if shift_down and self._in_buffer(position):
580 self._set_selection(position, start_pos)
658 self._set_selection(position, start_pos)
581 else:
659 else:
582 self._set_position(start_pos)
660 self._set_position(start_pos)
583 intercepted = True
661 intercepted = True
584
662
585 elif key == QtCore.Qt.Key_Backspace:
663 elif key == QtCore.Qt.Key_Backspace:
586
664
587 # Line deletion (remove continuation prompt)
665 # Line deletion (remove continuation prompt)
588 len_prompt = len(self._continuation_prompt)
666 len_prompt = len(self._continuation_prompt)
589 if not self._reading and \
667 if not self._reading and \
590 cursor.columnNumber() == len_prompt and \
668 cursor.columnNumber() == len_prompt and \
591 position != self._prompt_pos:
669 position != self._prompt_pos:
592 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
670 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
593 QtGui.QTextCursor.KeepAnchor)
671 QtGui.QTextCursor.KeepAnchor)
594 cursor.removeSelectedText()
672 cursor.removeSelectedText()
595
673
596 # Regular backwards deletion
674 # Regular backwards deletion
597 else:
675 else:
598 anchor = cursor.anchor()
676 anchor = cursor.anchor()
599 if anchor == position:
677 if anchor == position:
600 intercepted = not self._in_buffer(position - 1)
678 intercepted = not self._in_buffer(position - 1)
601 else:
679 else:
602 intercepted = not self._in_buffer(min(anchor, position))
680 intercepted = not self._in_buffer(min(anchor, position))
603
681
604 elif key == QtCore.Qt.Key_Delete:
682 elif key == QtCore.Qt.Key_Delete:
605 anchor = cursor.anchor()
683 anchor = cursor.anchor()
606 intercepted = not self._in_buffer(min(anchor, position))
684 intercepted = not self._in_buffer(min(anchor, position))
607
685
608 # Don't move the cursor if control is down to allow copy-paste using
686 # Don't move the cursor if control is down to allow copy-paste using
609 # the keyboard in any part of the buffer.
687 # the keyboard in any part of the buffer.
610 if not ctrl_down:
688 if not ctrl_down:
611 self._keep_cursor_in_buffer()
689 self._keep_cursor_in_buffer()
612
690
613 return intercepted
691 return intercepted
614
692
693 def _event_filter_page_keypress(self, event):
694 """ Filter key events for the paging widget to create console-like
695 interface.
696 """
697 key = event.key()
698
699 if key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
700 if self._splitter:
701 self._page_control.hide()
702 else:
703 self.layout().setCurrentWidget(self._control)
704 return True
705
706 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
707 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
708 QtCore.Qt.Key_Down,
709 QtCore.Qt.NoModifier)
710 QtGui.qApp.sendEvent(self._page_control, new_event)
711 return True
712
713 return False
714
615 def _format_as_columns(self, items, separator=' '):
715 def _format_as_columns(self, items, separator=' '):
616 """ Transform a list of strings into a single string with columns.
716 """ Transform a list of strings into a single string with columns.
617
717
618 Parameters
718 Parameters
619 ----------
719 ----------
620 items : sequence of strings
720 items : sequence of strings
621 The strings to process.
721 The strings to process.
622
722
623 separator : str, optional [default is two spaces]
723 separator : str, optional [default is two spaces]
624 The string that separates columns.
724 The string that separates columns.
625
725
626 Returns
726 Returns
627 -------
727 -------
628 The formatted string.
728 The formatted string.
629 """
729 """
630 # Note: this code is adapted from columnize 0.3.2.
730 # Note: this code is adapted from columnize 0.3.2.
631 # See http://code.google.com/p/pycolumnize/
731 # See http://code.google.com/p/pycolumnize/
632
732
633 width = self._control.viewport().width()
733 width = self._control.viewport().width()
634 char_width = QtGui.QFontMetrics(self.font).width(' ')
734 char_width = QtGui.QFontMetrics(self.font).width(' ')
635 displaywidth = max(5, width / char_width)
735 displaywidth = max(5, width / char_width)
636
736
637 # Some degenerate cases.
737 # Some degenerate cases.
638 size = len(items)
738 size = len(items)
639 if size == 0:
739 if size == 0:
640 return '\n'
740 return '\n'
641 elif size == 1:
741 elif size == 1:
642 return '%s\n' % str(items[0])
742 return '%s\n' % str(items[0])
643
743
644 # Try every row count from 1 upwards
744 # Try every row count from 1 upwards
645 array_index = lambda nrows, row, col: nrows*col + row
745 array_index = lambda nrows, row, col: nrows*col + row
646 for nrows in range(1, size):
746 for nrows in range(1, size):
647 ncols = (size + nrows - 1) // nrows
747 ncols = (size + nrows - 1) // nrows
648 colwidths = []
748 colwidths = []
649 totwidth = -len(separator)
749 totwidth = -len(separator)
650 for col in range(ncols):
750 for col in range(ncols):
651 # Get max column width for this column
751 # Get max column width for this column
652 colwidth = 0
752 colwidth = 0
653 for row in range(nrows):
753 for row in range(nrows):
654 i = array_index(nrows, row, col)
754 i = array_index(nrows, row, col)
655 if i >= size: break
755 if i >= size: break
656 x = items[i]
756 x = items[i]
657 colwidth = max(colwidth, len(x))
757 colwidth = max(colwidth, len(x))
658 colwidths.append(colwidth)
758 colwidths.append(colwidth)
659 totwidth += colwidth + len(separator)
759 totwidth += colwidth + len(separator)
660 if totwidth > displaywidth:
760 if totwidth > displaywidth:
661 break
761 break
662 if totwidth <= displaywidth:
762 if totwidth <= displaywidth:
663 break
763 break
664
764
665 # The smallest number of rows computed and the max widths for each
765 # The smallest number of rows computed and the max widths for each
666 # column has been obtained. Now we just have to format each of the rows.
766 # column has been obtained. Now we just have to format each of the rows.
667 string = ''
767 string = ''
668 for row in range(nrows):
768 for row in range(nrows):
669 texts = []
769 texts = []
670 for col in range(ncols):
770 for col in range(ncols):
671 i = row + nrows*col
771 i = row + nrows*col
672 if i >= size:
772 if i >= size:
673 texts.append('')
773 texts.append('')
674 else:
774 else:
675 texts.append(items[i])
775 texts.append(items[i])
676 while texts and not texts[-1]:
776 while texts and not texts[-1]:
677 del texts[-1]
777 del texts[-1]
678 for col in range(len(texts)):
778 for col in range(len(texts)):
679 texts[col] = texts[col].ljust(colwidths[col])
779 texts[col] = texts[col].ljust(colwidths[col])
680 string += '%s\n' % str(separator.join(texts))
780 string += '%s\n' % str(separator.join(texts))
681 return string
781 return string
682
782
683 def _get_block_plain_text(self, block):
783 def _get_block_plain_text(self, block):
684 """ Given a QTextBlock, return its unformatted text.
784 """ Given a QTextBlock, return its unformatted text.
685 """
785 """
686 cursor = QtGui.QTextCursor(block)
786 cursor = QtGui.QTextCursor(block)
687 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
787 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
688 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
788 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
689 QtGui.QTextCursor.KeepAnchor)
789 QtGui.QTextCursor.KeepAnchor)
690 return str(cursor.selection().toPlainText())
790 return str(cursor.selection().toPlainText())
691
791
692 def _get_cursor(self):
792 def _get_cursor(self):
693 """ Convenience method that returns a cursor for the current position.
793 """ Convenience method that returns a cursor for the current position.
694 """
794 """
695 return self._control.textCursor()
795 return self._control.textCursor()
696
796
697 def _get_end_cursor(self):
797 def _get_end_cursor(self):
698 """ Convenience method that returns a cursor for the last character.
798 """ Convenience method that returns a cursor for the last character.
699 """
799 """
700 cursor = self._control.textCursor()
800 cursor = self._control.textCursor()
701 cursor.movePosition(QtGui.QTextCursor.End)
801 cursor.movePosition(QtGui.QTextCursor.End)
702 return cursor
802 return cursor
703
803
704 def _get_input_buffer_cursor_line(self):
804 def _get_input_buffer_cursor_line(self):
705 """ The text in the line of the input buffer in which the user's cursor
805 """ The text in the line of the input buffer in which the user's cursor
706 rests. Returns a string if there is such a line; otherwise, None.
806 rests. Returns a string if there is such a line; otherwise, None.
707 """
807 """
708 if self._executing:
808 if self._executing:
709 return None
809 return None
710 cursor = self._control.textCursor()
810 cursor = self._control.textCursor()
711 if cursor.position() >= self._prompt_pos:
811 if cursor.position() >= self._prompt_pos:
712 text = self._get_block_plain_text(cursor.block())
812 text = self._get_block_plain_text(cursor.block())
713 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
813 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
714 return text[len(self._prompt):]
814 return text[len(self._prompt):]
715 else:
815 else:
716 return text[len(self._continuation_prompt):]
816 return text[len(self._continuation_prompt):]
717 else:
817 else:
718 return None
818 return None
719
819
720 def _get_prompt_cursor(self):
820 def _get_prompt_cursor(self):
721 """ Convenience method that returns a cursor for the prompt position.
821 """ Convenience method that returns a cursor for the prompt position.
722 """
822 """
723 cursor = self._control.textCursor()
823 cursor = self._control.textCursor()
724 cursor.setPosition(self._prompt_pos)
824 cursor.setPosition(self._prompt_pos)
725 return cursor
825 return cursor
726
826
727 def _get_selection_cursor(self, start, end):
827 def _get_selection_cursor(self, start, end):
728 """ Convenience method that returns a cursor with text selected between
828 """ Convenience method that returns a cursor with text selected between
729 the positions 'start' and 'end'.
829 the positions 'start' and 'end'.
730 """
830 """
731 cursor = self._control.textCursor()
831 cursor = self._control.textCursor()
732 cursor.setPosition(start)
832 cursor.setPosition(start)
733 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
833 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
734 return cursor
834 return cursor
735
835
736 def _get_word_start_cursor(self, position):
836 def _get_word_start_cursor(self, position):
737 """ Find the start of the word to the left the given position. If a
837 """ Find the start of the word to the left the given position. If a
738 sequence of non-word characters precedes the first word, skip over
838 sequence of non-word characters precedes the first word, skip over
739 them. (This emulates the behavior of bash, emacs, etc.)
839 them. (This emulates the behavior of bash, emacs, etc.)
740 """
840 """
741 document = self._control.document()
841 document = self._control.document()
742 position -= 1
842 position -= 1
743 while position >= self._prompt_pos and \
843 while position >= self._prompt_pos and \
744 not document.characterAt(position).isLetterOrNumber():
844 not document.characterAt(position).isLetterOrNumber():
745 position -= 1
845 position -= 1
746 while position >= self._prompt_pos and \
846 while position >= self._prompt_pos and \
747 document.characterAt(position).isLetterOrNumber():
847 document.characterAt(position).isLetterOrNumber():
748 position -= 1
848 position -= 1
749 cursor = self._control.textCursor()
849 cursor = self._control.textCursor()
750 cursor.setPosition(position + 1)
850 cursor.setPosition(position + 1)
751 return cursor
851 return cursor
752
852
753 def _get_word_end_cursor(self, position):
853 def _get_word_end_cursor(self, position):
754 """ Find the end of the word to the right the given position. If a
854 """ Find the end of the word to the right the given position. If a
755 sequence of non-word characters precedes the first word, skip over
855 sequence of non-word characters precedes the first word, skip over
756 them. (This emulates the behavior of bash, emacs, etc.)
856 them. (This emulates the behavior of bash, emacs, etc.)
757 """
857 """
758 document = self._control.document()
858 document = self._control.document()
759 end = self._get_end_cursor().position()
859 end = self._get_end_cursor().position()
760 while position < end and \
860 while position < end and \
761 not document.characterAt(position).isLetterOrNumber():
861 not document.characterAt(position).isLetterOrNumber():
762 position += 1
862 position += 1
763 while position < end and \
863 while position < end and \
764 document.characterAt(position).isLetterOrNumber():
864 document.characterAt(position).isLetterOrNumber():
765 position += 1
865 position += 1
766 cursor = self._control.textCursor()
866 cursor = self._control.textCursor()
767 cursor.setPosition(position)
867 cursor.setPosition(position)
768 return cursor
868 return cursor
769
869
770 def _insert_html(self, cursor, html):
870 def _insert_html(self, cursor, html):
771 """ Insert HTML using the specified cursor in such a way that future
871 """ Insert HTML using the specified cursor in such a way that future
772 formatting is unaffected.
872 formatting is unaffected.
773 """
873 """
774 cursor.beginEditBlock()
874 cursor.beginEditBlock()
775 cursor.insertHtml(html)
875 cursor.insertHtml(html)
776
876
777 # After inserting HTML, the text document "remembers" it's in "html
877 # After inserting HTML, the text document "remembers" it's in "html
778 # mode", which means that subsequent calls adding plain text will result
878 # mode", which means that subsequent calls adding plain text will result
779 # in unwanted formatting, lost tab characters, etc. The following code
879 # in unwanted formatting, lost tab characters, etc. The following code
780 # hacks around this behavior, which I consider to be a bug in Qt.
880 # hacks around this behavior, which I consider to be a bug in Qt.
781 cursor.movePosition(QtGui.QTextCursor.Left,
881 cursor.movePosition(QtGui.QTextCursor.Left,
782 QtGui.QTextCursor.KeepAnchor)
882 QtGui.QTextCursor.KeepAnchor)
783 if cursor.selection().toPlainText() == ' ':
883 if cursor.selection().toPlainText() == ' ':
784 cursor.removeSelectedText()
884 cursor.removeSelectedText()
785 cursor.movePosition(QtGui.QTextCursor.Right)
885 cursor.movePosition(QtGui.QTextCursor.Right)
786 cursor.insertText(' ', QtGui.QTextCharFormat())
886 cursor.insertText(' ', QtGui.QTextCharFormat())
787 cursor.endEditBlock()
887 cursor.endEditBlock()
788
888
889 def _insert_plain_text(self, cursor, text):
890 """ Inserts plain text using the specified cursor, processing ANSI codes
891 if enabled.
892 """
893 cursor.beginEditBlock()
894 if self.ansi_codes:
895 for substring in self._ansi_processor.split_string(text):
896 format = self._ansi_processor.get_format()
897 cursor.insertText(substring, format)
898 else:
899 cursor.insertText(text)
900 cursor.endEditBlock()
901
789 def _insert_into_buffer(self, text):
902 def _insert_into_buffer(self, text):
790 """ Inserts text into the input buffer at the current cursor position,
903 """ Inserts text into the input buffer at the current cursor position,
791 ensuring that continuation prompts are inserted as necessary.
904 ensuring that continuation prompts are inserted as necessary.
792 """
905 """
793 lines = str(text).splitlines(True)
906 lines = str(text).splitlines(True)
794 if lines:
907 if lines:
795 self._keep_cursor_in_buffer()
908 self._keep_cursor_in_buffer()
796 cursor = self._control.textCursor()
909 cursor = self._control.textCursor()
797 cursor.beginEditBlock()
910 cursor.beginEditBlock()
798 cursor.insertText(lines[0])
911 cursor.insertText(lines[0])
799 for line in lines[1:]:
912 for line in lines[1:]:
800 if self._continuation_prompt_html is None:
913 if self._continuation_prompt_html is None:
801 cursor.insertText(self._continuation_prompt)
914 cursor.insertText(self._continuation_prompt)
802 else:
915 else:
803 self._insert_html(cursor, self._continuation_prompt_html)
916 self._insert_html(cursor, self._continuation_prompt_html)
804 cursor.insertText(line)
917 cursor.insertText(line)
805 cursor.endEditBlock()
918 cursor.endEditBlock()
806 self._control.setTextCursor(cursor)
919 self._control.setTextCursor(cursor)
807
920
808 def _in_buffer(self, position):
921 def _in_buffer(self, position):
809 """ Returns whether the given position is inside the editing region.
922 """ Returns whether the given position is inside the editing region.
810 """
923 """
811 cursor = self._control.textCursor()
924 cursor = self._control.textCursor()
812 cursor.setPosition(position)
925 cursor.setPosition(position)
813 line = cursor.blockNumber()
926 line = cursor.blockNumber()
814 prompt_line = self._get_prompt_cursor().blockNumber()
927 prompt_line = self._get_prompt_cursor().blockNumber()
815 if line == prompt_line:
928 if line == prompt_line:
816 return position >= self._prompt_pos
929 return position >= self._prompt_pos
817 elif line > prompt_line:
930 elif line > prompt_line:
818 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
931 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
819 prompt_pos = cursor.position() + len(self._continuation_prompt)
932 prompt_pos = cursor.position() + len(self._continuation_prompt)
820 return position >= prompt_pos
933 return position >= prompt_pos
821 return False
934 return False
822
935
823 def _keep_cursor_in_buffer(self):
936 def _keep_cursor_in_buffer(self):
824 """ Ensures that the cursor is inside the editing region. Returns
937 """ Ensures that the cursor is inside the editing region. Returns
825 whether the cursor was moved.
938 whether the cursor was moved.
826 """
939 """
827 cursor = self._control.textCursor()
940 cursor = self._control.textCursor()
828 if self._in_buffer(cursor.position()):
941 if self._in_buffer(cursor.position()):
829 return False
942 return False
830 else:
943 else:
831 cursor.movePosition(QtGui.QTextCursor.End)
944 cursor.movePosition(QtGui.QTextCursor.End)
832 self._control.setTextCursor(cursor)
945 self._control.setTextCursor(cursor)
833 return True
946 return True
947
948 def _page(self, text):
949 """ Displays text using the pager.
950 """
951 if self._page_style == 'custom':
952 self.custom_page_requested.emit(text)
953 elif self._page_style == 'none':
954 self._append_plain_text(text)
955 else:
956 self._page_control.clear()
957 cursor = self._page_control.textCursor()
958 self._insert_plain_text(cursor, text)
959 self._page_control.moveCursor(QtGui.QTextCursor.Start)
960
961 self._page_control.viewport().resize(self._control.size())
962 if self._splitter:
963 self._page_control.show()
964 self._page_control.setFocus()
965 else:
966 self.layout().setCurrentWidget(self._page_control)
834
967
835 def _prompt_started(self):
968 def _prompt_started(self):
836 """ Called immediately after a new prompt is displayed.
969 """ Called immediately after a new prompt is displayed.
837 """
970 """
838 # Temporarily disable the maximum block count to permit undo/redo and
971 # Temporarily disable the maximum block count to permit undo/redo and
839 # to ensure that the prompt position does not change due to truncation.
972 # to ensure that the prompt position does not change due to truncation.
840 self._control.document().setMaximumBlockCount(0)
973 self._control.document().setMaximumBlockCount(0)
841 self._control.setUndoRedoEnabled(True)
974 self._control.setUndoRedoEnabled(True)
842
975
843 self._control.setReadOnly(False)
976 self._control.setReadOnly(False)
844 self._control.moveCursor(QtGui.QTextCursor.End)
977 self._control.moveCursor(QtGui.QTextCursor.End)
845
978
846 self._executing = False
979 self._executing = False
847 self._prompt_started_hook()
980 self._prompt_started_hook()
848
981
849 def _prompt_finished(self):
982 def _prompt_finished(self):
850 """ Called immediately after a prompt is finished, i.e. when some input
983 """ Called immediately after a prompt is finished, i.e. when some input
851 will be processed and a new prompt displayed.
984 will be processed and a new prompt displayed.
852 """
985 """
853 self._control.setUndoRedoEnabled(False)
986 self._control.setUndoRedoEnabled(False)
854 self._control.setReadOnly(True)
987 self._control.setReadOnly(True)
855 self._prompt_finished_hook()
988 self._prompt_finished_hook()
856
989
857 def _readline(self, prompt='', callback=None):
990 def _readline(self, prompt='', callback=None):
858 """ Reads one line of input from the user.
991 """ Reads one line of input from the user.
859
992
860 Parameters
993 Parameters
861 ----------
994 ----------
862 prompt : str, optional
995 prompt : str, optional
863 The prompt to print before reading the line.
996 The prompt to print before reading the line.
864
997
865 callback : callable, optional
998 callback : callable, optional
866 A callback to execute with the read line. If not specified, input is
999 A callback to execute with the read line. If not specified, input is
867 read *synchronously* and this method does not return until it has
1000 read *synchronously* and this method does not return until it has
868 been read.
1001 been read.
869
1002
870 Returns
1003 Returns
871 -------
1004 -------
872 If a callback is specified, returns nothing. Otherwise, returns the
1005 If a callback is specified, returns nothing. Otherwise, returns the
873 input string with the trailing newline stripped.
1006 input string with the trailing newline stripped.
874 """
1007 """
875 if self._reading:
1008 if self._reading:
876 raise RuntimeError('Cannot read a line. Widget is already reading.')
1009 raise RuntimeError('Cannot read a line. Widget is already reading.')
877
1010
878 if not callback and not self.isVisible():
1011 if not callback and not self.isVisible():
879 # If the user cannot see the widget, this function cannot return.
1012 # If the user cannot see the widget, this function cannot return.
880 raise RuntimeError('Cannot synchronously read a line if the widget'
1013 raise RuntimeError('Cannot synchronously read a line if the widget'
881 'is not visible!')
1014 'is not visible!')
882
1015
883 self._reading = True
1016 self._reading = True
884 self._show_prompt(prompt, newline=False)
1017 self._show_prompt(prompt, newline=False)
885
1018
886 if callback is None:
1019 if callback is None:
887 self._reading_callback = None
1020 self._reading_callback = None
888 while self._reading:
1021 while self._reading:
889 QtCore.QCoreApplication.processEvents()
1022 QtCore.QCoreApplication.processEvents()
890 return self.input_buffer.rstrip('\n')
1023 return self.input_buffer.rstrip('\n')
891
1024
892 else:
1025 else:
893 self._reading_callback = lambda: \
1026 self._reading_callback = lambda: \
894 callback(self.input_buffer.rstrip('\n'))
1027 callback(self.input_buffer.rstrip('\n'))
895
1028
896 def _reset(self):
1029 def _reset(self):
897 """ Clears the console and resets internal state variables.
1030 """ Clears the console and resets internal state variables.
898 """
1031 """
899 self._control.clear()
1032 self._control.clear()
900 self._executing = self._reading = False
1033 self._executing = self._reading = False
901
1034
902 def _set_continuation_prompt(self, prompt, html=False):
1035 def _set_continuation_prompt(self, prompt, html=False):
903 """ Sets the continuation prompt.
1036 """ Sets the continuation prompt.
904
1037
905 Parameters
1038 Parameters
906 ----------
1039 ----------
907 prompt : str
1040 prompt : str
908 The prompt to show when more input is needed.
1041 The prompt to show when more input is needed.
909
1042
910 html : bool, optional (default False)
1043 html : bool, optional (default False)
911 If set, the prompt will be inserted as formatted HTML. Otherwise,
1044 If set, the prompt will be inserted as formatted HTML. Otherwise,
912 the prompt will be treated as plain text, though ANSI color codes
1045 the prompt will be treated as plain text, though ANSI color codes
913 will be handled.
1046 will be handled.
914 """
1047 """
915 if html:
1048 if html:
916 self._continuation_prompt_html = prompt
1049 self._continuation_prompt_html = prompt
917 else:
1050 else:
918 self._continuation_prompt = prompt
1051 self._continuation_prompt = prompt
919 self._continuation_prompt_html = None
1052 self._continuation_prompt_html = None
920
1053
921 def _set_cursor(self, cursor):
1054 def _set_cursor(self, cursor):
922 """ Convenience method to set the current cursor.
1055 """ Convenience method to set the current cursor.
923 """
1056 """
924 self._control.setTextCursor(cursor)
1057 self._control.setTextCursor(cursor)
925
1058
926 def _set_position(self, position):
1059 def _set_position(self, position):
927 """ Convenience method to set the position of the cursor.
1060 """ Convenience method to set the position of the cursor.
928 """
1061 """
929 cursor = self._control.textCursor()
1062 cursor = self._control.textCursor()
930 cursor.setPosition(position)
1063 cursor.setPosition(position)
931 self._control.setTextCursor(cursor)
1064 self._control.setTextCursor(cursor)
932
1065
933 def _set_selection(self, start, end):
1066 def _set_selection(self, start, end):
934 """ Convenience method to set the current selected text.
1067 """ Convenience method to set the current selected text.
935 """
1068 """
936 self._control.setTextCursor(self._get_selection_cursor(start, end))
1069 self._control.setTextCursor(self._get_selection_cursor(start, end))
937
1070
938 def _show_context_menu(self, pos):
1071 def _show_context_menu(self, pos):
939 """ Shows a context menu at the given QPoint (in widget coordinates).
1072 """ Shows a context menu at the given QPoint (in widget coordinates).
940 """
1073 """
941 menu = QtGui.QMenu()
1074 menu = QtGui.QMenu()
942
1075
943 copy_action = menu.addAction('Copy', self.copy)
1076 copy_action = menu.addAction('Copy', self.copy)
944 copy_action.setEnabled(self._get_cursor().hasSelection())
1077 copy_action.setEnabled(self._get_cursor().hasSelection())
945 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1078 copy_action.setShortcut(QtGui.QKeySequence.Copy)
946
1079
947 paste_action = menu.addAction('Paste', self.paste)
1080 paste_action = menu.addAction('Paste', self.paste)
948 paste_action.setEnabled(self.can_paste())
1081 paste_action.setEnabled(self.can_paste())
949 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1082 paste_action.setShortcut(QtGui.QKeySequence.Paste)
950
1083
951 menu.addSeparator()
1084 menu.addSeparator()
952 menu.addAction('Select All', self.select_all)
1085 menu.addAction('Select All', self.select_all)
953
1086
954 menu.exec_(self._control.mapToGlobal(pos))
1087 menu.exec_(self._control.mapToGlobal(pos))
955
1088
956 def _show_prompt(self, prompt=None, html=False, newline=True):
1089 def _show_prompt(self, prompt=None, html=False, newline=True):
957 """ Writes a new prompt at the end of the buffer.
1090 """ Writes a new prompt at the end of the buffer.
958
1091
959 Parameters
1092 Parameters
960 ----------
1093 ----------
961 prompt : str, optional
1094 prompt : str, optional
962 The prompt to show. If not specified, the previous prompt is used.
1095 The prompt to show. If not specified, the previous prompt is used.
963
1096
964 html : bool, optional (default False)
1097 html : bool, optional (default False)
965 Only relevant when a prompt is specified. If set, the prompt will
1098 Only relevant when a prompt is specified. If set, the prompt will
966 be inserted as formatted HTML. Otherwise, the prompt will be treated
1099 be inserted as formatted HTML. Otherwise, the prompt will be treated
967 as plain text, though ANSI color codes will be handled.
1100 as plain text, though ANSI color codes will be handled.
968
1101
969 newline : bool, optional (default True)
1102 newline : bool, optional (default True)
970 If set, a new line will be written before showing the prompt if
1103 If set, a new line will be written before showing the prompt if
971 there is not already a newline at the end of the buffer.
1104 there is not already a newline at the end of the buffer.
972 """
1105 """
973 # Insert a preliminary newline, if necessary.
1106 # Insert a preliminary newline, if necessary.
974 if newline:
1107 if newline:
975 cursor = self._get_end_cursor()
1108 cursor = self._get_end_cursor()
976 if cursor.position() > 0:
1109 if cursor.position() > 0:
977 cursor.movePosition(QtGui.QTextCursor.Left,
1110 cursor.movePosition(QtGui.QTextCursor.Left,
978 QtGui.QTextCursor.KeepAnchor)
1111 QtGui.QTextCursor.KeepAnchor)
979 if str(cursor.selection().toPlainText()) != '\n':
1112 if str(cursor.selection().toPlainText()) != '\n':
980 self._append_plain_text('\n')
1113 self._append_plain_text('\n')
981
1114
982 # Write the prompt.
1115 # Write the prompt.
983 if prompt is None:
1116 if prompt is None:
984 if self._prompt_html is None:
1117 if self._prompt_html is None:
985 self._append_plain_text(self._prompt)
1118 self._append_plain_text(self._prompt)
986 else:
1119 else:
987 self._append_html(self._prompt_html)
1120 self._append_html(self._prompt_html)
988 else:
1121 else:
989 if html:
1122 if html:
990 self._prompt = self._append_html_fetching_plain_text(prompt)
1123 self._prompt = self._append_html_fetching_plain_text(prompt)
991 self._prompt_html = prompt
1124 self._prompt_html = prompt
992 else:
1125 else:
993 self._append_plain_text(prompt)
1126 self._append_plain_text(prompt)
994 self._prompt = prompt
1127 self._prompt = prompt
995 self._prompt_html = None
1128 self._prompt_html = None
996
1129
997 self._prompt_pos = self._get_end_cursor().position()
1130 self._prompt_pos = self._get_end_cursor().position()
998 self._prompt_started()
1131 self._prompt_started()
999
1132
1000 def _show_continuation_prompt(self):
1133 def _show_continuation_prompt(self):
1001 """ Writes a new continuation prompt at the end of the buffer.
1134 """ Writes a new continuation prompt at the end of the buffer.
1002 """
1135 """
1003 if self._continuation_prompt_html is None:
1136 if self._continuation_prompt_html is None:
1004 self._append_plain_text(self._continuation_prompt)
1137 self._append_plain_text(self._continuation_prompt)
1005 else:
1138 else:
1006 self._continuation_prompt = self._append_html_fetching_plain_text(
1139 self._continuation_prompt = self._append_html_fetching_plain_text(
1007 self._continuation_prompt_html)
1140 self._continuation_prompt_html)
1008
1141
1009 self._prompt_started()
1142 self._prompt_started()
1010
1143
1011
1144
1012 class HistoryConsoleWidget(ConsoleWidget):
1145 class HistoryConsoleWidget(ConsoleWidget):
1013 """ A ConsoleWidget that keeps a history of the commands that have been
1146 """ A ConsoleWidget that keeps a history of the commands that have been
1014 executed.
1147 executed.
1015 """
1148 """
1016
1149
1017 #---------------------------------------------------------------------------
1150 #---------------------------------------------------------------------------
1018 # 'object' interface
1151 # 'object' interface
1019 #---------------------------------------------------------------------------
1152 #---------------------------------------------------------------------------
1020
1153
1021 def __init__(self, *args, **kw):
1154 def __init__(self, *args, **kw):
1022 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1155 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1023 self._history = []
1156 self._history = []
1024 self._history_index = 0
1157 self._history_index = 0
1025
1158
1026 #---------------------------------------------------------------------------
1159 #---------------------------------------------------------------------------
1027 # 'ConsoleWidget' public interface
1160 # 'ConsoleWidget' public interface
1028 #---------------------------------------------------------------------------
1161 #---------------------------------------------------------------------------
1029
1162
1030 def execute(self, source=None, hidden=False, interactive=False):
1163 def execute(self, source=None, hidden=False, interactive=False):
1031 """ Reimplemented to the store history.
1164 """ Reimplemented to the store history.
1032 """
1165 """
1033 if not hidden:
1166 if not hidden:
1034 history = self.input_buffer if source is None else source
1167 history = self.input_buffer if source is None else source
1035
1168
1036 executed = super(HistoryConsoleWidget, self).execute(
1169 executed = super(HistoryConsoleWidget, self).execute(
1037 source, hidden, interactive)
1170 source, hidden, interactive)
1038
1171
1039 if executed and not hidden:
1172 if executed and not hidden:
1040 self._history.append(history.rstrip())
1173 self._history.append(history.rstrip())
1041 self._history_index = len(self._history)
1174 self._history_index = len(self._history)
1042
1175
1043 return executed
1176 return executed
1044
1177
1045 #---------------------------------------------------------------------------
1178 #---------------------------------------------------------------------------
1046 # 'ConsoleWidget' abstract interface
1179 # 'ConsoleWidget' abstract interface
1047 #---------------------------------------------------------------------------
1180 #---------------------------------------------------------------------------
1048
1181
1049 def _up_pressed(self):
1182 def _up_pressed(self):
1050 """ Called when the up key is pressed. Returns whether to continue
1183 """ Called when the up key is pressed. Returns whether to continue
1051 processing the event.
1184 processing the event.
1052 """
1185 """
1053 prompt_cursor = self._get_prompt_cursor()
1186 prompt_cursor = self._get_prompt_cursor()
1054 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1187 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1055 self.history_previous()
1188 self.history_previous()
1056
1189
1057 # Go to the first line of prompt for seemless history scrolling.
1190 # Go to the first line of prompt for seemless history scrolling.
1058 cursor = self._get_prompt_cursor()
1191 cursor = self._get_prompt_cursor()
1059 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1192 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1060 self._set_cursor(cursor)
1193 self._set_cursor(cursor)
1061
1194
1062 return False
1195 return False
1063 return True
1196 return True
1064
1197
1065 def _down_pressed(self):
1198 def _down_pressed(self):
1066 """ Called when the down key is pressed. Returns whether to continue
1199 """ Called when the down key is pressed. Returns whether to continue
1067 processing the event.
1200 processing the event.
1068 """
1201 """
1069 end_cursor = self._get_end_cursor()
1202 end_cursor = self._get_end_cursor()
1070 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1203 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1071 self.history_next()
1204 self.history_next()
1072 return False
1205 return False
1073 return True
1206 return True
1074
1207
1075 #---------------------------------------------------------------------------
1208 #---------------------------------------------------------------------------
1076 # 'HistoryConsoleWidget' interface
1209 # 'HistoryConsoleWidget' interface
1077 #---------------------------------------------------------------------------
1210 #---------------------------------------------------------------------------
1078
1211
1079 def history_previous(self):
1212 def history_previous(self):
1080 """ If possible, set the input buffer to the previous item in the
1213 """ If possible, set the input buffer to the previous item in the
1081 history.
1214 history.
1082 """
1215 """
1083 if self._history_index > 0:
1216 if self._history_index > 0:
1084 self._history_index -= 1
1217 self._history_index -= 1
1085 self.input_buffer = self._history[self._history_index]
1218 self.input_buffer = self._history[self._history_index]
1086
1219
1087 def history_next(self):
1220 def history_next(self):
1088 """ Set the input buffer to the next item in the history, or a blank
1221 """ Set the input buffer to the next item in the history, or a blank
1089 line if there is no subsequent item.
1222 line if there is no subsequent item.
1090 """
1223 """
1091 if self._history_index < len(self._history):
1224 if self._history_index < len(self._history):
1092 self._history_index += 1
1225 self._history_index += 1
1093 if self._history_index < len(self._history):
1226 if self._history_index < len(self._history):
1094 self.input_buffer = self._history[self._history_index]
1227 self.input_buffer = self._history[self._history_index]
1095 else:
1228 else:
1096 self.input_buffer = ''
1229 self.input_buffer = ''
@@ -1,54 +1,53 b''
1 """ A demo of the Qt console-style IPython frontend.
1 """ A demo of the Qt console-style IPython frontend.
2 """
2 """
3
3
4 # Systemm library imports
4 # Systemm library imports
5 from PyQt4 import QtCore, QtGui
5 from PyQt4 import QtCore, QtGui
6
6
7 # Local imports
7 # Local imports
8 from IPython.external.argparse import ArgumentParser
8 from IPython.external.argparse import ArgumentParser
9 from IPython.frontend.qt.kernelmanager import QtKernelManager
9 from IPython.frontend.qt.kernelmanager import QtKernelManager
10 from ipython_widget import IPythonWidget
10 from ipython_widget import IPythonWidget
11 from rich_ipython_widget import RichIPythonWidget
11 from rich_ipython_widget import RichIPythonWidget
12
12
13
13
14 def main():
14 def main():
15 """ Entry point for demo.
15 """ Entry point for demo.
16 """
16 """
17 # Parse command line arguments.
17 # Parse command line arguments.
18 parser = ArgumentParser()
18 parser = ArgumentParser()
19 parser.add_argument('--pylab', action='store_true',
19 parser.add_argument('--pylab', action='store_true',
20 help='start kernel with pylab enabled')
20 help='start kernel with pylab enabled')
21 parser.add_argument('--rich', action='store_true',
21 parser.add_argument('--rich', action='store_true',
22 help='use rich text frontend')
22 help='use rich text frontend')
23 namespace = parser.parse_args()
23 namespace = parser.parse_args()
24
24
25 # Don't let Qt or ZMQ swallow KeyboardInterupts.
25 # Don't let Qt or ZMQ swallow KeyboardInterupts.
26 import signal
26 import signal
27 signal.signal(signal.SIGINT, signal.SIG_DFL)
27 signal.signal(signal.SIGINT, signal.SIG_DFL)
28
28
29 # Create a KernelManager and start a kernel.
29 # Create a KernelManager and start a kernel.
30 kernel_manager = QtKernelManager()
30 kernel_manager = QtKernelManager()
31 if namespace.pylab:
31 if namespace.pylab:
32 if namespace.rich:
32 if namespace.rich:
33 kernel_manager.start_kernel(pylab='payload-svg')
33 kernel_manager.start_kernel(pylab='payload-svg')
34 else:
34 else:
35 kernel_manager.start_kernel(pylab='qt4')
35 kernel_manager.start_kernel(pylab='qt4')
36 else:
36 else:
37 kernel_manager.start_kernel()
37 kernel_manager.start_kernel()
38 kernel_manager.start_channels()
38 kernel_manager.start_channels()
39
39
40 # Launch the application.
40 # Launch the application.
41 app = QtGui.QApplication([])
41 app = QtGui.QApplication([])
42 if namespace.rich:
42 if namespace.rich:
43 widget = RichIPythonWidget()
43 widget = RichIPythonWidget()
44 else:
44 else:
45 widget = IPythonWidget()
45 widget = IPythonWidget()
46 widget.kernel_manager = kernel_manager
46 widget.kernel_manager = kernel_manager
47 widget.setWindowTitle('Python')
47 widget.setWindowTitle('Python')
48 widget.resize(640, 480)
49 widget.show()
48 widget.show()
50 app.exec_()
49 app.exec_()
51
50
52
51
53 if __name__ == '__main__':
52 if __name__ == '__main__':
54 main()
53 main()
@@ -1,119 +1,120 b''
1 # System library imports
1 # System library imports
2 from PyQt4 import QtCore, QtGui
2 from PyQt4 import QtCore, QtGui
3
3
4 # Local imports
4 # Local imports
5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
5 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
6 from ipython_widget import IPythonWidget
6 from ipython_widget import IPythonWidget
7
7
8
8
9 class RichIPythonWidget(IPythonWidget):
9 class RichIPythonWidget(IPythonWidget):
10 """ An IPythonWidget that supports rich text, including lists, images, and
10 """ An IPythonWidget that supports rich text, including lists, images, and
11 tables. Note that raw performance will be reduced compared to the plain
11 tables. Note that raw performance will be reduced compared to the plain
12 text version.
12 text version.
13 """
13 """
14
14
15 # Protected class variables.
15 # Protected class variables.
16 _svg_text_format_property = 1
16 _svg_text_format_property = 1
17
17
18 #---------------------------------------------------------------------------
18 #---------------------------------------------------------------------------
19 # 'QObject' interface
19 # 'object' interface
20 #---------------------------------------------------------------------------
20 #---------------------------------------------------------------------------
21
21
22 def __init__(self, parent=None):
22 def __init__(self, *args, **kw):
23 """ Create a RichIPythonWidget.
23 """ Create a RichIPythonWidget.
24 """
24 """
25 super(RichIPythonWidget, self).__init__(kind='rich', parent=parent)
25 kw['kind'] = 'rich'
26 super(RichIPythonWidget, self).__init__(*args, **kw)
26
27
27 #---------------------------------------------------------------------------
28 #---------------------------------------------------------------------------
28 # 'ConsoleWidget' protected interface
29 # 'ConsoleWidget' protected interface
29 #---------------------------------------------------------------------------
30 #---------------------------------------------------------------------------
30
31
31 def _show_context_menu(self, pos):
32 def _show_context_menu(self, pos):
32 """ Reimplemented to show a custom context menu for images.
33 """ Reimplemented to show a custom context menu for images.
33 """
34 """
34 format = self._control.cursorForPosition(pos).charFormat()
35 format = self._control.cursorForPosition(pos).charFormat()
35 name = format.stringProperty(QtGui.QTextFormat.ImageName)
36 name = format.stringProperty(QtGui.QTextFormat.ImageName)
36 if name.isEmpty():
37 if name.isEmpty():
37 super(RichIPythonWidget, self)._show_context_menu(pos)
38 super(RichIPythonWidget, self)._show_context_menu(pos)
38 else:
39 else:
39 menu = QtGui.QMenu()
40 menu = QtGui.QMenu()
40
41
41 menu.addAction('Copy Image', lambda: self._copy_image(name))
42 menu.addAction('Copy Image', lambda: self._copy_image(name))
42 menu.addAction('Save Image As...', lambda: self._save_image(name))
43 menu.addAction('Save Image As...', lambda: self._save_image(name))
43 menu.addSeparator()
44 menu.addSeparator()
44
45
45 svg = format.stringProperty(self._svg_text_format_property)
46 svg = format.stringProperty(self._svg_text_format_property)
46 if not svg.isEmpty():
47 if not svg.isEmpty():
47 menu.addSeparator()
48 menu.addSeparator()
48 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
49 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
49 menu.addAction('Save SVG As...',
50 menu.addAction('Save SVG As...',
50 lambda: save_svg(svg, self._control))
51 lambda: save_svg(svg, self._control))
51
52
52 menu.exec_(self._control.mapToGlobal(pos))
53 menu.exec_(self._control.mapToGlobal(pos))
53
54
54 #---------------------------------------------------------------------------
55 #---------------------------------------------------------------------------
55 # 'FrontendWidget' protected interface
56 # 'FrontendWidget' protected interface
56 #---------------------------------------------------------------------------
57 #---------------------------------------------------------------------------
57
58
58 def _process_execute_ok(self, msg):
59 def _process_execute_ok(self, msg):
59 """ Reimplemented to handle matplotlib plot payloads.
60 """ Reimplemented to handle matplotlib plot payloads.
60 """
61 """
61 payload = msg['content']['payload']
62 payload = msg['content']['payload']
62 plot_payload = payload.get('plot', None)
63 plot_payload = payload.get('plot', None)
63 if plot_payload and plot_payload['format'] == 'svg':
64 if plot_payload and plot_payload['format'] == 'svg':
64 svg = plot_payload['data']
65 svg = plot_payload['data']
65 try:
66 try:
66 image = svg_to_image(svg)
67 image = svg_to_image(svg)
67 except ValueError:
68 except ValueError:
68 self._append_plain_text('Received invalid plot data.')
69 self._append_plain_text('Received invalid plot data.')
69 else:
70 else:
70 format = self._add_image(image)
71 format = self._add_image(image)
71 format.setProperty(self._svg_text_format_property, svg)
72 format.setProperty(self._svg_text_format_property, svg)
72 cursor = self._get_end_cursor()
73 cursor = self._get_end_cursor()
73 cursor.insertBlock()
74 cursor.insertBlock()
74 cursor.insertImage(format)
75 cursor.insertImage(format)
75 cursor.insertBlock()
76 cursor.insertBlock()
76 else:
77 else:
77 super(RichIPythonWidget, self)._process_execute_ok(msg)
78 super(RichIPythonWidget, self)._process_execute_ok(msg)
78
79
79 #---------------------------------------------------------------------------
80 #---------------------------------------------------------------------------
80 # 'RichIPythonWidget' protected interface
81 # 'RichIPythonWidget' protected interface
81 #---------------------------------------------------------------------------
82 #---------------------------------------------------------------------------
82
83
83 def _add_image(self, image):
84 def _add_image(self, image):
84 """ Adds the specified QImage to the document and returns a
85 """ Adds the specified QImage to the document and returns a
85 QTextImageFormat that references it.
86 QTextImageFormat that references it.
86 """
87 """
87 document = self._control.document()
88 document = self._control.document()
88 name = QtCore.QString.number(image.cacheKey())
89 name = QtCore.QString.number(image.cacheKey())
89 document.addResource(QtGui.QTextDocument.ImageResource,
90 document.addResource(QtGui.QTextDocument.ImageResource,
90 QtCore.QUrl(name), image)
91 QtCore.QUrl(name), image)
91 format = QtGui.QTextImageFormat()
92 format = QtGui.QTextImageFormat()
92 format.setName(name)
93 format.setName(name)
93 return format
94 return format
94
95
95 def _copy_image(self, name):
96 def _copy_image(self, name):
96 """ Copies the ImageResource with 'name' to the clipboard.
97 """ Copies the ImageResource with 'name' to the clipboard.
97 """
98 """
98 image = self._get_image(name)
99 image = self._get_image(name)
99 QtGui.QApplication.clipboard().setImage(image)
100 QtGui.QApplication.clipboard().setImage(image)
100
101
101 def _get_image(self, name):
102 def _get_image(self, name):
102 """ Returns the QImage stored as the ImageResource with 'name'.
103 """ Returns the QImage stored as the ImageResource with 'name'.
103 """
104 """
104 document = self._control.document()
105 document = self._control.document()
105 variant = document.resource(QtGui.QTextDocument.ImageResource,
106 variant = document.resource(QtGui.QTextDocument.ImageResource,
106 QtCore.QUrl(name))
107 QtCore.QUrl(name))
107 return variant.toPyObject()
108 return variant.toPyObject()
108
109
109 def _save_image(self, name, format='PNG'):
110 def _save_image(self, name, format='PNG'):
110 """ Shows a save dialog for the ImageResource with 'name'.
111 """ Shows a save dialog for the ImageResource with 'name'.
111 """
112 """
112 dialog = QtGui.QFileDialog(self._control, 'Save Image')
113 dialog = QtGui.QFileDialog(self._control, 'Save Image')
113 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
114 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
114 dialog.setDefaultSuffix(format.lower())
115 dialog.setDefaultSuffix(format.lower())
115 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
116 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
116 if dialog.exec_():
117 if dialog.exec_():
117 filename = dialog.selectedFiles()[0]
118 filename = dialog.selectedFiles()[0]
118 image = self._get_image(name)
119 image = self._get_image(name)
119 image.save(filename, format)
120 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now