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