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