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