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