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