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