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