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