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