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