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