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