##// END OF EJS Templates
Fix all imports for Qt console.
Fernando Perez -
Show More
@@ -1,2009 +1,2009 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.qt.rich_text import HtmlExporter
22 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.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 width = Integer(81, config=True,
158 width = Integer(81, config=True,
159 help="""The width of the console at start time in number
159 help="""The width of the console at start time in number
160 of characters (will double with `hsplit` paging)
160 of characters (will double with `hsplit` paging)
161 """)
161 """)
162
162
163 height = Integer(25, config=True,
163 height = Integer(25, config=True,
164 help="""The height of the console at start time in number
164 help="""The height of the console at start time in number
165 of characters (will double with `vsplit` paging)
165 of characters (will double with `vsplit` paging)
166 """)
166 """)
167
167
168 # Whether to override ShortcutEvents for the keybindings defined by this
168 # Whether to override ShortcutEvents for the keybindings defined by this
169 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
169 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
170 # priority (when it has focus) over, e.g., window-level menu shortcuts.
170 # priority (when it has focus) over, e.g., window-level menu shortcuts.
171 override_shortcuts = Bool(False)
171 override_shortcuts = Bool(False)
172
172
173 # ------ Custom Qt Widgets -------------------------------------------------
173 # ------ Custom Qt Widgets -------------------------------------------------
174
174
175 # For other projects to easily override the Qt widgets used by the console
175 # For other projects to easily override the Qt widgets used by the console
176 # (e.g. Spyder)
176 # (e.g. Spyder)
177 custom_control = None
177 custom_control = None
178 custom_page_control = None
178 custom_page_control = None
179
179
180 #------ Signals ------------------------------------------------------------
180 #------ Signals ------------------------------------------------------------
181
181
182 # Signals that indicate ConsoleWidget state.
182 # Signals that indicate ConsoleWidget state.
183 copy_available = QtCore.Signal(bool)
183 copy_available = QtCore.Signal(bool)
184 redo_available = QtCore.Signal(bool)
184 redo_available = QtCore.Signal(bool)
185 undo_available = QtCore.Signal(bool)
185 undo_available = QtCore.Signal(bool)
186
186
187 # Signal emitted when paging is needed and the paging style has been
187 # Signal emitted when paging is needed and the paging style has been
188 # specified as 'custom'.
188 # specified as 'custom'.
189 custom_page_requested = QtCore.Signal(object)
189 custom_page_requested = QtCore.Signal(object)
190
190
191 # Signal emitted when the font is changed.
191 # Signal emitted when the font is changed.
192 font_changed = QtCore.Signal(QtGui.QFont)
192 font_changed = QtCore.Signal(QtGui.QFont)
193
193
194 #------ Protected class variables ------------------------------------------
194 #------ Protected class variables ------------------------------------------
195
195
196 # control handles
196 # control handles
197 _control = None
197 _control = None
198 _page_control = None
198 _page_control = None
199 _splitter = None
199 _splitter = None
200
200
201 # When the control key is down, these keys are mapped.
201 # When the control key is down, these keys are mapped.
202 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
202 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
203 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
203 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
204 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
204 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
205 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
205 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
206 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
206 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
207 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
207 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
208 if not sys.platform == 'darwin':
208 if not sys.platform == 'darwin':
209 # On OS X, Ctrl-E already does the right thing, whereas End moves the
209 # On OS X, Ctrl-E already does the right thing, whereas End moves the
210 # cursor to the bottom of the buffer.
210 # cursor to the bottom of the buffer.
211 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
211 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
212
212
213 # The shortcuts defined by this widget. We need to keep track of these to
213 # The shortcuts defined by this widget. We need to keep track of these to
214 # support 'override_shortcuts' above.
214 # support 'override_shortcuts' above.
215 _shortcuts = set(_ctrl_down_remap.keys() +
215 _shortcuts = set(_ctrl_down_remap.keys() +
216 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
216 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
217 QtCore.Qt.Key_V ])
217 QtCore.Qt.Key_V ])
218
218
219 _temp_buffer_filled = False
219 _temp_buffer_filled = False
220
220
221 #---------------------------------------------------------------------------
221 #---------------------------------------------------------------------------
222 # 'QObject' interface
222 # 'QObject' interface
223 #---------------------------------------------------------------------------
223 #---------------------------------------------------------------------------
224
224
225 def __init__(self, parent=None, **kw):
225 def __init__(self, parent=None, **kw):
226 """ Create a ConsoleWidget.
226 """ Create a ConsoleWidget.
227
227
228 Parameters:
228 Parameters:
229 -----------
229 -----------
230 parent : QWidget, optional [default None]
230 parent : QWidget, optional [default None]
231 The parent for this widget.
231 The parent for this widget.
232 """
232 """
233 QtGui.QWidget.__init__(self, parent)
233 QtGui.QWidget.__init__(self, parent)
234 LoggingConfigurable.__init__(self, **kw)
234 LoggingConfigurable.__init__(self, **kw)
235
235
236 # While scrolling the pager on Mac OS X, it tears badly. The
236 # While scrolling the pager on Mac OS X, it tears badly. The
237 # NativeGesture is platform and perhaps build-specific hence
237 # NativeGesture is platform and perhaps build-specific hence
238 # we take adequate precautions here.
238 # we take adequate precautions here.
239 self._pager_scroll_events = [QtCore.QEvent.Wheel]
239 self._pager_scroll_events = [QtCore.QEvent.Wheel]
240 if hasattr(QtCore.QEvent, 'NativeGesture'):
240 if hasattr(QtCore.QEvent, 'NativeGesture'):
241 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
241 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
242
242
243 # Create the layout and underlying text widget.
243 # Create the layout and underlying text widget.
244 layout = QtGui.QStackedLayout(self)
244 layout = QtGui.QStackedLayout(self)
245 layout.setContentsMargins(0, 0, 0, 0)
245 layout.setContentsMargins(0, 0, 0, 0)
246 self._control = self._create_control()
246 self._control = self._create_control()
247 if self.paging in ('hsplit', 'vsplit'):
247 if self.paging in ('hsplit', 'vsplit'):
248 self._splitter = QtGui.QSplitter()
248 self._splitter = QtGui.QSplitter()
249 if self.paging == 'hsplit':
249 if self.paging == 'hsplit':
250 self._splitter.setOrientation(QtCore.Qt.Horizontal)
250 self._splitter.setOrientation(QtCore.Qt.Horizontal)
251 else:
251 else:
252 self._splitter.setOrientation(QtCore.Qt.Vertical)
252 self._splitter.setOrientation(QtCore.Qt.Vertical)
253 self._splitter.addWidget(self._control)
253 self._splitter.addWidget(self._control)
254 layout.addWidget(self._splitter)
254 layout.addWidget(self._splitter)
255 else:
255 else:
256 layout.addWidget(self._control)
256 layout.addWidget(self._control)
257
257
258 # Create the paging widget, if necessary.
258 # Create the paging widget, if necessary.
259 if self.paging in ('inside', 'hsplit', 'vsplit'):
259 if self.paging in ('inside', 'hsplit', 'vsplit'):
260 self._page_control = self._create_page_control()
260 self._page_control = self._create_page_control()
261 if self._splitter:
261 if self._splitter:
262 self._page_control.hide()
262 self._page_control.hide()
263 self._splitter.addWidget(self._page_control)
263 self._splitter.addWidget(self._page_control)
264 else:
264 else:
265 layout.addWidget(self._page_control)
265 layout.addWidget(self._page_control)
266
266
267 # Initialize protected variables. Some variables contain useful state
267 # Initialize protected variables. Some variables contain useful state
268 # information for subclasses; they should be considered read-only.
268 # information for subclasses; they should be considered read-only.
269 self._append_before_prompt_pos = 0
269 self._append_before_prompt_pos = 0
270 self._ansi_processor = QtAnsiCodeProcessor()
270 self._ansi_processor = QtAnsiCodeProcessor()
271 if self.gui_completion == 'ncurses':
271 if self.gui_completion == 'ncurses':
272 self._completion_widget = CompletionHtml(self)
272 self._completion_widget = CompletionHtml(self)
273 elif self.gui_completion == 'droplist':
273 elif self.gui_completion == 'droplist':
274 self._completion_widget = CompletionWidget(self)
274 self._completion_widget = CompletionWidget(self)
275 elif self.gui_completion == 'plain':
275 elif self.gui_completion == 'plain':
276 self._completion_widget = CompletionPlain(self)
276 self._completion_widget = CompletionPlain(self)
277
277
278 self._continuation_prompt = '> '
278 self._continuation_prompt = '> '
279 self._continuation_prompt_html = None
279 self._continuation_prompt_html = None
280 self._executing = False
280 self._executing = False
281 self._filter_resize = False
281 self._filter_resize = False
282 self._html_exporter = HtmlExporter(self._control)
282 self._html_exporter = HtmlExporter(self._control)
283 self._input_buffer_executing = ''
283 self._input_buffer_executing = ''
284 self._input_buffer_pending = ''
284 self._input_buffer_pending = ''
285 self._kill_ring = QtKillRing(self._control)
285 self._kill_ring = QtKillRing(self._control)
286 self._prompt = ''
286 self._prompt = ''
287 self._prompt_html = None
287 self._prompt_html = None
288 self._prompt_pos = 0
288 self._prompt_pos = 0
289 self._prompt_sep = ''
289 self._prompt_sep = ''
290 self._reading = False
290 self._reading = False
291 self._reading_callback = None
291 self._reading_callback = None
292 self._tab_width = 8
292 self._tab_width = 8
293
293
294 # Set a monospaced font.
294 # Set a monospaced font.
295 self.reset_font()
295 self.reset_font()
296
296
297 # Configure actions.
297 # Configure actions.
298 action = QtGui.QAction('Print', None)
298 action = QtGui.QAction('Print', None)
299 action.setEnabled(True)
299 action.setEnabled(True)
300 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
300 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
301 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
301 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
302 # Only override the default if there is a collision.
302 # Only override the default if there is a collision.
303 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
303 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
304 printkey = "Ctrl+Shift+P"
304 printkey = "Ctrl+Shift+P"
305 action.setShortcut(printkey)
305 action.setShortcut(printkey)
306 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
306 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
307 action.triggered.connect(self.print_)
307 action.triggered.connect(self.print_)
308 self.addAction(action)
308 self.addAction(action)
309 self.print_action = action
309 self.print_action = action
310
310
311 action = QtGui.QAction('Save as HTML/XML', None)
311 action = QtGui.QAction('Save as HTML/XML', None)
312 action.setShortcut(QtGui.QKeySequence.Save)
312 action.setShortcut(QtGui.QKeySequence.Save)
313 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
313 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
314 action.triggered.connect(self.export_html)
314 action.triggered.connect(self.export_html)
315 self.addAction(action)
315 self.addAction(action)
316 self.export_action = action
316 self.export_action = action
317
317
318 action = QtGui.QAction('Select All', None)
318 action = QtGui.QAction('Select All', None)
319 action.setEnabled(True)
319 action.setEnabled(True)
320 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
320 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
321 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
321 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
322 # Only override the default if there is a collision.
322 # Only override the default if there is a collision.
323 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
323 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
324 selectall = "Ctrl+Shift+A"
324 selectall = "Ctrl+Shift+A"
325 action.setShortcut(selectall)
325 action.setShortcut(selectall)
326 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
326 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
327 action.triggered.connect(self.select_all)
327 action.triggered.connect(self.select_all)
328 self.addAction(action)
328 self.addAction(action)
329 self.select_all_action = action
329 self.select_all_action = action
330
330
331 self.increase_font_size = QtGui.QAction("Bigger Font",
331 self.increase_font_size = QtGui.QAction("Bigger Font",
332 self,
332 self,
333 shortcut=QtGui.QKeySequence.ZoomIn,
333 shortcut=QtGui.QKeySequence.ZoomIn,
334 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
334 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
335 statusTip="Increase the font size by one point",
335 statusTip="Increase the font size by one point",
336 triggered=self._increase_font_size)
336 triggered=self._increase_font_size)
337 self.addAction(self.increase_font_size)
337 self.addAction(self.increase_font_size)
338
338
339 self.decrease_font_size = QtGui.QAction("Smaller Font",
339 self.decrease_font_size = QtGui.QAction("Smaller Font",
340 self,
340 self,
341 shortcut=QtGui.QKeySequence.ZoomOut,
341 shortcut=QtGui.QKeySequence.ZoomOut,
342 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
342 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
343 statusTip="Decrease the font size by one point",
343 statusTip="Decrease the font size by one point",
344 triggered=self._decrease_font_size)
344 triggered=self._decrease_font_size)
345 self.addAction(self.decrease_font_size)
345 self.addAction(self.decrease_font_size)
346
346
347 self.reset_font_size = QtGui.QAction("Normal Font",
347 self.reset_font_size = QtGui.QAction("Normal Font",
348 self,
348 self,
349 shortcut="Ctrl+0",
349 shortcut="Ctrl+0",
350 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
350 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
351 statusTip="Restore the Normal font size",
351 statusTip="Restore the Normal font size",
352 triggered=self.reset_font)
352 triggered=self.reset_font)
353 self.addAction(self.reset_font_size)
353 self.addAction(self.reset_font_size)
354
354
355 # Accept drag and drop events here. Drops were already turned off
355 # Accept drag and drop events here. Drops were already turned off
356 # in self._control when that widget was created.
356 # in self._control when that widget was created.
357 self.setAcceptDrops(True)
357 self.setAcceptDrops(True)
358
358
359 #---------------------------------------------------------------------------
359 #---------------------------------------------------------------------------
360 # Drag and drop support
360 # Drag and drop support
361 #---------------------------------------------------------------------------
361 #---------------------------------------------------------------------------
362
362
363 def dragEnterEvent(self, e):
363 def dragEnterEvent(self, e):
364 if e.mimeData().hasUrls():
364 if e.mimeData().hasUrls():
365 # The link action should indicate to that the drop will insert
365 # The link action should indicate to that the drop will insert
366 # the file anme.
366 # the file anme.
367 e.setDropAction(QtCore.Qt.LinkAction)
367 e.setDropAction(QtCore.Qt.LinkAction)
368 e.accept()
368 e.accept()
369 elif e.mimeData().hasText():
369 elif e.mimeData().hasText():
370 # By changing the action to copy we don't need to worry about
370 # By changing the action to copy we don't need to worry about
371 # the user accidentally moving text around in the widget.
371 # the user accidentally moving text around in the widget.
372 e.setDropAction(QtCore.Qt.CopyAction)
372 e.setDropAction(QtCore.Qt.CopyAction)
373 e.accept()
373 e.accept()
374
374
375 def dragMoveEvent(self, e):
375 def dragMoveEvent(self, e):
376 if e.mimeData().hasUrls():
376 if e.mimeData().hasUrls():
377 pass
377 pass
378 elif e.mimeData().hasText():
378 elif e.mimeData().hasText():
379 cursor = self._control.cursorForPosition(e.pos())
379 cursor = self._control.cursorForPosition(e.pos())
380 if self._in_buffer(cursor.position()):
380 if self._in_buffer(cursor.position()):
381 e.setDropAction(QtCore.Qt.CopyAction)
381 e.setDropAction(QtCore.Qt.CopyAction)
382 self._control.setTextCursor(cursor)
382 self._control.setTextCursor(cursor)
383 else:
383 else:
384 e.setDropAction(QtCore.Qt.IgnoreAction)
384 e.setDropAction(QtCore.Qt.IgnoreAction)
385 e.accept()
385 e.accept()
386
386
387 def dropEvent(self, e):
387 def dropEvent(self, e):
388 if e.mimeData().hasUrls():
388 if e.mimeData().hasUrls():
389 self._keep_cursor_in_buffer()
389 self._keep_cursor_in_buffer()
390 cursor = self._control.textCursor()
390 cursor = self._control.textCursor()
391 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
391 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
392 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
392 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
393 for f in filenames)
393 for f in filenames)
394 self._insert_plain_text_into_buffer(cursor, text)
394 self._insert_plain_text_into_buffer(cursor, text)
395 elif e.mimeData().hasText():
395 elif e.mimeData().hasText():
396 cursor = self._control.cursorForPosition(e.pos())
396 cursor = self._control.cursorForPosition(e.pos())
397 if self._in_buffer(cursor.position()):
397 if self._in_buffer(cursor.position()):
398 text = e.mimeData().text()
398 text = e.mimeData().text()
399 self._insert_plain_text_into_buffer(cursor, text)
399 self._insert_plain_text_into_buffer(cursor, text)
400
400
401 def eventFilter(self, obj, event):
401 def eventFilter(self, obj, event):
402 """ Reimplemented to ensure a console-like behavior in the underlying
402 """ Reimplemented to ensure a console-like behavior in the underlying
403 text widgets.
403 text widgets.
404 """
404 """
405 etype = event.type()
405 etype = event.type()
406 if etype == QtCore.QEvent.KeyPress:
406 if etype == QtCore.QEvent.KeyPress:
407
407
408 # Re-map keys for all filtered widgets.
408 # Re-map keys for all filtered widgets.
409 key = event.key()
409 key = event.key()
410 if self._control_key_down(event.modifiers()) and \
410 if self._control_key_down(event.modifiers()) and \
411 key in self._ctrl_down_remap:
411 key in self._ctrl_down_remap:
412 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
412 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
413 self._ctrl_down_remap[key],
413 self._ctrl_down_remap[key],
414 QtCore.Qt.NoModifier)
414 QtCore.Qt.NoModifier)
415 QtGui.qApp.sendEvent(obj, new_event)
415 QtGui.qApp.sendEvent(obj, new_event)
416 return True
416 return True
417
417
418 elif obj == self._control:
418 elif obj == self._control:
419 return self._event_filter_console_keypress(event)
419 return self._event_filter_console_keypress(event)
420
420
421 elif obj == self._page_control:
421 elif obj == self._page_control:
422 return self._event_filter_page_keypress(event)
422 return self._event_filter_page_keypress(event)
423
423
424 # Make middle-click paste safe.
424 # Make middle-click paste safe.
425 elif etype == QtCore.QEvent.MouseButtonRelease and \
425 elif etype == QtCore.QEvent.MouseButtonRelease and \
426 event.button() == QtCore.Qt.MidButton and \
426 event.button() == QtCore.Qt.MidButton and \
427 obj == self._control.viewport():
427 obj == self._control.viewport():
428 cursor = self._control.cursorForPosition(event.pos())
428 cursor = self._control.cursorForPosition(event.pos())
429 self._control.setTextCursor(cursor)
429 self._control.setTextCursor(cursor)
430 self.paste(QtGui.QClipboard.Selection)
430 self.paste(QtGui.QClipboard.Selection)
431 return True
431 return True
432
432
433 # Manually adjust the scrollbars *after* a resize event is dispatched.
433 # Manually adjust the scrollbars *after* a resize event is dispatched.
434 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
434 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
435 self._filter_resize = True
435 self._filter_resize = True
436 QtGui.qApp.sendEvent(obj, event)
436 QtGui.qApp.sendEvent(obj, event)
437 self._adjust_scrollbars()
437 self._adjust_scrollbars()
438 self._filter_resize = False
438 self._filter_resize = False
439 return True
439 return True
440
440
441 # Override shortcuts for all filtered widgets.
441 # Override shortcuts for all filtered widgets.
442 elif etype == QtCore.QEvent.ShortcutOverride and \
442 elif etype == QtCore.QEvent.ShortcutOverride and \
443 self.override_shortcuts and \
443 self.override_shortcuts and \
444 self._control_key_down(event.modifiers()) and \
444 self._control_key_down(event.modifiers()) and \
445 event.key() in self._shortcuts:
445 event.key() in self._shortcuts:
446 event.accept()
446 event.accept()
447
447
448 # Handle scrolling of the vsplit pager. This hack attempts to solve
448 # Handle scrolling of the vsplit pager. This hack attempts to solve
449 # problems with tearing of the help text inside the pager window. This
449 # problems with tearing of the help text inside the pager window. This
450 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
450 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
451 # perfect but makes the pager more usable.
451 # perfect but makes the pager more usable.
452 elif etype in self._pager_scroll_events and \
452 elif etype in self._pager_scroll_events and \
453 obj == self._page_control:
453 obj == self._page_control:
454 self._page_control.repaint()
454 self._page_control.repaint()
455 return True
455 return True
456
456
457 elif etype == QtCore.QEvent.MouseMove:
457 elif etype == QtCore.QEvent.MouseMove:
458 anchor = self._control.anchorAt(event.pos())
458 anchor = self._control.anchorAt(event.pos())
459 QtGui.QToolTip.showText(event.globalPos(), anchor)
459 QtGui.QToolTip.showText(event.globalPos(), anchor)
460
460
461 return super(ConsoleWidget, self).eventFilter(obj, event)
461 return super(ConsoleWidget, self).eventFilter(obj, event)
462
462
463 #---------------------------------------------------------------------------
463 #---------------------------------------------------------------------------
464 # 'QWidget' interface
464 # 'QWidget' interface
465 #---------------------------------------------------------------------------
465 #---------------------------------------------------------------------------
466
466
467 def sizeHint(self):
467 def sizeHint(self):
468 """ Reimplemented to suggest a size that is 80 characters wide and
468 """ Reimplemented to suggest a size that is 80 characters wide and
469 25 lines high.
469 25 lines high.
470 """
470 """
471 font_metrics = QtGui.QFontMetrics(self.font)
471 font_metrics = QtGui.QFontMetrics(self.font)
472 margin = (self._control.frameWidth() +
472 margin = (self._control.frameWidth() +
473 self._control.document().documentMargin()) * 2
473 self._control.document().documentMargin()) * 2
474 style = self.style()
474 style = self.style()
475 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
475 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
476
476
477 # Note 1: Despite my best efforts to take the various margins into
477 # Note 1: Despite my best efforts to take the various margins into
478 # account, the width is still coming out a bit too small, so we include
478 # account, the width is still coming out a bit too small, so we include
479 # a fudge factor of one character here.
479 # a fudge factor of one character here.
480 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
480 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
481 # to a Qt bug on certain Mac OS systems where it returns 0.
481 # to a Qt bug on certain Mac OS systems where it returns 0.
482 width = font_metrics.width(' ') * self.width + margin
482 width = font_metrics.width(' ') * self.width + margin
483 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
483 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
484 if self.paging == 'hsplit':
484 if self.paging == 'hsplit':
485 width = width * 2 + splitwidth
485 width = width * 2 + splitwidth
486
486
487 height = font_metrics.height() * self.height + margin
487 height = font_metrics.height() * self.height + margin
488 if self.paging == 'vsplit':
488 if self.paging == 'vsplit':
489 height = height * 2 + splitwidth
489 height = height * 2 + splitwidth
490
490
491 return QtCore.QSize(width, height)
491 return QtCore.QSize(width, height)
492
492
493 #---------------------------------------------------------------------------
493 #---------------------------------------------------------------------------
494 # 'ConsoleWidget' public interface
494 # 'ConsoleWidget' public interface
495 #---------------------------------------------------------------------------
495 #---------------------------------------------------------------------------
496
496
497 def can_copy(self):
497 def can_copy(self):
498 """ Returns whether text can be copied to the clipboard.
498 """ Returns whether text can be copied to the clipboard.
499 """
499 """
500 return self._control.textCursor().hasSelection()
500 return self._control.textCursor().hasSelection()
501
501
502 def can_cut(self):
502 def can_cut(self):
503 """ Returns whether text can be cut to the clipboard.
503 """ Returns whether text can be cut to the clipboard.
504 """
504 """
505 cursor = self._control.textCursor()
505 cursor = self._control.textCursor()
506 return (cursor.hasSelection() and
506 return (cursor.hasSelection() and
507 self._in_buffer(cursor.anchor()) and
507 self._in_buffer(cursor.anchor()) and
508 self._in_buffer(cursor.position()))
508 self._in_buffer(cursor.position()))
509
509
510 def can_paste(self):
510 def can_paste(self):
511 """ Returns whether text can be pasted from the clipboard.
511 """ Returns whether text can be pasted from the clipboard.
512 """
512 """
513 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
513 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
514 return bool(QtGui.QApplication.clipboard().text())
514 return bool(QtGui.QApplication.clipboard().text())
515 return False
515 return False
516
516
517 def clear(self, keep_input=True):
517 def clear(self, keep_input=True):
518 """ Clear the console.
518 """ Clear the console.
519
519
520 Parameters:
520 Parameters:
521 -----------
521 -----------
522 keep_input : bool, optional (default True)
522 keep_input : bool, optional (default True)
523 If set, restores the old input buffer if a new prompt is written.
523 If set, restores the old input buffer if a new prompt is written.
524 """
524 """
525 if self._executing:
525 if self._executing:
526 self._control.clear()
526 self._control.clear()
527 else:
527 else:
528 if keep_input:
528 if keep_input:
529 input_buffer = self.input_buffer
529 input_buffer = self.input_buffer
530 self._control.clear()
530 self._control.clear()
531 self._show_prompt()
531 self._show_prompt()
532 if keep_input:
532 if keep_input:
533 self.input_buffer = input_buffer
533 self.input_buffer = input_buffer
534
534
535 def copy(self):
535 def copy(self):
536 """ Copy the currently selected text to the clipboard.
536 """ Copy the currently selected text to the clipboard.
537 """
537 """
538 self.layout().currentWidget().copy()
538 self.layout().currentWidget().copy()
539
539
540 def copy_anchor(self, anchor):
540 def copy_anchor(self, anchor):
541 """ Copy anchor text to the clipboard
541 """ Copy anchor text to the clipboard
542 """
542 """
543 QtGui.QApplication.clipboard().setText(anchor)
543 QtGui.QApplication.clipboard().setText(anchor)
544
544
545 def cut(self):
545 def cut(self):
546 """ Copy the currently selected text to the clipboard and delete it
546 """ Copy the currently selected text to the clipboard and delete it
547 if it's inside the input buffer.
547 if it's inside the input buffer.
548 """
548 """
549 self.copy()
549 self.copy()
550 if self.can_cut():
550 if self.can_cut():
551 self._control.textCursor().removeSelectedText()
551 self._control.textCursor().removeSelectedText()
552
552
553 def execute(self, source=None, hidden=False, interactive=False):
553 def execute(self, source=None, hidden=False, interactive=False):
554 """ Executes source or the input buffer, possibly prompting for more
554 """ Executes source or the input buffer, possibly prompting for more
555 input.
555 input.
556
556
557 Parameters:
557 Parameters:
558 -----------
558 -----------
559 source : str, optional
559 source : str, optional
560
560
561 The source to execute. If not specified, the input buffer will be
561 The source to execute. If not specified, the input buffer will be
562 used. If specified and 'hidden' is False, the input buffer will be
562 used. If specified and 'hidden' is False, the input buffer will be
563 replaced with the source before execution.
563 replaced with the source before execution.
564
564
565 hidden : bool, optional (default False)
565 hidden : bool, optional (default False)
566
566
567 If set, no output will be shown and the prompt will not be modified.
567 If set, no output will be shown and the prompt will not be modified.
568 In other words, it will be completely invisible to the user that
568 In other words, it will be completely invisible to the user that
569 an execution has occurred.
569 an execution has occurred.
570
570
571 interactive : bool, optional (default False)
571 interactive : bool, optional (default False)
572
572
573 Whether the console is to treat the source as having been manually
573 Whether the console is to treat the source as having been manually
574 entered by the user. The effect of this parameter depends on the
574 entered by the user. The effect of this parameter depends on the
575 subclass implementation.
575 subclass implementation.
576
576
577 Raises:
577 Raises:
578 -------
578 -------
579 RuntimeError
579 RuntimeError
580 If incomplete input is given and 'hidden' is True. In this case,
580 If incomplete input is given and 'hidden' is True. In this case,
581 it is not possible to prompt for more input.
581 it is not possible to prompt for more input.
582
582
583 Returns:
583 Returns:
584 --------
584 --------
585 A boolean indicating whether the source was executed.
585 A boolean indicating whether the source was executed.
586 """
586 """
587 # WARNING: The order in which things happen here is very particular, in
587 # WARNING: The order in which things happen here is very particular, in
588 # large part because our syntax highlighting is fragile. If you change
588 # large part because our syntax highlighting is fragile. If you change
589 # something, test carefully!
589 # something, test carefully!
590
590
591 # Decide what to execute.
591 # Decide what to execute.
592 if source is None:
592 if source is None:
593 source = self.input_buffer
593 source = self.input_buffer
594 if not hidden:
594 if not hidden:
595 # A newline is appended later, but it should be considered part
595 # A newline is appended later, but it should be considered part
596 # of the input buffer.
596 # of the input buffer.
597 source += '\n'
597 source += '\n'
598 elif not hidden:
598 elif not hidden:
599 self.input_buffer = source
599 self.input_buffer = source
600
600
601 # Execute the source or show a continuation prompt if it is incomplete.
601 # Execute the source or show a continuation prompt if it is incomplete.
602 complete = self._is_complete(source, interactive)
602 complete = self._is_complete(source, interactive)
603 if hidden:
603 if hidden:
604 if complete:
604 if complete:
605 self._execute(source, hidden)
605 self._execute(source, hidden)
606 else:
606 else:
607 error = 'Incomplete noninteractive input: "%s"'
607 error = 'Incomplete noninteractive input: "%s"'
608 raise RuntimeError(error % source)
608 raise RuntimeError(error % source)
609 else:
609 else:
610 if complete:
610 if complete:
611 self._append_plain_text('\n')
611 self._append_plain_text('\n')
612 self._input_buffer_executing = self.input_buffer
612 self._input_buffer_executing = self.input_buffer
613 self._executing = True
613 self._executing = True
614 self._prompt_finished()
614 self._prompt_finished()
615
615
616 # The maximum block count is only in effect during execution.
616 # The maximum block count is only in effect during execution.
617 # This ensures that _prompt_pos does not become invalid due to
617 # This ensures that _prompt_pos does not become invalid due to
618 # text truncation.
618 # text truncation.
619 self._control.document().setMaximumBlockCount(self.buffer_size)
619 self._control.document().setMaximumBlockCount(self.buffer_size)
620
620
621 # Setting a positive maximum block count will automatically
621 # Setting a positive maximum block count will automatically
622 # disable the undo/redo history, but just to be safe:
622 # disable the undo/redo history, but just to be safe:
623 self._control.setUndoRedoEnabled(False)
623 self._control.setUndoRedoEnabled(False)
624
624
625 # Perform actual execution.
625 # Perform actual execution.
626 self._execute(source, hidden)
626 self._execute(source, hidden)
627
627
628 else:
628 else:
629 # Do this inside an edit block so continuation prompts are
629 # Do this inside an edit block so continuation prompts are
630 # removed seamlessly via undo/redo.
630 # removed seamlessly via undo/redo.
631 cursor = self._get_end_cursor()
631 cursor = self._get_end_cursor()
632 cursor.beginEditBlock()
632 cursor.beginEditBlock()
633 cursor.insertText('\n')
633 cursor.insertText('\n')
634 self._insert_continuation_prompt(cursor)
634 self._insert_continuation_prompt(cursor)
635 cursor.endEditBlock()
635 cursor.endEditBlock()
636
636
637 # Do not do this inside the edit block. It works as expected
637 # Do not do this inside the edit block. It works as expected
638 # when using a QPlainTextEdit control, but does not have an
638 # when using a QPlainTextEdit control, but does not have an
639 # effect when using a QTextEdit. I believe this is a Qt bug.
639 # effect when using a QTextEdit. I believe this is a Qt bug.
640 self._control.moveCursor(QtGui.QTextCursor.End)
640 self._control.moveCursor(QtGui.QTextCursor.End)
641
641
642 return complete
642 return complete
643
643
644 def export_html(self):
644 def export_html(self):
645 """ Shows a dialog to export HTML/XML in various formats.
645 """ Shows a dialog to export HTML/XML in various formats.
646 """
646 """
647 self._html_exporter.export()
647 self._html_exporter.export()
648
648
649 def _get_input_buffer(self, force=False):
649 def _get_input_buffer(self, force=False):
650 """ The text that the user has entered entered at the current prompt.
650 """ The text that the user has entered entered at the current prompt.
651
651
652 If the console is currently executing, the text that is executing will
652 If the console is currently executing, the text that is executing will
653 always be returned.
653 always be returned.
654 """
654 """
655 # If we're executing, the input buffer may not even exist anymore due to
655 # If we're executing, the input buffer may not even exist anymore due to
656 # the limit imposed by 'buffer_size'. Therefore, we store it.
656 # the limit imposed by 'buffer_size'. Therefore, we store it.
657 if self._executing and not force:
657 if self._executing and not force:
658 return self._input_buffer_executing
658 return self._input_buffer_executing
659
659
660 cursor = self._get_end_cursor()
660 cursor = self._get_end_cursor()
661 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
661 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
662 input_buffer = cursor.selection().toPlainText()
662 input_buffer = cursor.selection().toPlainText()
663
663
664 # Strip out continuation prompts.
664 # Strip out continuation prompts.
665 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
665 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
666
666
667 def _set_input_buffer(self, string):
667 def _set_input_buffer(self, string):
668 """ Sets the text in the input buffer.
668 """ Sets the text in the input buffer.
669
669
670 If the console is currently executing, this call has no *immediate*
670 If the console is currently executing, this call has no *immediate*
671 effect. When the execution is finished, the input buffer will be updated
671 effect. When the execution is finished, the input buffer will be updated
672 appropriately.
672 appropriately.
673 """
673 """
674 # If we're executing, store the text for later.
674 # If we're executing, store the text for later.
675 if self._executing:
675 if self._executing:
676 self._input_buffer_pending = string
676 self._input_buffer_pending = string
677 return
677 return
678
678
679 # Remove old text.
679 # Remove old text.
680 cursor = self._get_end_cursor()
680 cursor = self._get_end_cursor()
681 cursor.beginEditBlock()
681 cursor.beginEditBlock()
682 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
682 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
683 cursor.removeSelectedText()
683 cursor.removeSelectedText()
684
684
685 # Insert new text with continuation prompts.
685 # Insert new text with continuation prompts.
686 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
686 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
687 cursor.endEditBlock()
687 cursor.endEditBlock()
688 self._control.moveCursor(QtGui.QTextCursor.End)
688 self._control.moveCursor(QtGui.QTextCursor.End)
689
689
690 input_buffer = property(_get_input_buffer, _set_input_buffer)
690 input_buffer = property(_get_input_buffer, _set_input_buffer)
691
691
692 def _get_font(self):
692 def _get_font(self):
693 """ The base font being used by the ConsoleWidget.
693 """ The base font being used by the ConsoleWidget.
694 """
694 """
695 return self._control.document().defaultFont()
695 return self._control.document().defaultFont()
696
696
697 def _set_font(self, font):
697 def _set_font(self, font):
698 """ Sets the base font for the ConsoleWidget to the specified QFont.
698 """ Sets the base font for the ConsoleWidget to the specified QFont.
699 """
699 """
700 font_metrics = QtGui.QFontMetrics(font)
700 font_metrics = QtGui.QFontMetrics(font)
701 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
701 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
702
702
703 self._completion_widget.setFont(font)
703 self._completion_widget.setFont(font)
704 self._control.document().setDefaultFont(font)
704 self._control.document().setDefaultFont(font)
705 if self._page_control:
705 if self._page_control:
706 self._page_control.document().setDefaultFont(font)
706 self._page_control.document().setDefaultFont(font)
707
707
708 self.font_changed.emit(font)
708 self.font_changed.emit(font)
709
709
710 font = property(_get_font, _set_font)
710 font = property(_get_font, _set_font)
711
711
712 def open_anchor(self, anchor):
712 def open_anchor(self, anchor):
713 """ Open selected anchor in the default webbrowser
713 """ Open selected anchor in the default webbrowser
714 """
714 """
715 webbrowser.open( anchor )
715 webbrowser.open( anchor )
716
716
717 def paste(self, mode=QtGui.QClipboard.Clipboard):
717 def paste(self, mode=QtGui.QClipboard.Clipboard):
718 """ Paste the contents of the clipboard into the input region.
718 """ Paste the contents of the clipboard into the input region.
719
719
720 Parameters:
720 Parameters:
721 -----------
721 -----------
722 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
722 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
723
723
724 Controls which part of the system clipboard is used. This can be
724 Controls which part of the system clipboard is used. This can be
725 used to access the selection clipboard in X11 and the Find buffer
725 used to access the selection clipboard in X11 and the Find buffer
726 in Mac OS. By default, the regular clipboard is used.
726 in Mac OS. By default, the regular clipboard is used.
727 """
727 """
728 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
728 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
729 # Make sure the paste is safe.
729 # Make sure the paste is safe.
730 self._keep_cursor_in_buffer()
730 self._keep_cursor_in_buffer()
731 cursor = self._control.textCursor()
731 cursor = self._control.textCursor()
732
732
733 # Remove any trailing newline, which confuses the GUI and forces the
733 # Remove any trailing newline, which confuses the GUI and forces the
734 # user to backspace.
734 # user to backspace.
735 text = QtGui.QApplication.clipboard().text(mode).rstrip()
735 text = QtGui.QApplication.clipboard().text(mode).rstrip()
736 self._insert_plain_text_into_buffer(cursor, dedent(text))
736 self._insert_plain_text_into_buffer(cursor, dedent(text))
737
737
738 def print_(self, printer = None):
738 def print_(self, printer = None):
739 """ Print the contents of the ConsoleWidget to the specified QPrinter.
739 """ Print the contents of the ConsoleWidget to the specified QPrinter.
740 """
740 """
741 if (not printer):
741 if (not printer):
742 printer = QtGui.QPrinter()
742 printer = QtGui.QPrinter()
743 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
743 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
744 return
744 return
745 self._control.print_(printer)
745 self._control.print_(printer)
746
746
747 def prompt_to_top(self):
747 def prompt_to_top(self):
748 """ Moves the prompt to the top of the viewport.
748 """ Moves the prompt to the top of the viewport.
749 """
749 """
750 if not self._executing:
750 if not self._executing:
751 prompt_cursor = self._get_prompt_cursor()
751 prompt_cursor = self._get_prompt_cursor()
752 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
752 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
753 self._set_cursor(prompt_cursor)
753 self._set_cursor(prompt_cursor)
754 self._set_top_cursor(prompt_cursor)
754 self._set_top_cursor(prompt_cursor)
755
755
756 def redo(self):
756 def redo(self):
757 """ Redo the last operation. If there is no operation to redo, nothing
757 """ Redo the last operation. If there is no operation to redo, nothing
758 happens.
758 happens.
759 """
759 """
760 self._control.redo()
760 self._control.redo()
761
761
762 def reset_font(self):
762 def reset_font(self):
763 """ Sets the font to the default fixed-width font for this platform.
763 """ Sets the font to the default fixed-width font for this platform.
764 """
764 """
765 if sys.platform == 'win32':
765 if sys.platform == 'win32':
766 # Consolas ships with Vista/Win7, fallback to Courier if needed
766 # Consolas ships with Vista/Win7, fallback to Courier if needed
767 fallback = 'Courier'
767 fallback = 'Courier'
768 elif sys.platform == 'darwin':
768 elif sys.platform == 'darwin':
769 # OSX always has Monaco
769 # OSX always has Monaco
770 fallback = 'Monaco'
770 fallback = 'Monaco'
771 else:
771 else:
772 # Monospace should always exist
772 # Monospace should always exist
773 fallback = 'Monospace'
773 fallback = 'Monospace'
774 font = get_font(self.font_family, fallback)
774 font = get_font(self.font_family, fallback)
775 if self.font_size:
775 if self.font_size:
776 font.setPointSize(self.font_size)
776 font.setPointSize(self.font_size)
777 else:
777 else:
778 font.setPointSize(QtGui.qApp.font().pointSize())
778 font.setPointSize(QtGui.qApp.font().pointSize())
779 font.setStyleHint(QtGui.QFont.TypeWriter)
779 font.setStyleHint(QtGui.QFont.TypeWriter)
780 self._set_font(font)
780 self._set_font(font)
781
781
782 def change_font_size(self, delta):
782 def change_font_size(self, delta):
783 """Change the font size by the specified amount (in points).
783 """Change the font size by the specified amount (in points).
784 """
784 """
785 font = self.font
785 font = self.font
786 size = max(font.pointSize() + delta, 1) # minimum 1 point
786 size = max(font.pointSize() + delta, 1) # minimum 1 point
787 font.setPointSize(size)
787 font.setPointSize(size)
788 self._set_font(font)
788 self._set_font(font)
789
789
790 def _increase_font_size(self):
790 def _increase_font_size(self):
791 self.change_font_size(1)
791 self.change_font_size(1)
792
792
793 def _decrease_font_size(self):
793 def _decrease_font_size(self):
794 self.change_font_size(-1)
794 self.change_font_size(-1)
795
795
796 def select_all(self):
796 def select_all(self):
797 """ Selects all the text in the buffer.
797 """ Selects all the text in the buffer.
798 """
798 """
799 self._control.selectAll()
799 self._control.selectAll()
800
800
801 def _get_tab_width(self):
801 def _get_tab_width(self):
802 """ The width (in terms of space characters) for tab characters.
802 """ The width (in terms of space characters) for tab characters.
803 """
803 """
804 return self._tab_width
804 return self._tab_width
805
805
806 def _set_tab_width(self, tab_width):
806 def _set_tab_width(self, tab_width):
807 """ Sets the width (in terms of space characters) for tab characters.
807 """ Sets the width (in terms of space characters) for tab characters.
808 """
808 """
809 font_metrics = QtGui.QFontMetrics(self.font)
809 font_metrics = QtGui.QFontMetrics(self.font)
810 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
810 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
811
811
812 self._tab_width = tab_width
812 self._tab_width = tab_width
813
813
814 tab_width = property(_get_tab_width, _set_tab_width)
814 tab_width = property(_get_tab_width, _set_tab_width)
815
815
816 def undo(self):
816 def undo(self):
817 """ Undo the last operation. If there is no operation to undo, nothing
817 """ Undo the last operation. If there is no operation to undo, nothing
818 happens.
818 happens.
819 """
819 """
820 self._control.undo()
820 self._control.undo()
821
821
822 #---------------------------------------------------------------------------
822 #---------------------------------------------------------------------------
823 # 'ConsoleWidget' abstract interface
823 # 'ConsoleWidget' abstract interface
824 #---------------------------------------------------------------------------
824 #---------------------------------------------------------------------------
825
825
826 def _is_complete(self, source, interactive):
826 def _is_complete(self, source, interactive):
827 """ Returns whether 'source' can be executed. When triggered by an
827 """ Returns whether 'source' can be executed. When triggered by an
828 Enter/Return key press, 'interactive' is True; otherwise, it is
828 Enter/Return key press, 'interactive' is True; otherwise, it is
829 False.
829 False.
830 """
830 """
831 raise NotImplementedError
831 raise NotImplementedError
832
832
833 def _execute(self, source, hidden):
833 def _execute(self, source, hidden):
834 """ Execute 'source'. If 'hidden', do not show any output.
834 """ Execute 'source'. If 'hidden', do not show any output.
835 """
835 """
836 raise NotImplementedError
836 raise NotImplementedError
837
837
838 def _prompt_started_hook(self):
838 def _prompt_started_hook(self):
839 """ Called immediately after a new prompt is displayed.
839 """ Called immediately after a new prompt is displayed.
840 """
840 """
841 pass
841 pass
842
842
843 def _prompt_finished_hook(self):
843 def _prompt_finished_hook(self):
844 """ Called immediately after a prompt is finished, i.e. when some input
844 """ Called immediately after a prompt is finished, i.e. when some input
845 will be processed and a new prompt displayed.
845 will be processed and a new prompt displayed.
846 """
846 """
847 pass
847 pass
848
848
849 def _up_pressed(self, shift_modifier):
849 def _up_pressed(self, shift_modifier):
850 """ Called when the up key is pressed. Returns whether to continue
850 """ Called when the up key is pressed. Returns whether to continue
851 processing the event.
851 processing the event.
852 """
852 """
853 return True
853 return True
854
854
855 def _down_pressed(self, shift_modifier):
855 def _down_pressed(self, shift_modifier):
856 """ Called when the down key is pressed. Returns whether to continue
856 """ Called when the down key is pressed. Returns whether to continue
857 processing the event.
857 processing the event.
858 """
858 """
859 return True
859 return True
860
860
861 def _tab_pressed(self):
861 def _tab_pressed(self):
862 """ Called when the tab key is pressed. Returns whether to continue
862 """ Called when the tab key is pressed. Returns whether to continue
863 processing the event.
863 processing the event.
864 """
864 """
865 return False
865 return False
866
866
867 #--------------------------------------------------------------------------
867 #--------------------------------------------------------------------------
868 # 'ConsoleWidget' protected interface
868 # 'ConsoleWidget' protected interface
869 #--------------------------------------------------------------------------
869 #--------------------------------------------------------------------------
870
870
871 def _append_custom(self, insert, input, before_prompt=False):
871 def _append_custom(self, insert, input, before_prompt=False):
872 """ A low-level method for appending content to the end of the buffer.
872 """ A low-level method for appending content to the end of the buffer.
873
873
874 If 'before_prompt' is enabled, the content will be inserted before the
874 If 'before_prompt' is enabled, the content will be inserted before the
875 current prompt, if there is one.
875 current prompt, if there is one.
876 """
876 """
877 # Determine where to insert the content.
877 # Determine where to insert the content.
878 cursor = self._control.textCursor()
878 cursor = self._control.textCursor()
879 if before_prompt and (self._reading or not self._executing):
879 if before_prompt and (self._reading or not self._executing):
880 cursor.setPosition(self._append_before_prompt_pos)
880 cursor.setPosition(self._append_before_prompt_pos)
881 else:
881 else:
882 cursor.movePosition(QtGui.QTextCursor.End)
882 cursor.movePosition(QtGui.QTextCursor.End)
883 start_pos = cursor.position()
883 start_pos = cursor.position()
884
884
885 # Perform the insertion.
885 # Perform the insertion.
886 result = insert(cursor, input)
886 result = insert(cursor, input)
887
887
888 # Adjust the prompt position if we have inserted before it. This is safe
888 # Adjust the prompt position if we have inserted before it. This is safe
889 # because buffer truncation is disabled when not executing.
889 # because buffer truncation is disabled when not executing.
890 if before_prompt and not self._executing:
890 if before_prompt and not self._executing:
891 diff = cursor.position() - start_pos
891 diff = cursor.position() - start_pos
892 self._append_before_prompt_pos += diff
892 self._append_before_prompt_pos += diff
893 self._prompt_pos += diff
893 self._prompt_pos += diff
894
894
895 return result
895 return result
896
896
897 def _append_block(self, block_format=None, before_prompt=False):
897 def _append_block(self, block_format=None, before_prompt=False):
898 """ Appends an new QTextBlock to the end of the console buffer.
898 """ Appends an new QTextBlock to the end of the console buffer.
899 """
899 """
900 self._append_custom(self._insert_block, block_format, before_prompt)
900 self._append_custom(self._insert_block, block_format, before_prompt)
901
901
902 def _append_html(self, html, before_prompt=False):
902 def _append_html(self, html, before_prompt=False):
903 """ Appends HTML at the end of the console buffer.
903 """ Appends HTML at the end of the console buffer.
904 """
904 """
905 self._append_custom(self._insert_html, html, before_prompt)
905 self._append_custom(self._insert_html, html, before_prompt)
906
906
907 def _append_html_fetching_plain_text(self, html, before_prompt=False):
907 def _append_html_fetching_plain_text(self, html, before_prompt=False):
908 """ Appends HTML, then returns the plain text version of it.
908 """ Appends HTML, then returns the plain text version of it.
909 """
909 """
910 return self._append_custom(self._insert_html_fetching_plain_text,
910 return self._append_custom(self._insert_html_fetching_plain_text,
911 html, before_prompt)
911 html, before_prompt)
912
912
913 def _append_plain_text(self, text, before_prompt=False):
913 def _append_plain_text(self, text, before_prompt=False):
914 """ Appends plain text, processing ANSI codes if enabled.
914 """ Appends plain text, processing ANSI codes if enabled.
915 """
915 """
916 self._append_custom(self._insert_plain_text, text, before_prompt)
916 self._append_custom(self._insert_plain_text, text, before_prompt)
917
917
918 def _cancel_completion(self):
918 def _cancel_completion(self):
919 """ If text completion is progress, cancel it.
919 """ If text completion is progress, cancel it.
920 """
920 """
921 self._completion_widget.cancel_completion()
921 self._completion_widget.cancel_completion()
922
922
923 def _clear_temporary_buffer(self):
923 def _clear_temporary_buffer(self):
924 """ Clears the "temporary text" buffer, i.e. all the text following
924 """ Clears the "temporary text" buffer, i.e. all the text following
925 the prompt region.
925 the prompt region.
926 """
926 """
927 # Select and remove all text below the input buffer.
927 # Select and remove all text below the input buffer.
928 cursor = self._get_prompt_cursor()
928 cursor = self._get_prompt_cursor()
929 prompt = self._continuation_prompt.lstrip()
929 prompt = self._continuation_prompt.lstrip()
930 if(self._temp_buffer_filled):
930 if(self._temp_buffer_filled):
931 self._temp_buffer_filled = False
931 self._temp_buffer_filled = False
932 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
932 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
933 temp_cursor = QtGui.QTextCursor(cursor)
933 temp_cursor = QtGui.QTextCursor(cursor)
934 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
934 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
935 text = temp_cursor.selection().toPlainText().lstrip()
935 text = temp_cursor.selection().toPlainText().lstrip()
936 if not text.startswith(prompt):
936 if not text.startswith(prompt):
937 break
937 break
938 else:
938 else:
939 # We've reached the end of the input buffer and no text follows.
939 # We've reached the end of the input buffer and no text follows.
940 return
940 return
941 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
941 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
942 cursor.movePosition(QtGui.QTextCursor.End,
942 cursor.movePosition(QtGui.QTextCursor.End,
943 QtGui.QTextCursor.KeepAnchor)
943 QtGui.QTextCursor.KeepAnchor)
944 cursor.removeSelectedText()
944 cursor.removeSelectedText()
945
945
946 # After doing this, we have no choice but to clear the undo/redo
946 # After doing this, we have no choice but to clear the undo/redo
947 # history. Otherwise, the text is not "temporary" at all, because it
947 # history. Otherwise, the text is not "temporary" at all, because it
948 # can be recalled with undo/redo. Unfortunately, Qt does not expose
948 # can be recalled with undo/redo. Unfortunately, Qt does not expose
949 # fine-grained control to the undo/redo system.
949 # fine-grained control to the undo/redo system.
950 if self._control.isUndoRedoEnabled():
950 if self._control.isUndoRedoEnabled():
951 self._control.setUndoRedoEnabled(False)
951 self._control.setUndoRedoEnabled(False)
952 self._control.setUndoRedoEnabled(True)
952 self._control.setUndoRedoEnabled(True)
953
953
954 def _complete_with_items(self, cursor, items):
954 def _complete_with_items(self, cursor, items):
955 """ Performs completion with 'items' at the specified cursor location.
955 """ Performs completion with 'items' at the specified cursor location.
956 """
956 """
957 self._cancel_completion()
957 self._cancel_completion()
958
958
959 if len(items) == 1:
959 if len(items) == 1:
960 cursor.setPosition(self._control.textCursor().position(),
960 cursor.setPosition(self._control.textCursor().position(),
961 QtGui.QTextCursor.KeepAnchor)
961 QtGui.QTextCursor.KeepAnchor)
962 cursor.insertText(items[0])
962 cursor.insertText(items[0])
963
963
964 elif len(items) > 1:
964 elif len(items) > 1:
965 current_pos = self._control.textCursor().position()
965 current_pos = self._control.textCursor().position()
966 prefix = commonprefix(items)
966 prefix = commonprefix(items)
967 if prefix:
967 if prefix:
968 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
968 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
969 cursor.insertText(prefix)
969 cursor.insertText(prefix)
970 current_pos = cursor.position()
970 current_pos = cursor.position()
971
971
972 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
972 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
973 self._completion_widget.show_items(cursor, items)
973 self._completion_widget.show_items(cursor, items)
974
974
975
975
976 def _fill_temporary_buffer(self, cursor, text, html=False):
976 def _fill_temporary_buffer(self, cursor, text, html=False):
977 """fill the area below the active editting zone with text"""
977 """fill the area below the active editting zone with text"""
978
978
979 current_pos = self._control.textCursor().position()
979 current_pos = self._control.textCursor().position()
980
980
981 cursor.beginEditBlock()
981 cursor.beginEditBlock()
982 self._append_plain_text('\n')
982 self._append_plain_text('\n')
983 self._page(text, html=html)
983 self._page(text, html=html)
984 cursor.endEditBlock()
984 cursor.endEditBlock()
985
985
986 cursor.setPosition(current_pos)
986 cursor.setPosition(current_pos)
987 self._control.moveCursor(QtGui.QTextCursor.End)
987 self._control.moveCursor(QtGui.QTextCursor.End)
988 self._control.setTextCursor(cursor)
988 self._control.setTextCursor(cursor)
989
989
990 self._temp_buffer_filled = True
990 self._temp_buffer_filled = True
991
991
992
992
993 def _context_menu_make(self, pos):
993 def _context_menu_make(self, pos):
994 """ Creates a context menu for the given QPoint (in widget coordinates).
994 """ Creates a context menu for the given QPoint (in widget coordinates).
995 """
995 """
996 menu = QtGui.QMenu(self)
996 menu = QtGui.QMenu(self)
997
997
998 self.cut_action = menu.addAction('Cut', self.cut)
998 self.cut_action = menu.addAction('Cut', self.cut)
999 self.cut_action.setEnabled(self.can_cut())
999 self.cut_action.setEnabled(self.can_cut())
1000 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1000 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1001
1001
1002 self.copy_action = menu.addAction('Copy', self.copy)
1002 self.copy_action = menu.addAction('Copy', self.copy)
1003 self.copy_action.setEnabled(self.can_copy())
1003 self.copy_action.setEnabled(self.can_copy())
1004 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1004 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1005
1005
1006 self.paste_action = menu.addAction('Paste', self.paste)
1006 self.paste_action = menu.addAction('Paste', self.paste)
1007 self.paste_action.setEnabled(self.can_paste())
1007 self.paste_action.setEnabled(self.can_paste())
1008 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1008 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1009
1009
1010 anchor = self._control.anchorAt(pos)
1010 anchor = self._control.anchorAt(pos)
1011 if anchor:
1011 if anchor:
1012 menu.addSeparator()
1012 menu.addSeparator()
1013 self.copy_link_action = menu.addAction(
1013 self.copy_link_action = menu.addAction(
1014 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1014 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1015 self.open_link_action = menu.addAction(
1015 self.open_link_action = menu.addAction(
1016 'Open Link', lambda: self.open_anchor(anchor=anchor))
1016 'Open Link', lambda: self.open_anchor(anchor=anchor))
1017
1017
1018 menu.addSeparator()
1018 menu.addSeparator()
1019 menu.addAction(self.select_all_action)
1019 menu.addAction(self.select_all_action)
1020
1020
1021 menu.addSeparator()
1021 menu.addSeparator()
1022 menu.addAction(self.export_action)
1022 menu.addAction(self.export_action)
1023 menu.addAction(self.print_action)
1023 menu.addAction(self.print_action)
1024
1024
1025 return menu
1025 return menu
1026
1026
1027 def _control_key_down(self, modifiers, include_command=False):
1027 def _control_key_down(self, modifiers, include_command=False):
1028 """ Given a KeyboardModifiers flags object, return whether the Control
1028 """ Given a KeyboardModifiers flags object, return whether the Control
1029 key is down.
1029 key is down.
1030
1030
1031 Parameters:
1031 Parameters:
1032 -----------
1032 -----------
1033 include_command : bool, optional (default True)
1033 include_command : bool, optional (default True)
1034 Whether to treat the Command key as a (mutually exclusive) synonym
1034 Whether to treat the Command key as a (mutually exclusive) synonym
1035 for Control when in Mac OS.
1035 for Control when in Mac OS.
1036 """
1036 """
1037 # Note that on Mac OS, ControlModifier corresponds to the Command key
1037 # Note that on Mac OS, ControlModifier corresponds to the Command key
1038 # while MetaModifier corresponds to the Control key.
1038 # while MetaModifier corresponds to the Control key.
1039 if sys.platform == 'darwin':
1039 if sys.platform == 'darwin':
1040 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1040 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1041 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1041 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1042 else:
1042 else:
1043 return bool(modifiers & QtCore.Qt.ControlModifier)
1043 return bool(modifiers & QtCore.Qt.ControlModifier)
1044
1044
1045 def _create_control(self):
1045 def _create_control(self):
1046 """ Creates and connects the underlying text widget.
1046 """ Creates and connects the underlying text widget.
1047 """
1047 """
1048 # Create the underlying control.
1048 # Create the underlying control.
1049 if self.custom_control:
1049 if self.custom_control:
1050 control = self.custom_control()
1050 control = self.custom_control()
1051 elif self.kind == 'plain':
1051 elif self.kind == 'plain':
1052 control = QtGui.QPlainTextEdit()
1052 control = QtGui.QPlainTextEdit()
1053 elif self.kind == 'rich':
1053 elif self.kind == 'rich':
1054 control = QtGui.QTextEdit()
1054 control = QtGui.QTextEdit()
1055 control.setAcceptRichText(False)
1055 control.setAcceptRichText(False)
1056 control.setMouseTracking(True)
1056 control.setMouseTracking(True)
1057
1057
1058 # Prevent the widget from handling drops, as we already provide
1058 # Prevent the widget from handling drops, as we already provide
1059 # the logic in this class.
1059 # the logic in this class.
1060 control.setAcceptDrops(False)
1060 control.setAcceptDrops(False)
1061
1061
1062 # Install event filters. The filter on the viewport is needed for
1062 # Install event filters. The filter on the viewport is needed for
1063 # mouse events.
1063 # mouse events.
1064 control.installEventFilter(self)
1064 control.installEventFilter(self)
1065 control.viewport().installEventFilter(self)
1065 control.viewport().installEventFilter(self)
1066
1066
1067 # Connect signals.
1067 # Connect signals.
1068 control.customContextMenuRequested.connect(
1068 control.customContextMenuRequested.connect(
1069 self._custom_context_menu_requested)
1069 self._custom_context_menu_requested)
1070 control.copyAvailable.connect(self.copy_available)
1070 control.copyAvailable.connect(self.copy_available)
1071 control.redoAvailable.connect(self.redo_available)
1071 control.redoAvailable.connect(self.redo_available)
1072 control.undoAvailable.connect(self.undo_available)
1072 control.undoAvailable.connect(self.undo_available)
1073
1073
1074 # Hijack the document size change signal to prevent Qt from adjusting
1074 # Hijack the document size change signal to prevent Qt from adjusting
1075 # the viewport's scrollbar. We are relying on an implementation detail
1075 # the viewport's scrollbar. We are relying on an implementation detail
1076 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1076 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1077 # this functionality we cannot create a nice terminal interface.
1077 # this functionality we cannot create a nice terminal interface.
1078 layout = control.document().documentLayout()
1078 layout = control.document().documentLayout()
1079 layout.documentSizeChanged.disconnect()
1079 layout.documentSizeChanged.disconnect()
1080 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1080 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1081
1081
1082 # Configure the control.
1082 # Configure the control.
1083 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1083 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1084 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1084 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1085 control.setReadOnly(True)
1085 control.setReadOnly(True)
1086 control.setUndoRedoEnabled(False)
1086 control.setUndoRedoEnabled(False)
1087 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1087 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1088 return control
1088 return control
1089
1089
1090 def _create_page_control(self):
1090 def _create_page_control(self):
1091 """ Creates and connects the underlying paging widget.
1091 """ Creates and connects the underlying paging widget.
1092 """
1092 """
1093 if self.custom_page_control:
1093 if self.custom_page_control:
1094 control = self.custom_page_control()
1094 control = self.custom_page_control()
1095 elif self.kind == 'plain':
1095 elif self.kind == 'plain':
1096 control = QtGui.QPlainTextEdit()
1096 control = QtGui.QPlainTextEdit()
1097 elif self.kind == 'rich':
1097 elif self.kind == 'rich':
1098 control = QtGui.QTextEdit()
1098 control = QtGui.QTextEdit()
1099 control.installEventFilter(self)
1099 control.installEventFilter(self)
1100 viewport = control.viewport()
1100 viewport = control.viewport()
1101 viewport.installEventFilter(self)
1101 viewport.installEventFilter(self)
1102 control.setReadOnly(True)
1102 control.setReadOnly(True)
1103 control.setUndoRedoEnabled(False)
1103 control.setUndoRedoEnabled(False)
1104 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1104 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1105 return control
1105 return control
1106
1106
1107 def _event_filter_console_keypress(self, event):
1107 def _event_filter_console_keypress(self, event):
1108 """ Filter key events for the underlying text widget to create a
1108 """ Filter key events for the underlying text widget to create a
1109 console-like interface.
1109 console-like interface.
1110 """
1110 """
1111 intercepted = False
1111 intercepted = False
1112 cursor = self._control.textCursor()
1112 cursor = self._control.textCursor()
1113 position = cursor.position()
1113 position = cursor.position()
1114 key = event.key()
1114 key = event.key()
1115 ctrl_down = self._control_key_down(event.modifiers())
1115 ctrl_down = self._control_key_down(event.modifiers())
1116 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1116 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1117 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1117 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1118
1118
1119 #------ Special sequences ----------------------------------------------
1119 #------ Special sequences ----------------------------------------------
1120
1120
1121 if event.matches(QtGui.QKeySequence.Copy):
1121 if event.matches(QtGui.QKeySequence.Copy):
1122 self.copy()
1122 self.copy()
1123 intercepted = True
1123 intercepted = True
1124
1124
1125 elif event.matches(QtGui.QKeySequence.Cut):
1125 elif event.matches(QtGui.QKeySequence.Cut):
1126 self.cut()
1126 self.cut()
1127 intercepted = True
1127 intercepted = True
1128
1128
1129 elif event.matches(QtGui.QKeySequence.Paste):
1129 elif event.matches(QtGui.QKeySequence.Paste):
1130 self.paste()
1130 self.paste()
1131 intercepted = True
1131 intercepted = True
1132
1132
1133 #------ Special modifier logic -----------------------------------------
1133 #------ Special modifier logic -----------------------------------------
1134
1134
1135 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1135 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1136 intercepted = True
1136 intercepted = True
1137
1137
1138 # Special handling when tab completing in text mode.
1138 # Special handling when tab completing in text mode.
1139 self._cancel_completion()
1139 self._cancel_completion()
1140
1140
1141 if self._in_buffer(position):
1141 if self._in_buffer(position):
1142 # Special handling when a reading a line of raw input.
1142 # Special handling when a reading a line of raw input.
1143 if self._reading:
1143 if self._reading:
1144 self._append_plain_text('\n')
1144 self._append_plain_text('\n')
1145 self._reading = False
1145 self._reading = False
1146 if self._reading_callback:
1146 if self._reading_callback:
1147 self._reading_callback()
1147 self._reading_callback()
1148
1148
1149 # If the input buffer is a single line or there is only
1149 # If the input buffer is a single line or there is only
1150 # whitespace after the cursor, execute. Otherwise, split the
1150 # whitespace after the cursor, execute. Otherwise, split the
1151 # line with a continuation prompt.
1151 # line with a continuation prompt.
1152 elif not self._executing:
1152 elif not self._executing:
1153 cursor.movePosition(QtGui.QTextCursor.End,
1153 cursor.movePosition(QtGui.QTextCursor.End,
1154 QtGui.QTextCursor.KeepAnchor)
1154 QtGui.QTextCursor.KeepAnchor)
1155 at_end = len(cursor.selectedText().strip()) == 0
1155 at_end = len(cursor.selectedText().strip()) == 0
1156 single_line = (self._get_end_cursor().blockNumber() ==
1156 single_line = (self._get_end_cursor().blockNumber() ==
1157 self._get_prompt_cursor().blockNumber())
1157 self._get_prompt_cursor().blockNumber())
1158 if (at_end or shift_down or single_line) and not ctrl_down:
1158 if (at_end or shift_down or single_line) and not ctrl_down:
1159 self.execute(interactive = not shift_down)
1159 self.execute(interactive = not shift_down)
1160 else:
1160 else:
1161 # Do this inside an edit block for clean undo/redo.
1161 # Do this inside an edit block for clean undo/redo.
1162 cursor.beginEditBlock()
1162 cursor.beginEditBlock()
1163 cursor.setPosition(position)
1163 cursor.setPosition(position)
1164 cursor.insertText('\n')
1164 cursor.insertText('\n')
1165 self._insert_continuation_prompt(cursor)
1165 self._insert_continuation_prompt(cursor)
1166 cursor.endEditBlock()
1166 cursor.endEditBlock()
1167
1167
1168 # Ensure that the whole input buffer is visible.
1168 # Ensure that the whole input buffer is visible.
1169 # FIXME: This will not be usable if the input buffer is
1169 # FIXME: This will not be usable if the input buffer is
1170 # taller than the console widget.
1170 # taller than the console widget.
1171 self._control.moveCursor(QtGui.QTextCursor.End)
1171 self._control.moveCursor(QtGui.QTextCursor.End)
1172 self._control.setTextCursor(cursor)
1172 self._control.setTextCursor(cursor)
1173
1173
1174 #------ Control/Cmd modifier -------------------------------------------
1174 #------ Control/Cmd modifier -------------------------------------------
1175
1175
1176 elif ctrl_down:
1176 elif ctrl_down:
1177 if key == QtCore.Qt.Key_G:
1177 if key == QtCore.Qt.Key_G:
1178 self._keyboard_quit()
1178 self._keyboard_quit()
1179 intercepted = True
1179 intercepted = True
1180
1180
1181 elif key == QtCore.Qt.Key_K:
1181 elif key == QtCore.Qt.Key_K:
1182 if self._in_buffer(position):
1182 if self._in_buffer(position):
1183 cursor.clearSelection()
1183 cursor.clearSelection()
1184 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1184 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1185 QtGui.QTextCursor.KeepAnchor)
1185 QtGui.QTextCursor.KeepAnchor)
1186 if not cursor.hasSelection():
1186 if not cursor.hasSelection():
1187 # Line deletion (remove continuation prompt)
1187 # Line deletion (remove continuation prompt)
1188 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1188 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1189 QtGui.QTextCursor.KeepAnchor)
1189 QtGui.QTextCursor.KeepAnchor)
1190 cursor.movePosition(QtGui.QTextCursor.Right,
1190 cursor.movePosition(QtGui.QTextCursor.Right,
1191 QtGui.QTextCursor.KeepAnchor,
1191 QtGui.QTextCursor.KeepAnchor,
1192 len(self._continuation_prompt))
1192 len(self._continuation_prompt))
1193 self._kill_ring.kill_cursor(cursor)
1193 self._kill_ring.kill_cursor(cursor)
1194 self._set_cursor(cursor)
1194 self._set_cursor(cursor)
1195 intercepted = True
1195 intercepted = True
1196
1196
1197 elif key == QtCore.Qt.Key_L:
1197 elif key == QtCore.Qt.Key_L:
1198 self.prompt_to_top()
1198 self.prompt_to_top()
1199 intercepted = True
1199 intercepted = True
1200
1200
1201 elif key == QtCore.Qt.Key_O:
1201 elif key == QtCore.Qt.Key_O:
1202 if self._page_control and self._page_control.isVisible():
1202 if self._page_control and self._page_control.isVisible():
1203 self._page_control.setFocus()
1203 self._page_control.setFocus()
1204 intercepted = True
1204 intercepted = True
1205
1205
1206 elif key == QtCore.Qt.Key_U:
1206 elif key == QtCore.Qt.Key_U:
1207 if self._in_buffer(position):
1207 if self._in_buffer(position):
1208 cursor.clearSelection()
1208 cursor.clearSelection()
1209 start_line = cursor.blockNumber()
1209 start_line = cursor.blockNumber()
1210 if start_line == self._get_prompt_cursor().blockNumber():
1210 if start_line == self._get_prompt_cursor().blockNumber():
1211 offset = len(self._prompt)
1211 offset = len(self._prompt)
1212 else:
1212 else:
1213 offset = len(self._continuation_prompt)
1213 offset = len(self._continuation_prompt)
1214 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1214 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1215 QtGui.QTextCursor.KeepAnchor)
1215 QtGui.QTextCursor.KeepAnchor)
1216 cursor.movePosition(QtGui.QTextCursor.Right,
1216 cursor.movePosition(QtGui.QTextCursor.Right,
1217 QtGui.QTextCursor.KeepAnchor, offset)
1217 QtGui.QTextCursor.KeepAnchor, offset)
1218 self._kill_ring.kill_cursor(cursor)
1218 self._kill_ring.kill_cursor(cursor)
1219 self._set_cursor(cursor)
1219 self._set_cursor(cursor)
1220 intercepted = True
1220 intercepted = True
1221
1221
1222 elif key == QtCore.Qt.Key_Y:
1222 elif key == QtCore.Qt.Key_Y:
1223 self._keep_cursor_in_buffer()
1223 self._keep_cursor_in_buffer()
1224 self._kill_ring.yank()
1224 self._kill_ring.yank()
1225 intercepted = True
1225 intercepted = True
1226
1226
1227 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1227 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1228 if key == QtCore.Qt.Key_Backspace:
1228 if key == QtCore.Qt.Key_Backspace:
1229 cursor = self._get_word_start_cursor(position)
1229 cursor = self._get_word_start_cursor(position)
1230 else: # key == QtCore.Qt.Key_Delete
1230 else: # key == QtCore.Qt.Key_Delete
1231 cursor = self._get_word_end_cursor(position)
1231 cursor = self._get_word_end_cursor(position)
1232 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1232 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1233 self._kill_ring.kill_cursor(cursor)
1233 self._kill_ring.kill_cursor(cursor)
1234 intercepted = True
1234 intercepted = True
1235
1235
1236 elif key == QtCore.Qt.Key_D:
1236 elif key == QtCore.Qt.Key_D:
1237 if len(self.input_buffer) == 0:
1237 if len(self.input_buffer) == 0:
1238 self.exit_requested.emit(self)
1238 self.exit_requested.emit(self)
1239 else:
1239 else:
1240 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1240 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1241 QtCore.Qt.Key_Delete,
1241 QtCore.Qt.Key_Delete,
1242 QtCore.Qt.NoModifier)
1242 QtCore.Qt.NoModifier)
1243 QtGui.qApp.sendEvent(self._control, new_event)
1243 QtGui.qApp.sendEvent(self._control, new_event)
1244 intercepted = True
1244 intercepted = True
1245
1245
1246 #------ Alt modifier ---------------------------------------------------
1246 #------ Alt modifier ---------------------------------------------------
1247
1247
1248 elif alt_down:
1248 elif alt_down:
1249 if key == QtCore.Qt.Key_B:
1249 if key == QtCore.Qt.Key_B:
1250 self._set_cursor(self._get_word_start_cursor(position))
1250 self._set_cursor(self._get_word_start_cursor(position))
1251 intercepted = True
1251 intercepted = True
1252
1252
1253 elif key == QtCore.Qt.Key_F:
1253 elif key == QtCore.Qt.Key_F:
1254 self._set_cursor(self._get_word_end_cursor(position))
1254 self._set_cursor(self._get_word_end_cursor(position))
1255 intercepted = True
1255 intercepted = True
1256
1256
1257 elif key == QtCore.Qt.Key_Y:
1257 elif key == QtCore.Qt.Key_Y:
1258 self._kill_ring.rotate()
1258 self._kill_ring.rotate()
1259 intercepted = True
1259 intercepted = True
1260
1260
1261 elif key == QtCore.Qt.Key_Backspace:
1261 elif key == QtCore.Qt.Key_Backspace:
1262 cursor = self._get_word_start_cursor(position)
1262 cursor = self._get_word_start_cursor(position)
1263 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1263 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1264 self._kill_ring.kill_cursor(cursor)
1264 self._kill_ring.kill_cursor(cursor)
1265 intercepted = True
1265 intercepted = True
1266
1266
1267 elif key == QtCore.Qt.Key_D:
1267 elif key == QtCore.Qt.Key_D:
1268 cursor = self._get_word_end_cursor(position)
1268 cursor = self._get_word_end_cursor(position)
1269 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1269 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1270 self._kill_ring.kill_cursor(cursor)
1270 self._kill_ring.kill_cursor(cursor)
1271 intercepted = True
1271 intercepted = True
1272
1272
1273 elif key == QtCore.Qt.Key_Delete:
1273 elif key == QtCore.Qt.Key_Delete:
1274 intercepted = True
1274 intercepted = True
1275
1275
1276 elif key == QtCore.Qt.Key_Greater:
1276 elif key == QtCore.Qt.Key_Greater:
1277 self._control.moveCursor(QtGui.QTextCursor.End)
1277 self._control.moveCursor(QtGui.QTextCursor.End)
1278 intercepted = True
1278 intercepted = True
1279
1279
1280 elif key == QtCore.Qt.Key_Less:
1280 elif key == QtCore.Qt.Key_Less:
1281 self._control.setTextCursor(self._get_prompt_cursor())
1281 self._control.setTextCursor(self._get_prompt_cursor())
1282 intercepted = True
1282 intercepted = True
1283
1283
1284 #------ No modifiers ---------------------------------------------------
1284 #------ No modifiers ---------------------------------------------------
1285
1285
1286 else:
1286 else:
1287 if shift_down:
1287 if shift_down:
1288 anchormode = QtGui.QTextCursor.KeepAnchor
1288 anchormode = QtGui.QTextCursor.KeepAnchor
1289 else:
1289 else:
1290 anchormode = QtGui.QTextCursor.MoveAnchor
1290 anchormode = QtGui.QTextCursor.MoveAnchor
1291
1291
1292 if key == QtCore.Qt.Key_Escape:
1292 if key == QtCore.Qt.Key_Escape:
1293 self._keyboard_quit()
1293 self._keyboard_quit()
1294 intercepted = True
1294 intercepted = True
1295
1295
1296 elif key == QtCore.Qt.Key_Up:
1296 elif key == QtCore.Qt.Key_Up:
1297 if self._reading or not self._up_pressed(shift_down):
1297 if self._reading or not self._up_pressed(shift_down):
1298 intercepted = True
1298 intercepted = True
1299 else:
1299 else:
1300 prompt_line = self._get_prompt_cursor().blockNumber()
1300 prompt_line = self._get_prompt_cursor().blockNumber()
1301 intercepted = cursor.blockNumber() <= prompt_line
1301 intercepted = cursor.blockNumber() <= prompt_line
1302
1302
1303 elif key == QtCore.Qt.Key_Down:
1303 elif key == QtCore.Qt.Key_Down:
1304 if self._reading or not self._down_pressed(shift_down):
1304 if self._reading or not self._down_pressed(shift_down):
1305 intercepted = True
1305 intercepted = True
1306 else:
1306 else:
1307 end_line = self._get_end_cursor().blockNumber()
1307 end_line = self._get_end_cursor().blockNumber()
1308 intercepted = cursor.blockNumber() == end_line
1308 intercepted = cursor.blockNumber() == end_line
1309
1309
1310 elif key == QtCore.Qt.Key_Tab:
1310 elif key == QtCore.Qt.Key_Tab:
1311 if not self._reading:
1311 if not self._reading:
1312 if self._tab_pressed():
1312 if self._tab_pressed():
1313 # real tab-key, insert four spaces
1313 # real tab-key, insert four spaces
1314 cursor.insertText(' '*4)
1314 cursor.insertText(' '*4)
1315 intercepted = True
1315 intercepted = True
1316
1316
1317 elif key == QtCore.Qt.Key_Left:
1317 elif key == QtCore.Qt.Key_Left:
1318
1318
1319 # Move to the previous line
1319 # Move to the previous line
1320 line, col = cursor.blockNumber(), cursor.columnNumber()
1320 line, col = cursor.blockNumber(), cursor.columnNumber()
1321 if line > self._get_prompt_cursor().blockNumber() and \
1321 if line > self._get_prompt_cursor().blockNumber() and \
1322 col == len(self._continuation_prompt):
1322 col == len(self._continuation_prompt):
1323 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1323 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1324 mode=anchormode)
1324 mode=anchormode)
1325 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1325 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1326 mode=anchormode)
1326 mode=anchormode)
1327 intercepted = True
1327 intercepted = True
1328
1328
1329 # Regular left movement
1329 # Regular left movement
1330 else:
1330 else:
1331 intercepted = not self._in_buffer(position - 1)
1331 intercepted = not self._in_buffer(position - 1)
1332
1332
1333 elif key == QtCore.Qt.Key_Right:
1333 elif key == QtCore.Qt.Key_Right:
1334 original_block_number = cursor.blockNumber()
1334 original_block_number = cursor.blockNumber()
1335 cursor.movePosition(QtGui.QTextCursor.Right,
1335 cursor.movePosition(QtGui.QTextCursor.Right,
1336 mode=anchormode)
1336 mode=anchormode)
1337 if cursor.blockNumber() != original_block_number:
1337 if cursor.blockNumber() != original_block_number:
1338 cursor.movePosition(QtGui.QTextCursor.Right,
1338 cursor.movePosition(QtGui.QTextCursor.Right,
1339 n=len(self._continuation_prompt),
1339 n=len(self._continuation_prompt),
1340 mode=anchormode)
1340 mode=anchormode)
1341 self._set_cursor(cursor)
1341 self._set_cursor(cursor)
1342 intercepted = True
1342 intercepted = True
1343
1343
1344 elif key == QtCore.Qt.Key_Home:
1344 elif key == QtCore.Qt.Key_Home:
1345 start_line = cursor.blockNumber()
1345 start_line = cursor.blockNumber()
1346 if start_line == self._get_prompt_cursor().blockNumber():
1346 if start_line == self._get_prompt_cursor().blockNumber():
1347 start_pos = self._prompt_pos
1347 start_pos = self._prompt_pos
1348 else:
1348 else:
1349 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1349 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1350 QtGui.QTextCursor.KeepAnchor)
1350 QtGui.QTextCursor.KeepAnchor)
1351 start_pos = cursor.position()
1351 start_pos = cursor.position()
1352 start_pos += len(self._continuation_prompt)
1352 start_pos += len(self._continuation_prompt)
1353 cursor.setPosition(position)
1353 cursor.setPosition(position)
1354 if shift_down and self._in_buffer(position):
1354 if shift_down and self._in_buffer(position):
1355 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1355 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1356 else:
1356 else:
1357 cursor.setPosition(start_pos)
1357 cursor.setPosition(start_pos)
1358 self._set_cursor(cursor)
1358 self._set_cursor(cursor)
1359 intercepted = True
1359 intercepted = True
1360
1360
1361 elif key == QtCore.Qt.Key_Backspace:
1361 elif key == QtCore.Qt.Key_Backspace:
1362
1362
1363 # Line deletion (remove continuation prompt)
1363 # Line deletion (remove continuation prompt)
1364 line, col = cursor.blockNumber(), cursor.columnNumber()
1364 line, col = cursor.blockNumber(), cursor.columnNumber()
1365 if not self._reading and \
1365 if not self._reading and \
1366 col == len(self._continuation_prompt) and \
1366 col == len(self._continuation_prompt) and \
1367 line > self._get_prompt_cursor().blockNumber():
1367 line > self._get_prompt_cursor().blockNumber():
1368 cursor.beginEditBlock()
1368 cursor.beginEditBlock()
1369 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1369 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1370 QtGui.QTextCursor.KeepAnchor)
1370 QtGui.QTextCursor.KeepAnchor)
1371 cursor.removeSelectedText()
1371 cursor.removeSelectedText()
1372 cursor.deletePreviousChar()
1372 cursor.deletePreviousChar()
1373 cursor.endEditBlock()
1373 cursor.endEditBlock()
1374 intercepted = True
1374 intercepted = True
1375
1375
1376 # Regular backwards deletion
1376 # Regular backwards deletion
1377 else:
1377 else:
1378 anchor = cursor.anchor()
1378 anchor = cursor.anchor()
1379 if anchor == position:
1379 if anchor == position:
1380 intercepted = not self._in_buffer(position - 1)
1380 intercepted = not self._in_buffer(position - 1)
1381 else:
1381 else:
1382 intercepted = not self._in_buffer(min(anchor, position))
1382 intercepted = not self._in_buffer(min(anchor, position))
1383
1383
1384 elif key == QtCore.Qt.Key_Delete:
1384 elif key == QtCore.Qt.Key_Delete:
1385
1385
1386 # Line deletion (remove continuation prompt)
1386 # Line deletion (remove continuation prompt)
1387 if not self._reading and self._in_buffer(position) and \
1387 if not self._reading and self._in_buffer(position) and \
1388 cursor.atBlockEnd() and not cursor.hasSelection():
1388 cursor.atBlockEnd() and not cursor.hasSelection():
1389 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1389 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1390 QtGui.QTextCursor.KeepAnchor)
1390 QtGui.QTextCursor.KeepAnchor)
1391 cursor.movePosition(QtGui.QTextCursor.Right,
1391 cursor.movePosition(QtGui.QTextCursor.Right,
1392 QtGui.QTextCursor.KeepAnchor,
1392 QtGui.QTextCursor.KeepAnchor,
1393 len(self._continuation_prompt))
1393 len(self._continuation_prompt))
1394 cursor.removeSelectedText()
1394 cursor.removeSelectedText()
1395 intercepted = True
1395 intercepted = True
1396
1396
1397 # Regular forwards deletion:
1397 # Regular forwards deletion:
1398 else:
1398 else:
1399 anchor = cursor.anchor()
1399 anchor = cursor.anchor()
1400 intercepted = (not self._in_buffer(anchor) or
1400 intercepted = (not self._in_buffer(anchor) or
1401 not self._in_buffer(position))
1401 not self._in_buffer(position))
1402
1402
1403 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1403 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1404 # using the keyboard in any part of the buffer. Also, permit scrolling
1404 # using the keyboard in any part of the buffer. Also, permit scrolling
1405 # with Page Up/Down keys. Finally, if we're executing, don't move the
1405 # with Page Up/Down keys. Finally, if we're executing, don't move the
1406 # cursor (if even this made sense, we can't guarantee that the prompt
1406 # cursor (if even this made sense, we can't guarantee that the prompt
1407 # position is still valid due to text truncation).
1407 # position is still valid due to text truncation).
1408 if not (self._control_key_down(event.modifiers(), include_command=True)
1408 if not (self._control_key_down(event.modifiers(), include_command=True)
1409 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1409 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1410 or (self._executing and not self._reading)):
1410 or (self._executing and not self._reading)):
1411 self._keep_cursor_in_buffer()
1411 self._keep_cursor_in_buffer()
1412
1412
1413 return intercepted
1413 return intercepted
1414
1414
1415 def _event_filter_page_keypress(self, event):
1415 def _event_filter_page_keypress(self, event):
1416 """ Filter key events for the paging widget to create console-like
1416 """ Filter key events for the paging widget to create console-like
1417 interface.
1417 interface.
1418 """
1418 """
1419 key = event.key()
1419 key = event.key()
1420 ctrl_down = self._control_key_down(event.modifiers())
1420 ctrl_down = self._control_key_down(event.modifiers())
1421 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1421 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1422
1422
1423 if ctrl_down:
1423 if ctrl_down:
1424 if key == QtCore.Qt.Key_O:
1424 if key == QtCore.Qt.Key_O:
1425 self._control.setFocus()
1425 self._control.setFocus()
1426 intercept = True
1426 intercept = True
1427
1427
1428 elif alt_down:
1428 elif alt_down:
1429 if key == QtCore.Qt.Key_Greater:
1429 if key == QtCore.Qt.Key_Greater:
1430 self._page_control.moveCursor(QtGui.QTextCursor.End)
1430 self._page_control.moveCursor(QtGui.QTextCursor.End)
1431 intercepted = True
1431 intercepted = True
1432
1432
1433 elif key == QtCore.Qt.Key_Less:
1433 elif key == QtCore.Qt.Key_Less:
1434 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1434 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1435 intercepted = True
1435 intercepted = True
1436
1436
1437 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1437 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1438 if self._splitter:
1438 if self._splitter:
1439 self._page_control.hide()
1439 self._page_control.hide()
1440 self._control.setFocus()
1440 self._control.setFocus()
1441 else:
1441 else:
1442 self.layout().setCurrentWidget(self._control)
1442 self.layout().setCurrentWidget(self._control)
1443 return True
1443 return True
1444
1444
1445 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1445 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1446 QtCore.Qt.Key_Tab):
1446 QtCore.Qt.Key_Tab):
1447 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1447 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1448 QtCore.Qt.Key_PageDown,
1448 QtCore.Qt.Key_PageDown,
1449 QtCore.Qt.NoModifier)
1449 QtCore.Qt.NoModifier)
1450 QtGui.qApp.sendEvent(self._page_control, new_event)
1450 QtGui.qApp.sendEvent(self._page_control, new_event)
1451 return True
1451 return True
1452
1452
1453 elif key == QtCore.Qt.Key_Backspace:
1453 elif key == QtCore.Qt.Key_Backspace:
1454 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1454 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1455 QtCore.Qt.Key_PageUp,
1455 QtCore.Qt.Key_PageUp,
1456 QtCore.Qt.NoModifier)
1456 QtCore.Qt.NoModifier)
1457 QtGui.qApp.sendEvent(self._page_control, new_event)
1457 QtGui.qApp.sendEvent(self._page_control, new_event)
1458 return True
1458 return True
1459
1459
1460 return False
1460 return False
1461
1461
1462 def _format_as_columns(self, items, separator=' '):
1462 def _format_as_columns(self, items, separator=' '):
1463 """ Transform a list of strings into a single string with columns.
1463 """ Transform a list of strings into a single string with columns.
1464
1464
1465 Parameters
1465 Parameters
1466 ----------
1466 ----------
1467 items : sequence of strings
1467 items : sequence of strings
1468 The strings to process.
1468 The strings to process.
1469
1469
1470 separator : str, optional [default is two spaces]
1470 separator : str, optional [default is two spaces]
1471 The string that separates columns.
1471 The string that separates columns.
1472
1472
1473 Returns
1473 Returns
1474 -------
1474 -------
1475 The formatted string.
1475 The formatted string.
1476 """
1476 """
1477 # Calculate the number of characters available.
1477 # Calculate the number of characters available.
1478 width = self._control.viewport().width()
1478 width = self._control.viewport().width()
1479 char_width = QtGui.QFontMetrics(self.font).width(' ')
1479 char_width = QtGui.QFontMetrics(self.font).width(' ')
1480 displaywidth = max(10, (width / char_width) - 1)
1480 displaywidth = max(10, (width / char_width) - 1)
1481
1481
1482 return columnize(items, separator, displaywidth)
1482 return columnize(items, separator, displaywidth)
1483
1483
1484 def _get_block_plain_text(self, block):
1484 def _get_block_plain_text(self, block):
1485 """ Given a QTextBlock, return its unformatted text.
1485 """ Given a QTextBlock, return its unformatted text.
1486 """
1486 """
1487 cursor = QtGui.QTextCursor(block)
1487 cursor = QtGui.QTextCursor(block)
1488 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1488 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1489 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1489 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1490 QtGui.QTextCursor.KeepAnchor)
1490 QtGui.QTextCursor.KeepAnchor)
1491 return cursor.selection().toPlainText()
1491 return cursor.selection().toPlainText()
1492
1492
1493 def _get_cursor(self):
1493 def _get_cursor(self):
1494 """ Convenience method that returns a cursor for the current position.
1494 """ Convenience method that returns a cursor for the current position.
1495 """
1495 """
1496 return self._control.textCursor()
1496 return self._control.textCursor()
1497
1497
1498 def _get_end_cursor(self):
1498 def _get_end_cursor(self):
1499 """ Convenience method that returns a cursor for the last character.
1499 """ Convenience method that returns a cursor for the last character.
1500 """
1500 """
1501 cursor = self._control.textCursor()
1501 cursor = self._control.textCursor()
1502 cursor.movePosition(QtGui.QTextCursor.End)
1502 cursor.movePosition(QtGui.QTextCursor.End)
1503 return cursor
1503 return cursor
1504
1504
1505 def _get_input_buffer_cursor_column(self):
1505 def _get_input_buffer_cursor_column(self):
1506 """ Returns the column of the cursor in the input buffer, excluding the
1506 """ Returns the column of the cursor in the input buffer, excluding the
1507 contribution by the prompt, or -1 if there is no such column.
1507 contribution by the prompt, or -1 if there is no such column.
1508 """
1508 """
1509 prompt = self._get_input_buffer_cursor_prompt()
1509 prompt = self._get_input_buffer_cursor_prompt()
1510 if prompt is None:
1510 if prompt is None:
1511 return -1
1511 return -1
1512 else:
1512 else:
1513 cursor = self._control.textCursor()
1513 cursor = self._control.textCursor()
1514 return cursor.columnNumber() - len(prompt)
1514 return cursor.columnNumber() - len(prompt)
1515
1515
1516 def _get_input_buffer_cursor_line(self):
1516 def _get_input_buffer_cursor_line(self):
1517 """ Returns the text of the line of the input buffer that contains the
1517 """ Returns the text of the line of the input buffer that contains the
1518 cursor, or None if there is no such line.
1518 cursor, or None if there is no such line.
1519 """
1519 """
1520 prompt = self._get_input_buffer_cursor_prompt()
1520 prompt = self._get_input_buffer_cursor_prompt()
1521 if prompt is None:
1521 if prompt is None:
1522 return None
1522 return None
1523 else:
1523 else:
1524 cursor = self._control.textCursor()
1524 cursor = self._control.textCursor()
1525 text = self._get_block_plain_text(cursor.block())
1525 text = self._get_block_plain_text(cursor.block())
1526 return text[len(prompt):]
1526 return text[len(prompt):]
1527
1527
1528 def _get_input_buffer_cursor_prompt(self):
1528 def _get_input_buffer_cursor_prompt(self):
1529 """ Returns the (plain text) prompt for line of the input buffer that
1529 """ Returns the (plain text) prompt for line of the input buffer that
1530 contains the cursor, or None if there is no such line.
1530 contains the cursor, or None if there is no such line.
1531 """
1531 """
1532 if self._executing:
1532 if self._executing:
1533 return None
1533 return None
1534 cursor = self._control.textCursor()
1534 cursor = self._control.textCursor()
1535 if cursor.position() >= self._prompt_pos:
1535 if cursor.position() >= self._prompt_pos:
1536 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1536 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1537 return self._prompt
1537 return self._prompt
1538 else:
1538 else:
1539 return self._continuation_prompt
1539 return self._continuation_prompt
1540 else:
1540 else:
1541 return None
1541 return None
1542
1542
1543 def _get_prompt_cursor(self):
1543 def _get_prompt_cursor(self):
1544 """ Convenience method that returns a cursor for the prompt position.
1544 """ Convenience method that returns a cursor for the prompt position.
1545 """
1545 """
1546 cursor = self._control.textCursor()
1546 cursor = self._control.textCursor()
1547 cursor.setPosition(self._prompt_pos)
1547 cursor.setPosition(self._prompt_pos)
1548 return cursor
1548 return cursor
1549
1549
1550 def _get_selection_cursor(self, start, end):
1550 def _get_selection_cursor(self, start, end):
1551 """ Convenience method that returns a cursor with text selected between
1551 """ Convenience method that returns a cursor with text selected between
1552 the positions 'start' and 'end'.
1552 the positions 'start' and 'end'.
1553 """
1553 """
1554 cursor = self._control.textCursor()
1554 cursor = self._control.textCursor()
1555 cursor.setPosition(start)
1555 cursor.setPosition(start)
1556 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1556 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1557 return cursor
1557 return cursor
1558
1558
1559 def _get_word_start_cursor(self, position):
1559 def _get_word_start_cursor(self, position):
1560 """ Find the start of the word to the left the given position. If a
1560 """ Find the start of the word to the left the given position. If a
1561 sequence of non-word characters precedes the first word, skip over
1561 sequence of non-word characters precedes the first word, skip over
1562 them. (This emulates the behavior of bash, emacs, etc.)
1562 them. (This emulates the behavior of bash, emacs, etc.)
1563 """
1563 """
1564 document = self._control.document()
1564 document = self._control.document()
1565 position -= 1
1565 position -= 1
1566 while position >= self._prompt_pos and \
1566 while position >= self._prompt_pos and \
1567 not is_letter_or_number(document.characterAt(position)):
1567 not is_letter_or_number(document.characterAt(position)):
1568 position -= 1
1568 position -= 1
1569 while position >= self._prompt_pos and \
1569 while position >= self._prompt_pos and \
1570 is_letter_or_number(document.characterAt(position)):
1570 is_letter_or_number(document.characterAt(position)):
1571 position -= 1
1571 position -= 1
1572 cursor = self._control.textCursor()
1572 cursor = self._control.textCursor()
1573 cursor.setPosition(position + 1)
1573 cursor.setPosition(position + 1)
1574 return cursor
1574 return cursor
1575
1575
1576 def _get_word_end_cursor(self, position):
1576 def _get_word_end_cursor(self, position):
1577 """ Find the end of the word to the right the given position. If a
1577 """ Find the end of the word to the right the given position. If a
1578 sequence of non-word characters precedes the first word, skip over
1578 sequence of non-word characters precedes the first word, skip over
1579 them. (This emulates the behavior of bash, emacs, etc.)
1579 them. (This emulates the behavior of bash, emacs, etc.)
1580 """
1580 """
1581 document = self._control.document()
1581 document = self._control.document()
1582 end = self._get_end_cursor().position()
1582 end = self._get_end_cursor().position()
1583 while position < end and \
1583 while position < end and \
1584 not is_letter_or_number(document.characterAt(position)):
1584 not is_letter_or_number(document.characterAt(position)):
1585 position += 1
1585 position += 1
1586 while position < end and \
1586 while position < end and \
1587 is_letter_or_number(document.characterAt(position)):
1587 is_letter_or_number(document.characterAt(position)):
1588 position += 1
1588 position += 1
1589 cursor = self._control.textCursor()
1589 cursor = self._control.textCursor()
1590 cursor.setPosition(position)
1590 cursor.setPosition(position)
1591 return cursor
1591 return cursor
1592
1592
1593 def _insert_continuation_prompt(self, cursor):
1593 def _insert_continuation_prompt(self, cursor):
1594 """ Inserts new continuation prompt using the specified cursor.
1594 """ Inserts new continuation prompt using the specified cursor.
1595 """
1595 """
1596 if self._continuation_prompt_html is None:
1596 if self._continuation_prompt_html is None:
1597 self._insert_plain_text(cursor, self._continuation_prompt)
1597 self._insert_plain_text(cursor, self._continuation_prompt)
1598 else:
1598 else:
1599 self._continuation_prompt = self._insert_html_fetching_plain_text(
1599 self._continuation_prompt = self._insert_html_fetching_plain_text(
1600 cursor, self._continuation_prompt_html)
1600 cursor, self._continuation_prompt_html)
1601
1601
1602 def _insert_block(self, cursor, block_format=None):
1602 def _insert_block(self, cursor, block_format=None):
1603 """ Inserts an empty QTextBlock using the specified cursor.
1603 """ Inserts an empty QTextBlock using the specified cursor.
1604 """
1604 """
1605 if block_format is None:
1605 if block_format is None:
1606 block_format = QtGui.QTextBlockFormat()
1606 block_format = QtGui.QTextBlockFormat()
1607 cursor.insertBlock(block_format)
1607 cursor.insertBlock(block_format)
1608
1608
1609 def _insert_html(self, cursor, html):
1609 def _insert_html(self, cursor, html):
1610 """ Inserts HTML using the specified cursor in such a way that future
1610 """ Inserts HTML using the specified cursor in such a way that future
1611 formatting is unaffected.
1611 formatting is unaffected.
1612 """
1612 """
1613 cursor.beginEditBlock()
1613 cursor.beginEditBlock()
1614 cursor.insertHtml(html)
1614 cursor.insertHtml(html)
1615
1615
1616 # After inserting HTML, the text document "remembers" it's in "html
1616 # After inserting HTML, the text document "remembers" it's in "html
1617 # mode", which means that subsequent calls adding plain text will result
1617 # mode", which means that subsequent calls adding plain text will result
1618 # in unwanted formatting, lost tab characters, etc. The following code
1618 # in unwanted formatting, lost tab characters, etc. The following code
1619 # hacks around this behavior, which I consider to be a bug in Qt, by
1619 # hacks around this behavior, which I consider to be a bug in Qt, by
1620 # (crudely) resetting the document's style state.
1620 # (crudely) resetting the document's style state.
1621 cursor.movePosition(QtGui.QTextCursor.Left,
1621 cursor.movePosition(QtGui.QTextCursor.Left,
1622 QtGui.QTextCursor.KeepAnchor)
1622 QtGui.QTextCursor.KeepAnchor)
1623 if cursor.selection().toPlainText() == ' ':
1623 if cursor.selection().toPlainText() == ' ':
1624 cursor.removeSelectedText()
1624 cursor.removeSelectedText()
1625 else:
1625 else:
1626 cursor.movePosition(QtGui.QTextCursor.Right)
1626 cursor.movePosition(QtGui.QTextCursor.Right)
1627 cursor.insertText(' ', QtGui.QTextCharFormat())
1627 cursor.insertText(' ', QtGui.QTextCharFormat())
1628 cursor.endEditBlock()
1628 cursor.endEditBlock()
1629
1629
1630 def _insert_html_fetching_plain_text(self, cursor, html):
1630 def _insert_html_fetching_plain_text(self, cursor, html):
1631 """ Inserts HTML using the specified cursor, then returns its plain text
1631 """ Inserts HTML using the specified cursor, then returns its plain text
1632 version.
1632 version.
1633 """
1633 """
1634 cursor.beginEditBlock()
1634 cursor.beginEditBlock()
1635 cursor.removeSelectedText()
1635 cursor.removeSelectedText()
1636
1636
1637 start = cursor.position()
1637 start = cursor.position()
1638 self._insert_html(cursor, html)
1638 self._insert_html(cursor, html)
1639 end = cursor.position()
1639 end = cursor.position()
1640 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1640 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1641 text = cursor.selection().toPlainText()
1641 text = cursor.selection().toPlainText()
1642
1642
1643 cursor.setPosition(end)
1643 cursor.setPosition(end)
1644 cursor.endEditBlock()
1644 cursor.endEditBlock()
1645 return text
1645 return text
1646
1646
1647 def _insert_plain_text(self, cursor, text):
1647 def _insert_plain_text(self, cursor, text):
1648 """ Inserts plain text using the specified cursor, processing ANSI codes
1648 """ Inserts plain text using the specified cursor, processing ANSI codes
1649 if enabled.
1649 if enabled.
1650 """
1650 """
1651 cursor.beginEditBlock()
1651 cursor.beginEditBlock()
1652 if self.ansi_codes:
1652 if self.ansi_codes:
1653 for substring in self._ansi_processor.split_string(text):
1653 for substring in self._ansi_processor.split_string(text):
1654 for act in self._ansi_processor.actions:
1654 for act in self._ansi_processor.actions:
1655
1655
1656 # Unlike real terminal emulators, we don't distinguish
1656 # Unlike real terminal emulators, we don't distinguish
1657 # between the screen and the scrollback buffer. A screen
1657 # between the screen and the scrollback buffer. A screen
1658 # erase request clears everything.
1658 # erase request clears everything.
1659 if act.action == 'erase' and act.area == 'screen':
1659 if act.action == 'erase' and act.area == 'screen':
1660 cursor.select(QtGui.QTextCursor.Document)
1660 cursor.select(QtGui.QTextCursor.Document)
1661 cursor.removeSelectedText()
1661 cursor.removeSelectedText()
1662
1662
1663 # Simulate a form feed by scrolling just past the last line.
1663 # Simulate a form feed by scrolling just past the last line.
1664 elif act.action == 'scroll' and act.unit == 'page':
1664 elif act.action == 'scroll' and act.unit == 'page':
1665 cursor.insertText('\n')
1665 cursor.insertText('\n')
1666 cursor.endEditBlock()
1666 cursor.endEditBlock()
1667 self._set_top_cursor(cursor)
1667 self._set_top_cursor(cursor)
1668 cursor.joinPreviousEditBlock()
1668 cursor.joinPreviousEditBlock()
1669 cursor.deletePreviousChar()
1669 cursor.deletePreviousChar()
1670
1670
1671 elif act.action == 'carriage-return':
1671 elif act.action == 'carriage-return':
1672 cursor.movePosition(
1672 cursor.movePosition(
1673 cursor.StartOfLine, cursor.KeepAnchor)
1673 cursor.StartOfLine, cursor.KeepAnchor)
1674
1674
1675 elif act.action == 'beep':
1675 elif act.action == 'beep':
1676 QtGui.qApp.beep()
1676 QtGui.qApp.beep()
1677
1677
1678 elif act.action == 'backspace':
1678 elif act.action == 'backspace':
1679 if not cursor.atBlockStart():
1679 if not cursor.atBlockStart():
1680 cursor.movePosition(
1680 cursor.movePosition(
1681 cursor.PreviousCharacter, cursor.KeepAnchor)
1681 cursor.PreviousCharacter, cursor.KeepAnchor)
1682
1682
1683 elif act.action == 'newline':
1683 elif act.action == 'newline':
1684 cursor.movePosition(cursor.EndOfLine)
1684 cursor.movePosition(cursor.EndOfLine)
1685
1685
1686 format = self._ansi_processor.get_format()
1686 format = self._ansi_processor.get_format()
1687
1687
1688 selection = cursor.selectedText()
1688 selection = cursor.selectedText()
1689 if len(selection) == 0:
1689 if len(selection) == 0:
1690 cursor.insertText(substring, format)
1690 cursor.insertText(substring, format)
1691 elif substring is not None:
1691 elif substring is not None:
1692 # BS and CR are treated as a change in print
1692 # BS and CR are treated as a change in print
1693 # position, rather than a backwards character
1693 # position, rather than a backwards character
1694 # deletion for output equivalence with (I)Python
1694 # deletion for output equivalence with (I)Python
1695 # terminal.
1695 # terminal.
1696 if len(substring) >= len(selection):
1696 if len(substring) >= len(selection):
1697 cursor.insertText(substring, format)
1697 cursor.insertText(substring, format)
1698 else:
1698 else:
1699 old_text = selection[len(substring):]
1699 old_text = selection[len(substring):]
1700 cursor.insertText(substring + old_text, format)
1700 cursor.insertText(substring + old_text, format)
1701 cursor.movePosition(cursor.PreviousCharacter,
1701 cursor.movePosition(cursor.PreviousCharacter,
1702 cursor.KeepAnchor, len(old_text))
1702 cursor.KeepAnchor, len(old_text))
1703 else:
1703 else:
1704 cursor.insertText(text)
1704 cursor.insertText(text)
1705 cursor.endEditBlock()
1705 cursor.endEditBlock()
1706
1706
1707 def _insert_plain_text_into_buffer(self, cursor, text):
1707 def _insert_plain_text_into_buffer(self, cursor, text):
1708 """ Inserts text into the input buffer using the specified cursor (which
1708 """ Inserts text into the input buffer using the specified cursor (which
1709 must be in the input buffer), ensuring that continuation prompts are
1709 must be in the input buffer), ensuring that continuation prompts are
1710 inserted as necessary.
1710 inserted as necessary.
1711 """
1711 """
1712 lines = text.splitlines(True)
1712 lines = text.splitlines(True)
1713 if lines:
1713 if lines:
1714 cursor.beginEditBlock()
1714 cursor.beginEditBlock()
1715 cursor.insertText(lines[0])
1715 cursor.insertText(lines[0])
1716 for line in lines[1:]:
1716 for line in lines[1:]:
1717 if self._continuation_prompt_html is None:
1717 if self._continuation_prompt_html is None:
1718 cursor.insertText(self._continuation_prompt)
1718 cursor.insertText(self._continuation_prompt)
1719 else:
1719 else:
1720 self._continuation_prompt = \
1720 self._continuation_prompt = \
1721 self._insert_html_fetching_plain_text(
1721 self._insert_html_fetching_plain_text(
1722 cursor, self._continuation_prompt_html)
1722 cursor, self._continuation_prompt_html)
1723 cursor.insertText(line)
1723 cursor.insertText(line)
1724 cursor.endEditBlock()
1724 cursor.endEditBlock()
1725
1725
1726 def _in_buffer(self, position=None):
1726 def _in_buffer(self, position=None):
1727 """ Returns whether the current cursor (or, if specified, a position) is
1727 """ Returns whether the current cursor (or, if specified, a position) is
1728 inside the editing region.
1728 inside the editing region.
1729 """
1729 """
1730 cursor = self._control.textCursor()
1730 cursor = self._control.textCursor()
1731 if position is None:
1731 if position is None:
1732 position = cursor.position()
1732 position = cursor.position()
1733 else:
1733 else:
1734 cursor.setPosition(position)
1734 cursor.setPosition(position)
1735 line = cursor.blockNumber()
1735 line = cursor.blockNumber()
1736 prompt_line = self._get_prompt_cursor().blockNumber()
1736 prompt_line = self._get_prompt_cursor().blockNumber()
1737 if line == prompt_line:
1737 if line == prompt_line:
1738 return position >= self._prompt_pos
1738 return position >= self._prompt_pos
1739 elif line > prompt_line:
1739 elif line > prompt_line:
1740 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1740 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1741 prompt_pos = cursor.position() + len(self._continuation_prompt)
1741 prompt_pos = cursor.position() + len(self._continuation_prompt)
1742 return position >= prompt_pos
1742 return position >= prompt_pos
1743 return False
1743 return False
1744
1744
1745 def _keep_cursor_in_buffer(self):
1745 def _keep_cursor_in_buffer(self):
1746 """ Ensures that the cursor is inside the editing region. Returns
1746 """ Ensures that the cursor is inside the editing region. Returns
1747 whether the cursor was moved.
1747 whether the cursor was moved.
1748 """
1748 """
1749 moved = not self._in_buffer()
1749 moved = not self._in_buffer()
1750 if moved:
1750 if moved:
1751 cursor = self._control.textCursor()
1751 cursor = self._control.textCursor()
1752 cursor.movePosition(QtGui.QTextCursor.End)
1752 cursor.movePosition(QtGui.QTextCursor.End)
1753 self._control.setTextCursor(cursor)
1753 self._control.setTextCursor(cursor)
1754 return moved
1754 return moved
1755
1755
1756 def _keyboard_quit(self):
1756 def _keyboard_quit(self):
1757 """ Cancels the current editing task ala Ctrl-G in Emacs.
1757 """ Cancels the current editing task ala Ctrl-G in Emacs.
1758 """
1758 """
1759 if self._temp_buffer_filled :
1759 if self._temp_buffer_filled :
1760 self._cancel_completion()
1760 self._cancel_completion()
1761 self._clear_temporary_buffer()
1761 self._clear_temporary_buffer()
1762 else:
1762 else:
1763 self.input_buffer = ''
1763 self.input_buffer = ''
1764
1764
1765 def _page(self, text, html=False):
1765 def _page(self, text, html=False):
1766 """ Displays text using the pager if it exceeds the height of the
1766 """ Displays text using the pager if it exceeds the height of the
1767 viewport.
1767 viewport.
1768
1768
1769 Parameters:
1769 Parameters:
1770 -----------
1770 -----------
1771 html : bool, optional (default False)
1771 html : bool, optional (default False)
1772 If set, the text will be interpreted as HTML instead of plain text.
1772 If set, the text will be interpreted as HTML instead of plain text.
1773 """
1773 """
1774 line_height = QtGui.QFontMetrics(self.font).height()
1774 line_height = QtGui.QFontMetrics(self.font).height()
1775 minlines = self._control.viewport().height() / line_height
1775 minlines = self._control.viewport().height() / line_height
1776 if self.paging != 'none' and \
1776 if self.paging != 'none' and \
1777 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1777 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1778 if self.paging == 'custom':
1778 if self.paging == 'custom':
1779 self.custom_page_requested.emit(text)
1779 self.custom_page_requested.emit(text)
1780 else:
1780 else:
1781 self._page_control.clear()
1781 self._page_control.clear()
1782 cursor = self._page_control.textCursor()
1782 cursor = self._page_control.textCursor()
1783 if html:
1783 if html:
1784 self._insert_html(cursor, text)
1784 self._insert_html(cursor, text)
1785 else:
1785 else:
1786 self._insert_plain_text(cursor, text)
1786 self._insert_plain_text(cursor, text)
1787 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1787 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1788
1788
1789 self._page_control.viewport().resize(self._control.size())
1789 self._page_control.viewport().resize(self._control.size())
1790 if self._splitter:
1790 if self._splitter:
1791 self._page_control.show()
1791 self._page_control.show()
1792 self._page_control.setFocus()
1792 self._page_control.setFocus()
1793 else:
1793 else:
1794 self.layout().setCurrentWidget(self._page_control)
1794 self.layout().setCurrentWidget(self._page_control)
1795 elif html:
1795 elif html:
1796 self._append_html(text)
1796 self._append_html(text)
1797 else:
1797 else:
1798 self._append_plain_text(text)
1798 self._append_plain_text(text)
1799
1799
1800 def _set_paging(self, paging):
1800 def _set_paging(self, paging):
1801 """
1801 """
1802 Change the pager to `paging` style.
1802 Change the pager to `paging` style.
1803
1803
1804 XXX: currently, this is limited to switching between 'hsplit' and
1804 XXX: currently, this is limited to switching between 'hsplit' and
1805 'vsplit'.
1805 'vsplit'.
1806
1806
1807 Parameters:
1807 Parameters:
1808 -----------
1808 -----------
1809 paging : string
1809 paging : string
1810 Either "hsplit", "vsplit", or "inside"
1810 Either "hsplit", "vsplit", or "inside"
1811 """
1811 """
1812 if self._splitter is None:
1812 if self._splitter is None:
1813 raise NotImplementedError("""can only switch if --paging=hsplit or
1813 raise NotImplementedError("""can only switch if --paging=hsplit or
1814 --paging=vsplit is used.""")
1814 --paging=vsplit is used.""")
1815 if paging == 'hsplit':
1815 if paging == 'hsplit':
1816 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1816 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1817 elif paging == 'vsplit':
1817 elif paging == 'vsplit':
1818 self._splitter.setOrientation(QtCore.Qt.Vertical)
1818 self._splitter.setOrientation(QtCore.Qt.Vertical)
1819 elif paging == 'inside':
1819 elif paging == 'inside':
1820 raise NotImplementedError("""switching to 'inside' paging not
1820 raise NotImplementedError("""switching to 'inside' paging not
1821 supported yet.""")
1821 supported yet.""")
1822 else:
1822 else:
1823 raise ValueError("unknown paging method '%s'" % paging)
1823 raise ValueError("unknown paging method '%s'" % paging)
1824 self.paging = paging
1824 self.paging = paging
1825
1825
1826 def _prompt_finished(self):
1826 def _prompt_finished(self):
1827 """ Called immediately after a prompt is finished, i.e. when some input
1827 """ Called immediately after a prompt is finished, i.e. when some input
1828 will be processed and a new prompt displayed.
1828 will be processed and a new prompt displayed.
1829 """
1829 """
1830 self._control.setReadOnly(True)
1830 self._control.setReadOnly(True)
1831 self._prompt_finished_hook()
1831 self._prompt_finished_hook()
1832
1832
1833 def _prompt_started(self):
1833 def _prompt_started(self):
1834 """ Called immediately after a new prompt is displayed.
1834 """ Called immediately after a new prompt is displayed.
1835 """
1835 """
1836 # Temporarily disable the maximum block count to permit undo/redo and
1836 # Temporarily disable the maximum block count to permit undo/redo and
1837 # to ensure that the prompt position does not change due to truncation.
1837 # to ensure that the prompt position does not change due to truncation.
1838 self._control.document().setMaximumBlockCount(0)
1838 self._control.document().setMaximumBlockCount(0)
1839 self._control.setUndoRedoEnabled(True)
1839 self._control.setUndoRedoEnabled(True)
1840
1840
1841 # Work around bug in QPlainTextEdit: input method is not re-enabled
1841 # Work around bug in QPlainTextEdit: input method is not re-enabled
1842 # when read-only is disabled.
1842 # when read-only is disabled.
1843 self._control.setReadOnly(False)
1843 self._control.setReadOnly(False)
1844 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1844 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1845
1845
1846 if not self._reading:
1846 if not self._reading:
1847 self._executing = False
1847 self._executing = False
1848 self._prompt_started_hook()
1848 self._prompt_started_hook()
1849
1849
1850 # If the input buffer has changed while executing, load it.
1850 # If the input buffer has changed while executing, load it.
1851 if self._input_buffer_pending:
1851 if self._input_buffer_pending:
1852 self.input_buffer = self._input_buffer_pending
1852 self.input_buffer = self._input_buffer_pending
1853 self._input_buffer_pending = ''
1853 self._input_buffer_pending = ''
1854
1854
1855 self._control.moveCursor(QtGui.QTextCursor.End)
1855 self._control.moveCursor(QtGui.QTextCursor.End)
1856
1856
1857 def _readline(self, prompt='', callback=None):
1857 def _readline(self, prompt='', callback=None):
1858 """ Reads one line of input from the user.
1858 """ Reads one line of input from the user.
1859
1859
1860 Parameters
1860 Parameters
1861 ----------
1861 ----------
1862 prompt : str, optional
1862 prompt : str, optional
1863 The prompt to print before reading the line.
1863 The prompt to print before reading the line.
1864
1864
1865 callback : callable, optional
1865 callback : callable, optional
1866 A callback to execute with the read line. If not specified, input is
1866 A callback to execute with the read line. If not specified, input is
1867 read *synchronously* and this method does not return until it has
1867 read *synchronously* and this method does not return until it has
1868 been read.
1868 been read.
1869
1869
1870 Returns
1870 Returns
1871 -------
1871 -------
1872 If a callback is specified, returns nothing. Otherwise, returns the
1872 If a callback is specified, returns nothing. Otherwise, returns the
1873 input string with the trailing newline stripped.
1873 input string with the trailing newline stripped.
1874 """
1874 """
1875 if self._reading:
1875 if self._reading:
1876 raise RuntimeError('Cannot read a line. Widget is already reading.')
1876 raise RuntimeError('Cannot read a line. Widget is already reading.')
1877
1877
1878 if not callback and not self.isVisible():
1878 if not callback and not self.isVisible():
1879 # If the user cannot see the widget, this function cannot return.
1879 # If the user cannot see the widget, this function cannot return.
1880 raise RuntimeError('Cannot synchronously read a line if the widget '
1880 raise RuntimeError('Cannot synchronously read a line if the widget '
1881 'is not visible!')
1881 'is not visible!')
1882
1882
1883 self._reading = True
1883 self._reading = True
1884 self._show_prompt(prompt, newline=False)
1884 self._show_prompt(prompt, newline=False)
1885
1885
1886 if callback is None:
1886 if callback is None:
1887 self._reading_callback = None
1887 self._reading_callback = None
1888 while self._reading:
1888 while self._reading:
1889 QtCore.QCoreApplication.processEvents()
1889 QtCore.QCoreApplication.processEvents()
1890 return self._get_input_buffer(force=True).rstrip('\n')
1890 return self._get_input_buffer(force=True).rstrip('\n')
1891
1891
1892 else:
1892 else:
1893 self._reading_callback = lambda: \
1893 self._reading_callback = lambda: \
1894 callback(self._get_input_buffer(force=True).rstrip('\n'))
1894 callback(self._get_input_buffer(force=True).rstrip('\n'))
1895
1895
1896 def _set_continuation_prompt(self, prompt, html=False):
1896 def _set_continuation_prompt(self, prompt, html=False):
1897 """ Sets the continuation prompt.
1897 """ Sets the continuation prompt.
1898
1898
1899 Parameters
1899 Parameters
1900 ----------
1900 ----------
1901 prompt : str
1901 prompt : str
1902 The prompt to show when more input is needed.
1902 The prompt to show when more input is needed.
1903
1903
1904 html : bool, optional (default False)
1904 html : bool, optional (default False)
1905 If set, the prompt will be inserted as formatted HTML. Otherwise,
1905 If set, the prompt will be inserted as formatted HTML. Otherwise,
1906 the prompt will be treated as plain text, though ANSI color codes
1906 the prompt will be treated as plain text, though ANSI color codes
1907 will be handled.
1907 will be handled.
1908 """
1908 """
1909 if html:
1909 if html:
1910 self._continuation_prompt_html = prompt
1910 self._continuation_prompt_html = prompt
1911 else:
1911 else:
1912 self._continuation_prompt = prompt
1912 self._continuation_prompt = prompt
1913 self._continuation_prompt_html = None
1913 self._continuation_prompt_html = None
1914
1914
1915 def _set_cursor(self, cursor):
1915 def _set_cursor(self, cursor):
1916 """ Convenience method to set the current cursor.
1916 """ Convenience method to set the current cursor.
1917 """
1917 """
1918 self._control.setTextCursor(cursor)
1918 self._control.setTextCursor(cursor)
1919
1919
1920 def _set_top_cursor(self, cursor):
1920 def _set_top_cursor(self, cursor):
1921 """ Scrolls the viewport so that the specified cursor is at the top.
1921 """ Scrolls the viewport so that the specified cursor is at the top.
1922 """
1922 """
1923 scrollbar = self._control.verticalScrollBar()
1923 scrollbar = self._control.verticalScrollBar()
1924 scrollbar.setValue(scrollbar.maximum())
1924 scrollbar.setValue(scrollbar.maximum())
1925 original_cursor = self._control.textCursor()
1925 original_cursor = self._control.textCursor()
1926 self._control.setTextCursor(cursor)
1926 self._control.setTextCursor(cursor)
1927 self._control.ensureCursorVisible()
1927 self._control.ensureCursorVisible()
1928 self._control.setTextCursor(original_cursor)
1928 self._control.setTextCursor(original_cursor)
1929
1929
1930 def _show_prompt(self, prompt=None, html=False, newline=True):
1930 def _show_prompt(self, prompt=None, html=False, newline=True):
1931 """ Writes a new prompt at the end of the buffer.
1931 """ Writes a new prompt at the end of the buffer.
1932
1932
1933 Parameters
1933 Parameters
1934 ----------
1934 ----------
1935 prompt : str, optional
1935 prompt : str, optional
1936 The prompt to show. If not specified, the previous prompt is used.
1936 The prompt to show. If not specified, the previous prompt is used.
1937
1937
1938 html : bool, optional (default False)
1938 html : bool, optional (default False)
1939 Only relevant when a prompt is specified. If set, the prompt will
1939 Only relevant when a prompt is specified. If set, the prompt will
1940 be inserted as formatted HTML. Otherwise, the prompt will be treated
1940 be inserted as formatted HTML. Otherwise, the prompt will be treated
1941 as plain text, though ANSI color codes will be handled.
1941 as plain text, though ANSI color codes will be handled.
1942
1942
1943 newline : bool, optional (default True)
1943 newline : bool, optional (default True)
1944 If set, a new line will be written before showing the prompt if
1944 If set, a new line will be written before showing the prompt if
1945 there is not already a newline at the end of the buffer.
1945 there is not already a newline at the end of the buffer.
1946 """
1946 """
1947 # Save the current end position to support _append*(before_prompt=True).
1947 # Save the current end position to support _append*(before_prompt=True).
1948 cursor = self._get_end_cursor()
1948 cursor = self._get_end_cursor()
1949 self._append_before_prompt_pos = cursor.position()
1949 self._append_before_prompt_pos = cursor.position()
1950
1950
1951 # Insert a preliminary newline, if necessary.
1951 # Insert a preliminary newline, if necessary.
1952 if newline and cursor.position() > 0:
1952 if newline and cursor.position() > 0:
1953 cursor.movePosition(QtGui.QTextCursor.Left,
1953 cursor.movePosition(QtGui.QTextCursor.Left,
1954 QtGui.QTextCursor.KeepAnchor)
1954 QtGui.QTextCursor.KeepAnchor)
1955 if cursor.selection().toPlainText() != '\n':
1955 if cursor.selection().toPlainText() != '\n':
1956 self._append_block()
1956 self._append_block()
1957
1957
1958 # Write the prompt.
1958 # Write the prompt.
1959 self._append_plain_text(self._prompt_sep)
1959 self._append_plain_text(self._prompt_sep)
1960 if prompt is None:
1960 if prompt is None:
1961 if self._prompt_html is None:
1961 if self._prompt_html is None:
1962 self._append_plain_text(self._prompt)
1962 self._append_plain_text(self._prompt)
1963 else:
1963 else:
1964 self._append_html(self._prompt_html)
1964 self._append_html(self._prompt_html)
1965 else:
1965 else:
1966 if html:
1966 if html:
1967 self._prompt = self._append_html_fetching_plain_text(prompt)
1967 self._prompt = self._append_html_fetching_plain_text(prompt)
1968 self._prompt_html = prompt
1968 self._prompt_html = prompt
1969 else:
1969 else:
1970 self._append_plain_text(prompt)
1970 self._append_plain_text(prompt)
1971 self._prompt = prompt
1971 self._prompt = prompt
1972 self._prompt_html = None
1972 self._prompt_html = None
1973
1973
1974 self._prompt_pos = self._get_end_cursor().position()
1974 self._prompt_pos = self._get_end_cursor().position()
1975 self._prompt_started()
1975 self._prompt_started()
1976
1976
1977 #------ Signal handlers ----------------------------------------------------
1977 #------ Signal handlers ----------------------------------------------------
1978
1978
1979 def _adjust_scrollbars(self):
1979 def _adjust_scrollbars(self):
1980 """ Expands the vertical scrollbar beyond the range set by Qt.
1980 """ Expands the vertical scrollbar beyond the range set by Qt.
1981 """
1981 """
1982 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1982 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1983 # and qtextedit.cpp.
1983 # and qtextedit.cpp.
1984 document = self._control.document()
1984 document = self._control.document()
1985 scrollbar = self._control.verticalScrollBar()
1985 scrollbar = self._control.verticalScrollBar()
1986 viewport_height = self._control.viewport().height()
1986 viewport_height = self._control.viewport().height()
1987 if isinstance(self._control, QtGui.QPlainTextEdit):
1987 if isinstance(self._control, QtGui.QPlainTextEdit):
1988 maximum = max(0, document.lineCount() - 1)
1988 maximum = max(0, document.lineCount() - 1)
1989 step = viewport_height / self._control.fontMetrics().lineSpacing()
1989 step = viewport_height / self._control.fontMetrics().lineSpacing()
1990 else:
1990 else:
1991 # QTextEdit does not do line-based layout and blocks will not in
1991 # QTextEdit does not do line-based layout and blocks will not in
1992 # general have the same height. Therefore it does not make sense to
1992 # general have the same height. Therefore it does not make sense to
1993 # attempt to scroll in line height increments.
1993 # attempt to scroll in line height increments.
1994 maximum = document.size().height()
1994 maximum = document.size().height()
1995 step = viewport_height
1995 step = viewport_height
1996 diff = maximum - scrollbar.maximum()
1996 diff = maximum - scrollbar.maximum()
1997 scrollbar.setRange(0, maximum)
1997 scrollbar.setRange(0, maximum)
1998 scrollbar.setPageStep(step)
1998 scrollbar.setPageStep(step)
1999
1999
2000 # Compensate for undesirable scrolling that occurs automatically due to
2000 # Compensate for undesirable scrolling that occurs automatically due to
2001 # maximumBlockCount() text truncation.
2001 # maximumBlockCount() text truncation.
2002 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2002 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2003 scrollbar.setValue(scrollbar.value() + diff)
2003 scrollbar.setValue(scrollbar.value() + diff)
2004
2004
2005 def _custom_context_menu_requested(self, pos):
2005 def _custom_context_menu_requested(self, pos):
2006 """ Shows a context menu at the given QPoint (in widget coordinates).
2006 """ Shows a context menu at the given QPoint (in widget coordinates).
2007 """
2007 """
2008 menu = self._context_menu_make(pos)
2008 menu = self._context_menu_make(pos)
2009 menu.exec_(self._control.mapToGlobal(pos))
2009 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,785 +1,785 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7 import uuid
7 import uuid
8
8
9 # System library imports
9 # System library imports
10 from pygments.lexers import PythonLexer
10 from pygments.lexers import PythonLexer
11 from IPython.external import qt
11 from IPython.external import qt
12 from IPython.external.qt import QtCore, QtGui
12 from IPython.external.qt import QtCore, QtGui
13
13
14 # Local imports
14 # Local imports
15 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
15 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
16 from IPython.core.inputtransformer import classic_prompt
16 from IPython.core.inputtransformer import classic_prompt
17 from IPython.core.oinspect import call_tip
17 from IPython.core.oinspect import call_tip
18 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
18 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
19 from IPython.utils.traitlets import Bool, Instance, Unicode
19 from IPython.utils.traitlets import Bool, Instance, Unicode
20 from bracket_matcher import BracketMatcher
20 from bracket_matcher import BracketMatcher
21 from call_tip_widget import CallTipWidget
21 from call_tip_widget import CallTipWidget
22 from completion_lexer import CompletionLexer
22 from completion_lexer import CompletionLexer
23 from history_console_widget import HistoryConsoleWidget
23 from history_console_widget import HistoryConsoleWidget
24 from pygments_highlighter import PygmentsHighlighter
24 from pygments_highlighter import PygmentsHighlighter
25
25
26
26
27 class FrontendHighlighter(PygmentsHighlighter):
27 class FrontendHighlighter(PygmentsHighlighter):
28 """ A PygmentsHighlighter that understands and ignores prompts.
28 """ A PygmentsHighlighter that understands and ignores prompts.
29 """
29 """
30
30
31 def __init__(self, frontend):
31 def __init__(self, frontend):
32 super(FrontendHighlighter, self).__init__(frontend._control.document())
32 super(FrontendHighlighter, self).__init__(frontend._control.document())
33 self._current_offset = 0
33 self._current_offset = 0
34 self._frontend = frontend
34 self._frontend = frontend
35 self.highlighting_on = False
35 self.highlighting_on = False
36
36
37 def highlightBlock(self, string):
37 def highlightBlock(self, string):
38 """ Highlight a block of text. Reimplemented to highlight selectively.
38 """ Highlight a block of text. Reimplemented to highlight selectively.
39 """
39 """
40 if not self.highlighting_on:
40 if not self.highlighting_on:
41 return
41 return
42
42
43 # The input to this function is a unicode string that may contain
43 # The input to this function is a unicode string that may contain
44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
44 # paragraph break characters, non-breaking spaces, etc. Here we acquire
45 # the string as plain text so we can compare it.
45 # the string as plain text so we can compare it.
46 current_block = self.currentBlock()
46 current_block = self.currentBlock()
47 string = self._frontend._get_block_plain_text(current_block)
47 string = self._frontend._get_block_plain_text(current_block)
48
48
49 # Decide whether to check for the regular or continuation prompt.
49 # Decide whether to check for the regular or continuation prompt.
50 if current_block.contains(self._frontend._prompt_pos):
50 if current_block.contains(self._frontend._prompt_pos):
51 prompt = self._frontend._prompt
51 prompt = self._frontend._prompt
52 else:
52 else:
53 prompt = self._frontend._continuation_prompt
53 prompt = self._frontend._continuation_prompt
54
54
55 # Only highlight if we can identify a prompt, but make sure not to
55 # Only highlight if we can identify a prompt, but make sure not to
56 # highlight the prompt.
56 # highlight the prompt.
57 if string.startswith(prompt):
57 if string.startswith(prompt):
58 self._current_offset = len(prompt)
58 self._current_offset = len(prompt)
59 string = string[len(prompt):]
59 string = string[len(prompt):]
60 super(FrontendHighlighter, self).highlightBlock(string)
60 super(FrontendHighlighter, self).highlightBlock(string)
61
61
62 def rehighlightBlock(self, block):
62 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
63 """ Reimplemented to temporarily enable highlighting if disabled.
64 """
64 """
65 old = self.highlighting_on
65 old = self.highlighting_on
66 self.highlighting_on = True
66 self.highlighting_on = True
67 super(FrontendHighlighter, self).rehighlightBlock(block)
67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 self.highlighting_on = old
68 self.highlighting_on = old
69
69
70 def setFormat(self, start, count, format):
70 def setFormat(self, start, count, format):
71 """ Reimplemented to highlight selectively.
71 """ Reimplemented to highlight selectively.
72 """
72 """
73 start += self._current_offset
73 start += self._current_offset
74 super(FrontendHighlighter, self).setFormat(start, count, format)
74 super(FrontendHighlighter, self).setFormat(start, count, format)
75
75
76
76
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 """ A Qt frontend for a generic Python kernel.
78 """ A Qt frontend for a generic Python kernel.
79 """
79 """
80
80
81 # The text to show when the kernel is (re)started.
81 # The text to show when the kernel is (re)started.
82 banner = Unicode()
82 banner = Unicode()
83
83
84 # An option and corresponding signal for overriding the default kernel
84 # An option and corresponding signal for overriding the default kernel
85 # interrupt behavior.
85 # interrupt behavior.
86 custom_interrupt = Bool(False)
86 custom_interrupt = Bool(False)
87 custom_interrupt_requested = QtCore.Signal()
87 custom_interrupt_requested = QtCore.Signal()
88
88
89 # An option and corresponding signals for overriding the default kernel
89 # An option and corresponding signals for overriding the default kernel
90 # restart behavior.
90 # restart behavior.
91 custom_restart = Bool(False)
91 custom_restart = Bool(False)
92 custom_restart_kernel_died = QtCore.Signal(float)
92 custom_restart_kernel_died = QtCore.Signal(float)
93 custom_restart_requested = QtCore.Signal()
93 custom_restart_requested = QtCore.Signal()
94
94
95 # Whether to automatically show calltips on open-parentheses.
95 # Whether to automatically show calltips on open-parentheses.
96 enable_calltips = Bool(True, config=True,
96 enable_calltips = Bool(True, config=True,
97 help="Whether to draw information calltips on open-parentheses.")
97 help="Whether to draw information calltips on open-parentheses.")
98
98
99 clear_on_kernel_restart = Bool(True, config=True,
99 clear_on_kernel_restart = Bool(True, config=True,
100 help="Whether to clear the console when the kernel is restarted")
100 help="Whether to clear the console when the kernel is restarted")
101
101
102 confirm_restart = Bool(True, config=True,
102 confirm_restart = Bool(True, config=True,
103 help="Whether to ask for user confirmation when restarting kernel")
103 help="Whether to ask for user confirmation when restarting kernel")
104
104
105 # Emitted when a user visible 'execute_request' has been submitted to the
105 # Emitted when a user visible 'execute_request' has been submitted to the
106 # kernel from the FrontendWidget. Contains the code to be executed.
106 # kernel from the FrontendWidget. Contains the code to be executed.
107 executing = QtCore.Signal(object)
107 executing = QtCore.Signal(object)
108
108
109 # Emitted when a user-visible 'execute_reply' has been received from the
109 # Emitted when a user-visible 'execute_reply' has been received from the
110 # kernel and processed by the FrontendWidget. Contains the response message.
110 # kernel and processed by the FrontendWidget. Contains the response message.
111 executed = QtCore.Signal(object)
111 executed = QtCore.Signal(object)
112
112
113 # Emitted when an exit request has been received from the kernel.
113 # Emitted when an exit request has been received from the kernel.
114 exit_requested = QtCore.Signal(object)
114 exit_requested = QtCore.Signal(object)
115
115
116 # Protected class variables.
116 # Protected class variables.
117 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
117 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
118 logical_line_transforms=[],
118 logical_line_transforms=[],
119 python_line_transforms=[],
119 python_line_transforms=[],
120 )
120 )
121 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
121 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
122 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
122 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
123 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
123 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
124 _input_splitter_class = InputSplitter
124 _input_splitter_class = InputSplitter
125 _local_kernel = False
125 _local_kernel = False
126 _highlighter = Instance(FrontendHighlighter)
126 _highlighter = Instance(FrontendHighlighter)
127
127
128 #---------------------------------------------------------------------------
128 #---------------------------------------------------------------------------
129 # 'object' interface
129 # 'object' interface
130 #---------------------------------------------------------------------------
130 #---------------------------------------------------------------------------
131
131
132 def __init__(self, *args, **kw):
132 def __init__(self, *args, **kw):
133 super(FrontendWidget, self).__init__(*args, **kw)
133 super(FrontendWidget, self).__init__(*args, **kw)
134 # FIXME: remove this when PySide min version is updated past 1.0.7
134 # FIXME: remove this when PySide min version is updated past 1.0.7
135 # forcefully disable calltips if PySide is < 1.0.7, because they crash
135 # forcefully disable calltips if PySide is < 1.0.7, because they crash
136 if qt.QT_API == qt.QT_API_PYSIDE:
136 if qt.QT_API == qt.QT_API_PYSIDE:
137 import PySide
137 import PySide
138 if PySide.__version_info__ < (1,0,7):
138 if PySide.__version_info__ < (1,0,7):
139 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
139 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
140 self.enable_calltips = False
140 self.enable_calltips = False
141
141
142 # FrontendWidget protected variables.
142 # FrontendWidget protected variables.
143 self._bracket_matcher = BracketMatcher(self._control)
143 self._bracket_matcher = BracketMatcher(self._control)
144 self._call_tip_widget = CallTipWidget(self._control)
144 self._call_tip_widget = CallTipWidget(self._control)
145 self._completion_lexer = CompletionLexer(PythonLexer())
145 self._completion_lexer = CompletionLexer(PythonLexer())
146 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
146 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
147 self._hidden = False
147 self._hidden = False
148 self._highlighter = FrontendHighlighter(self)
148 self._highlighter = FrontendHighlighter(self)
149 self._input_splitter = self._input_splitter_class()
149 self._input_splitter = self._input_splitter_class()
150 self._kernel_manager = None
150 self._kernel_manager = None
151 self._kernel_client = None
151 self._kernel_client = None
152 self._request_info = {}
152 self._request_info = {}
153 self._request_info['execute'] = {};
153 self._request_info['execute'] = {};
154 self._callback_dict = {}
154 self._callback_dict = {}
155
155
156 # Configure the ConsoleWidget.
156 # Configure the ConsoleWidget.
157 self.tab_width = 4
157 self.tab_width = 4
158 self._set_continuation_prompt('... ')
158 self._set_continuation_prompt('... ')
159
159
160 # Configure the CallTipWidget.
160 # Configure the CallTipWidget.
161 self._call_tip_widget.setFont(self.font)
161 self._call_tip_widget.setFont(self.font)
162 self.font_changed.connect(self._call_tip_widget.setFont)
162 self.font_changed.connect(self._call_tip_widget.setFont)
163
163
164 # Configure actions.
164 # Configure actions.
165 action = self._copy_raw_action
165 action = self._copy_raw_action
166 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
166 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
167 action.setEnabled(False)
167 action.setEnabled(False)
168 action.setShortcut(QtGui.QKeySequence(key))
168 action.setShortcut(QtGui.QKeySequence(key))
169 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
169 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
170 action.triggered.connect(self.copy_raw)
170 action.triggered.connect(self.copy_raw)
171 self.copy_available.connect(action.setEnabled)
171 self.copy_available.connect(action.setEnabled)
172 self.addAction(action)
172 self.addAction(action)
173
173
174 # Connect signal handlers.
174 # Connect signal handlers.
175 document = self._control.document()
175 document = self._control.document()
176 document.contentsChange.connect(self._document_contents_change)
176 document.contentsChange.connect(self._document_contents_change)
177
177
178 # Set flag for whether we are connected via localhost.
178 # Set flag for whether we are connected via localhost.
179 self._local_kernel = kw.get('local_kernel',
179 self._local_kernel = kw.get('local_kernel',
180 FrontendWidget._local_kernel)
180 FrontendWidget._local_kernel)
181
181
182 #---------------------------------------------------------------------------
182 #---------------------------------------------------------------------------
183 # 'ConsoleWidget' public interface
183 # 'ConsoleWidget' public interface
184 #---------------------------------------------------------------------------
184 #---------------------------------------------------------------------------
185
185
186 def copy(self):
186 def copy(self):
187 """ Copy the currently selected text to the clipboard, removing prompts.
187 """ Copy the currently selected text to the clipboard, removing prompts.
188 """
188 """
189 if self._page_control is not None and self._page_control.hasFocus():
189 if self._page_control is not None and self._page_control.hasFocus():
190 self._page_control.copy()
190 self._page_control.copy()
191 elif self._control.hasFocus():
191 elif self._control.hasFocus():
192 text = self._control.textCursor().selection().toPlainText()
192 text = self._control.textCursor().selection().toPlainText()
193 if text:
193 if text:
194 text = self._prompt_transformer.transform_cell(text)
194 text = self._prompt_transformer.transform_cell(text)
195 QtGui.QApplication.clipboard().setText(text)
195 QtGui.QApplication.clipboard().setText(text)
196 else:
196 else:
197 self.log.debug("frontend widget : unknown copy target")
197 self.log.debug("frontend widget : unknown copy target")
198
198
199 #---------------------------------------------------------------------------
199 #---------------------------------------------------------------------------
200 # 'ConsoleWidget' abstract interface
200 # 'ConsoleWidget' abstract interface
201 #---------------------------------------------------------------------------
201 #---------------------------------------------------------------------------
202
202
203 def _is_complete(self, source, interactive):
203 def _is_complete(self, source, interactive):
204 """ Returns whether 'source' can be completely processed and a new
204 """ Returns whether 'source' can be completely processed and a new
205 prompt created. When triggered by an Enter/Return key press,
205 prompt created. When triggered by an Enter/Return key press,
206 'interactive' is True; otherwise, it is False.
206 'interactive' is True; otherwise, it is False.
207 """
207 """
208 self._input_splitter.reset()
208 self._input_splitter.reset()
209 complete = self._input_splitter.push(source)
209 complete = self._input_splitter.push(source)
210 if interactive:
210 if interactive:
211 complete = not self._input_splitter.push_accepts_more()
211 complete = not self._input_splitter.push_accepts_more()
212 return complete
212 return complete
213
213
214 def _execute(self, source, hidden):
214 def _execute(self, source, hidden):
215 """ Execute 'source'. If 'hidden', do not show any output.
215 """ Execute 'source'. If 'hidden', do not show any output.
216
216
217 See parent class :meth:`execute` docstring for full details.
217 See parent class :meth:`execute` docstring for full details.
218 """
218 """
219 msg_id = self.kernel_client.execute(source, hidden)
219 msg_id = self.kernel_client.execute(source, hidden)
220 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
220 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
221 self._hidden = hidden
221 self._hidden = hidden
222 if not hidden:
222 if not hidden:
223 self.executing.emit(source)
223 self.executing.emit(source)
224
224
225 def _prompt_started_hook(self):
225 def _prompt_started_hook(self):
226 """ Called immediately after a new prompt is displayed.
226 """ Called immediately after a new prompt is displayed.
227 """
227 """
228 if not self._reading:
228 if not self._reading:
229 self._highlighter.highlighting_on = True
229 self._highlighter.highlighting_on = True
230
230
231 def _prompt_finished_hook(self):
231 def _prompt_finished_hook(self):
232 """ Called immediately after a prompt is finished, i.e. when some input
232 """ Called immediately after a prompt is finished, i.e. when some input
233 will be processed and a new prompt displayed.
233 will be processed and a new prompt displayed.
234 """
234 """
235 # Flush all state from the input splitter so the next round of
235 # Flush all state from the input splitter so the next round of
236 # reading input starts with a clean buffer.
236 # reading input starts with a clean buffer.
237 self._input_splitter.reset()
237 self._input_splitter.reset()
238
238
239 if not self._reading:
239 if not self._reading:
240 self._highlighter.highlighting_on = False
240 self._highlighter.highlighting_on = False
241
241
242 def _tab_pressed(self):
242 def _tab_pressed(self):
243 """ Called when the tab key is pressed. Returns whether to continue
243 """ Called when the tab key is pressed. Returns whether to continue
244 processing the event.
244 processing the event.
245 """
245 """
246 # Perform tab completion if:
246 # Perform tab completion if:
247 # 1) The cursor is in the input buffer.
247 # 1) The cursor is in the input buffer.
248 # 2) There is a non-whitespace character before the cursor.
248 # 2) There is a non-whitespace character before the cursor.
249 text = self._get_input_buffer_cursor_line()
249 text = self._get_input_buffer_cursor_line()
250 if text is None:
250 if text is None:
251 return False
251 return False
252 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
252 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
253 if complete:
253 if complete:
254 self._complete()
254 self._complete()
255 return not complete
255 return not complete
256
256
257 #---------------------------------------------------------------------------
257 #---------------------------------------------------------------------------
258 # 'ConsoleWidget' protected interface
258 # 'ConsoleWidget' protected interface
259 #---------------------------------------------------------------------------
259 #---------------------------------------------------------------------------
260
260
261 def _context_menu_make(self, pos):
261 def _context_menu_make(self, pos):
262 """ Reimplemented to add an action for raw copy.
262 """ Reimplemented to add an action for raw copy.
263 """
263 """
264 menu = super(FrontendWidget, self)._context_menu_make(pos)
264 menu = super(FrontendWidget, self)._context_menu_make(pos)
265 for before_action in menu.actions():
265 for before_action in menu.actions():
266 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
266 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
267 QtGui.QKeySequence.ExactMatch:
267 QtGui.QKeySequence.ExactMatch:
268 menu.insertAction(before_action, self._copy_raw_action)
268 menu.insertAction(before_action, self._copy_raw_action)
269 break
269 break
270 return menu
270 return menu
271
271
272 def request_interrupt_kernel(self):
272 def request_interrupt_kernel(self):
273 if self._executing:
273 if self._executing:
274 self.interrupt_kernel()
274 self.interrupt_kernel()
275
275
276 def request_restart_kernel(self):
276 def request_restart_kernel(self):
277 message = 'Are you sure you want to restart the kernel?'
277 message = 'Are you sure you want to restart the kernel?'
278 self.restart_kernel(message, now=False)
278 self.restart_kernel(message, now=False)
279
279
280 def _event_filter_console_keypress(self, event):
280 def _event_filter_console_keypress(self, event):
281 """ Reimplemented for execution interruption and smart backspace.
281 """ Reimplemented for execution interruption and smart backspace.
282 """
282 """
283 key = event.key()
283 key = event.key()
284 if self._control_key_down(event.modifiers(), include_command=False):
284 if self._control_key_down(event.modifiers(), include_command=False):
285
285
286 if key == QtCore.Qt.Key_C and self._executing:
286 if key == QtCore.Qt.Key_C and self._executing:
287 self.request_interrupt_kernel()
287 self.request_interrupt_kernel()
288 return True
288 return True
289
289
290 elif key == QtCore.Qt.Key_Period:
290 elif key == QtCore.Qt.Key_Period:
291 self.request_restart_kernel()
291 self.request_restart_kernel()
292 return True
292 return True
293
293
294 elif not event.modifiers() & QtCore.Qt.AltModifier:
294 elif not event.modifiers() & QtCore.Qt.AltModifier:
295
295
296 # Smart backspace: remove four characters in one backspace if:
296 # Smart backspace: remove four characters in one backspace if:
297 # 1) everything left of the cursor is whitespace
297 # 1) everything left of the cursor is whitespace
298 # 2) the four characters immediately left of the cursor are spaces
298 # 2) the four characters immediately left of the cursor are spaces
299 if key == QtCore.Qt.Key_Backspace:
299 if key == QtCore.Qt.Key_Backspace:
300 col = self._get_input_buffer_cursor_column()
300 col = self._get_input_buffer_cursor_column()
301 cursor = self._control.textCursor()
301 cursor = self._control.textCursor()
302 if col > 3 and not cursor.hasSelection():
302 if col > 3 and not cursor.hasSelection():
303 text = self._get_input_buffer_cursor_line()[:col]
303 text = self._get_input_buffer_cursor_line()[:col]
304 if text.endswith(' ') and not text.strip():
304 if text.endswith(' ') and not text.strip():
305 cursor.movePosition(QtGui.QTextCursor.Left,
305 cursor.movePosition(QtGui.QTextCursor.Left,
306 QtGui.QTextCursor.KeepAnchor, 4)
306 QtGui.QTextCursor.KeepAnchor, 4)
307 cursor.removeSelectedText()
307 cursor.removeSelectedText()
308 return True
308 return True
309
309
310 return super(FrontendWidget, self)._event_filter_console_keypress(event)
310 return super(FrontendWidget, self)._event_filter_console_keypress(event)
311
311
312 def _insert_continuation_prompt(self, cursor):
312 def _insert_continuation_prompt(self, cursor):
313 """ Reimplemented for auto-indentation.
313 """ Reimplemented for auto-indentation.
314 """
314 """
315 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
315 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
316 cursor.insertText(' ' * self._input_splitter.indent_spaces)
316 cursor.insertText(' ' * self._input_splitter.indent_spaces)
317
317
318 #---------------------------------------------------------------------------
318 #---------------------------------------------------------------------------
319 # 'BaseFrontendMixin' abstract interface
319 # 'BaseFrontendMixin' abstract interface
320 #---------------------------------------------------------------------------
320 #---------------------------------------------------------------------------
321
321
322 def _handle_complete_reply(self, rep):
322 def _handle_complete_reply(self, rep):
323 """ Handle replies for tab completion.
323 """ Handle replies for tab completion.
324 """
324 """
325 self.log.debug("complete: %s", rep.get('content', ''))
325 self.log.debug("complete: %s", rep.get('content', ''))
326 cursor = self._get_cursor()
326 cursor = self._get_cursor()
327 info = self._request_info.get('complete')
327 info = self._request_info.get('complete')
328 if info and info.id == rep['parent_header']['msg_id'] and \
328 if info and info.id == rep['parent_header']['msg_id'] and \
329 info.pos == cursor.position():
329 info.pos == cursor.position():
330 text = '.'.join(self._get_context())
330 text = '.'.join(self._get_context())
331 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
331 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
332 self._complete_with_items(cursor, rep['content']['matches'])
332 self._complete_with_items(cursor, rep['content']['matches'])
333
333
334 def _silent_exec_callback(self, expr, callback):
334 def _silent_exec_callback(self, expr, callback):
335 """Silently execute `expr` in the kernel and call `callback` with reply
335 """Silently execute `expr` in the kernel and call `callback` with reply
336
336
337 the `expr` is evaluated silently in the kernel (without) output in
337 the `expr` is evaluated silently in the kernel (without) output in
338 the frontend. Call `callback` with the
338 the frontend. Call `callback` with the
339 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
339 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
340
340
341 Parameters
341 Parameters
342 ----------
342 ----------
343 expr : string
343 expr : string
344 valid string to be executed by the kernel.
344 valid string to be executed by the kernel.
345 callback : function
345 callback : function
346 function accepting one argument, as a string. The string will be
346 function accepting one argument, as a string. The string will be
347 the `repr` of the result of evaluating `expr`
347 the `repr` of the result of evaluating `expr`
348
348
349 The `callback` is called with the `repr()` of the result of `expr` as
349 The `callback` is called with the `repr()` of the result of `expr` as
350 first argument. To get the object, do `eval()` on the passed value.
350 first argument. To get the object, do `eval()` on the passed value.
351
351
352 See Also
352 See Also
353 --------
353 --------
354 _handle_exec_callback : private method, deal with calling callback with reply
354 _handle_exec_callback : private method, deal with calling callback with reply
355
355
356 """
356 """
357
357
358 # generate uuid, which would be used as an indication of whether or
358 # generate uuid, which would be used as an indication of whether or
359 # not the unique request originated from here (can use msg id ?)
359 # not the unique request originated from here (can use msg id ?)
360 local_uuid = str(uuid.uuid1())
360 local_uuid = str(uuid.uuid1())
361 msg_id = self.kernel_client.execute('',
361 msg_id = self.kernel_client.execute('',
362 silent=True, user_expressions={ local_uuid:expr })
362 silent=True, user_expressions={ local_uuid:expr })
363 self._callback_dict[local_uuid] = callback
363 self._callback_dict[local_uuid] = callback
364 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
364 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
365
365
366 def _handle_exec_callback(self, msg):
366 def _handle_exec_callback(self, msg):
367 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
367 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
368
368
369 Parameters
369 Parameters
370 ----------
370 ----------
371 msg : raw message send by the kernel containing an `user_expressions`
371 msg : raw message send by the kernel containing an `user_expressions`
372 and having a 'silent_exec_callback' kind.
372 and having a 'silent_exec_callback' kind.
373
373
374 Notes
374 Notes
375 -----
375 -----
376 This function will look for a `callback` associated with the
376 This function will look for a `callback` associated with the
377 corresponding message id. Association has been made by
377 corresponding message id. Association has been made by
378 `_silent_exec_callback`. `callback` is then called with the `repr()`
378 `_silent_exec_callback`. `callback` is then called with the `repr()`
379 of the value of corresponding `user_expressions` as argument.
379 of the value of corresponding `user_expressions` as argument.
380 `callback` is then removed from the known list so that any message
380 `callback` is then removed from the known list so that any message
381 coming again with the same id won't trigger it.
381 coming again with the same id won't trigger it.
382
382
383 """
383 """
384
384
385 user_exp = msg['content'].get('user_expressions')
385 user_exp = msg['content'].get('user_expressions')
386 if not user_exp:
386 if not user_exp:
387 return
387 return
388 for expression in user_exp:
388 for expression in user_exp:
389 if expression in self._callback_dict:
389 if expression in self._callback_dict:
390 self._callback_dict.pop(expression)(user_exp[expression])
390 self._callback_dict.pop(expression)(user_exp[expression])
391
391
392 def _handle_execute_reply(self, msg):
392 def _handle_execute_reply(self, msg):
393 """ Handles replies for code execution.
393 """ Handles replies for code execution.
394 """
394 """
395 self.log.debug("execute: %s", msg.get('content', ''))
395 self.log.debug("execute: %s", msg.get('content', ''))
396 msg_id = msg['parent_header']['msg_id']
396 msg_id = msg['parent_header']['msg_id']
397 info = self._request_info['execute'].get(msg_id)
397 info = self._request_info['execute'].get(msg_id)
398 # unset reading flag, because if execute finished, raw_input can't
398 # unset reading flag, because if execute finished, raw_input can't
399 # still be pending.
399 # still be pending.
400 self._reading = False
400 self._reading = False
401 if info and info.kind == 'user' and not self._hidden:
401 if info and info.kind == 'user' and not self._hidden:
402 # Make sure that all output from the SUB channel has been processed
402 # Make sure that all output from the SUB channel has been processed
403 # before writing a new prompt.
403 # before writing a new prompt.
404 self.kernel_client.iopub_channel.flush()
404 self.kernel_client.iopub_channel.flush()
405
405
406 # Reset the ANSI style information to prevent bad text in stdout
406 # Reset the ANSI style information to prevent bad text in stdout
407 # from messing up our colors. We're not a true terminal so we're
407 # from messing up our colors. We're not a true terminal so we're
408 # allowed to do this.
408 # allowed to do this.
409 if self.ansi_codes:
409 if self.ansi_codes:
410 self._ansi_processor.reset_sgr()
410 self._ansi_processor.reset_sgr()
411
411
412 content = msg['content']
412 content = msg['content']
413 status = content['status']
413 status = content['status']
414 if status == 'ok':
414 if status == 'ok':
415 self._process_execute_ok(msg)
415 self._process_execute_ok(msg)
416 elif status == 'error':
416 elif status == 'error':
417 self._process_execute_error(msg)
417 self._process_execute_error(msg)
418 elif status == 'aborted':
418 elif status == 'aborted':
419 self._process_execute_abort(msg)
419 self._process_execute_abort(msg)
420
420
421 self._show_interpreter_prompt_for_reply(msg)
421 self._show_interpreter_prompt_for_reply(msg)
422 self.executed.emit(msg)
422 self.executed.emit(msg)
423 self._request_info['execute'].pop(msg_id)
423 self._request_info['execute'].pop(msg_id)
424 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
424 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
425 self._handle_exec_callback(msg)
425 self._handle_exec_callback(msg)
426 self._request_info['execute'].pop(msg_id)
426 self._request_info['execute'].pop(msg_id)
427 else:
427 else:
428 super(FrontendWidget, self)._handle_execute_reply(msg)
428 super(FrontendWidget, self)._handle_execute_reply(msg)
429
429
430 def _handle_input_request(self, msg):
430 def _handle_input_request(self, msg):
431 """ Handle requests for raw_input.
431 """ Handle requests for raw_input.
432 """
432 """
433 self.log.debug("input: %s", msg.get('content', ''))
433 self.log.debug("input: %s", msg.get('content', ''))
434 if self._hidden:
434 if self._hidden:
435 raise RuntimeError('Request for raw input during hidden execution.')
435 raise RuntimeError('Request for raw input during hidden execution.')
436
436
437 # Make sure that all output from the SUB channel has been processed
437 # Make sure that all output from the SUB channel has been processed
438 # before entering readline mode.
438 # before entering readline mode.
439 self.kernel_client.iopub_channel.flush()
439 self.kernel_client.iopub_channel.flush()
440
440
441 def callback(line):
441 def callback(line):
442 self.kernel_client.stdin_channel.input(line)
442 self.kernel_client.stdin_channel.input(line)
443 if self._reading:
443 if self._reading:
444 self.log.debug("Got second input request, assuming first was interrupted.")
444 self.log.debug("Got second input request, assuming first was interrupted.")
445 self._reading = False
445 self._reading = False
446 self._readline(msg['content']['prompt'], callback=callback)
446 self._readline(msg['content']['prompt'], callback=callback)
447
447
448 def _kernel_restarted_message(self, died=True):
448 def _kernel_restarted_message(self, died=True):
449 msg = "Kernel died, restarting" if died else "Kernel restarting"
449 msg = "Kernel died, restarting" if died else "Kernel restarting"
450 self._append_html("<br>%s<hr><br>" % msg,
450 self._append_html("<br>%s<hr><br>" % msg,
451 before_prompt=False
451 before_prompt=False
452 )
452 )
453
453
454 def _handle_kernel_died(self, since_last_heartbeat):
454 def _handle_kernel_died(self, since_last_heartbeat):
455 """Handle the kernel's death (if we do not own the kernel).
455 """Handle the kernel's death (if we do not own the kernel).
456 """
456 """
457 self.log.warn("kernel died: %s", since_last_heartbeat)
457 self.log.warn("kernel died: %s", since_last_heartbeat)
458 if self.custom_restart:
458 if self.custom_restart:
459 self.custom_restart_kernel_died.emit(since_last_heartbeat)
459 self.custom_restart_kernel_died.emit(since_last_heartbeat)
460 else:
460 else:
461 self._kernel_restarted_message(died=True)
461 self._kernel_restarted_message(died=True)
462 self.reset()
462 self.reset()
463
463
464 def _handle_kernel_restarted(self, died=True):
464 def _handle_kernel_restarted(self, died=True):
465 """Notice that the autorestarter restarted the kernel.
465 """Notice that the autorestarter restarted the kernel.
466
466
467 There's nothing to do but show a message.
467 There's nothing to do but show a message.
468 """
468 """
469 self.log.warn("kernel restarted")
469 self.log.warn("kernel restarted")
470 self._kernel_restarted_message(died=died)
470 self._kernel_restarted_message(died=died)
471 self.reset()
471 self.reset()
472
472
473 def _handle_object_info_reply(self, rep):
473 def _handle_object_info_reply(self, rep):
474 """ Handle replies for call tips.
474 """ Handle replies for call tips.
475 """
475 """
476 self.log.debug("oinfo: %s", rep.get('content', ''))
476 self.log.debug("oinfo: %s", rep.get('content', ''))
477 cursor = self._get_cursor()
477 cursor = self._get_cursor()
478 info = self._request_info.get('call_tip')
478 info = self._request_info.get('call_tip')
479 if info and info.id == rep['parent_header']['msg_id'] and \
479 if info and info.id == rep['parent_header']['msg_id'] and \
480 info.pos == cursor.position():
480 info.pos == cursor.position():
481 # Get the information for a call tip. For now we format the call
481 # Get the information for a call tip. For now we format the call
482 # line as string, later we can pass False to format_call and
482 # line as string, later we can pass False to format_call and
483 # syntax-highlight it ourselves for nicer formatting in the
483 # syntax-highlight it ourselves for nicer formatting in the
484 # calltip.
484 # calltip.
485 content = rep['content']
485 content = rep['content']
486 # if this is from pykernel, 'docstring' will be the only key
486 # if this is from pykernel, 'docstring' will be the only key
487 if content.get('ismagic', False):
487 if content.get('ismagic', False):
488 # Don't generate a call-tip for magics. Ideally, we should
488 # Don't generate a call-tip for magics. Ideally, we should
489 # generate a tooltip, but not on ( like we do for actual
489 # generate a tooltip, but not on ( like we do for actual
490 # callables.
490 # callables.
491 call_info, doc = None, None
491 call_info, doc = None, None
492 else:
492 else:
493 call_info, doc = call_tip(content, format_call=True)
493 call_info, doc = call_tip(content, format_call=True)
494 if call_info or doc:
494 if call_info or doc:
495 self._call_tip_widget.show_call_info(call_info, doc)
495 self._call_tip_widget.show_call_info(call_info, doc)
496
496
497 def _handle_pyout(self, msg):
497 def _handle_pyout(self, msg):
498 """ Handle display hook output.
498 """ Handle display hook output.
499 """
499 """
500 self.log.debug("pyout: %s", msg.get('content', ''))
500 self.log.debug("pyout: %s", msg.get('content', ''))
501 if not self._hidden and self._is_from_this_session(msg):
501 if not self._hidden and self._is_from_this_session(msg):
502 text = msg['content']['data']
502 text = msg['content']['data']
503 self._append_plain_text(text + '\n', before_prompt=True)
503 self._append_plain_text(text + '\n', before_prompt=True)
504
504
505 def _handle_stream(self, msg):
505 def _handle_stream(self, msg):
506 """ Handle stdout, stderr, and stdin.
506 """ Handle stdout, stderr, and stdin.
507 """
507 """
508 self.log.debug("stream: %s", msg.get('content', ''))
508 self.log.debug("stream: %s", msg.get('content', ''))
509 if not self._hidden and self._is_from_this_session(msg):
509 if not self._hidden and self._is_from_this_session(msg):
510 # Most consoles treat tabs as being 8 space characters. Convert tabs
510 # Most consoles treat tabs as being 8 space characters. Convert tabs
511 # to spaces so that output looks as expected regardless of this
511 # to spaces so that output looks as expected regardless of this
512 # widget's tab width.
512 # widget's tab width.
513 text = msg['content']['data'].expandtabs(8)
513 text = msg['content']['data'].expandtabs(8)
514
514
515 self._append_plain_text(text, before_prompt=True)
515 self._append_plain_text(text, before_prompt=True)
516 self._control.moveCursor(QtGui.QTextCursor.End)
516 self._control.moveCursor(QtGui.QTextCursor.End)
517
517
518 def _handle_shutdown_reply(self, msg):
518 def _handle_shutdown_reply(self, msg):
519 """ Handle shutdown signal, only if from other console.
519 """ Handle shutdown signal, only if from other console.
520 """
520 """
521 self.log.warn("shutdown: %s", msg.get('content', ''))
521 self.log.warn("shutdown: %s", msg.get('content', ''))
522 restart = msg.get('content', {}).get('restart', False)
522 restart = msg.get('content', {}).get('restart', False)
523 if not self._hidden and not self._is_from_this_session(msg):
523 if not self._hidden and not self._is_from_this_session(msg):
524 # got shutdown reply, request came from session other than ours
524 # got shutdown reply, request came from session other than ours
525 if restart:
525 if restart:
526 # someone restarted the kernel, handle it
526 # someone restarted the kernel, handle it
527 self._handle_kernel_restarted(died=False)
527 self._handle_kernel_restarted(died=False)
528 else:
528 else:
529 # kernel was shutdown permanently
529 # kernel was shutdown permanently
530 # this triggers exit_requested if the kernel was local,
530 # this triggers exit_requested if the kernel was local,
531 # and a dialog if the kernel was remote,
531 # and a dialog if the kernel was remote,
532 # so we don't suddenly clear the qtconsole without asking.
532 # so we don't suddenly clear the qtconsole without asking.
533 if self._local_kernel:
533 if self._local_kernel:
534 self.exit_requested.emit(self)
534 self.exit_requested.emit(self)
535 else:
535 else:
536 title = self.window().windowTitle()
536 title = self.window().windowTitle()
537 reply = QtGui.QMessageBox.question(self, title,
537 reply = QtGui.QMessageBox.question(self, title,
538 "Kernel has been shutdown permanently. "
538 "Kernel has been shutdown permanently. "
539 "Close the Console?",
539 "Close the Console?",
540 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
540 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
541 if reply == QtGui.QMessageBox.Yes:
541 if reply == QtGui.QMessageBox.Yes:
542 self.exit_requested.emit(self)
542 self.exit_requested.emit(self)
543
543
544 def _handle_status(self, msg):
544 def _handle_status(self, msg):
545 """Handle status message"""
545 """Handle status message"""
546 # This is where a busy/idle indicator would be triggered,
546 # This is where a busy/idle indicator would be triggered,
547 # when we make one.
547 # when we make one.
548 state = msg['content'].get('execution_state', '')
548 state = msg['content'].get('execution_state', '')
549 if state == 'starting':
549 if state == 'starting':
550 # kernel started while we were running
550 # kernel started while we were running
551 if self._executing:
551 if self._executing:
552 self._handle_kernel_restarted(died=True)
552 self._handle_kernel_restarted(died=True)
553 elif state == 'idle':
553 elif state == 'idle':
554 pass
554 pass
555 elif state == 'busy':
555 elif state == 'busy':
556 pass
556 pass
557
557
558 def _started_channels(self):
558 def _started_channels(self):
559 """ Called when the KernelManager channels have started listening or
559 """ Called when the KernelManager channels have started listening or
560 when the frontend is assigned an already listening KernelManager.
560 when the frontend is assigned an already listening KernelManager.
561 """
561 """
562 self.reset(clear=True)
562 self.reset(clear=True)
563
563
564 #---------------------------------------------------------------------------
564 #---------------------------------------------------------------------------
565 # 'FrontendWidget' public interface
565 # 'FrontendWidget' public interface
566 #---------------------------------------------------------------------------
566 #---------------------------------------------------------------------------
567
567
568 def copy_raw(self):
568 def copy_raw(self):
569 """ Copy the currently selected text to the clipboard without attempting
569 """ Copy the currently selected text to the clipboard without attempting
570 to remove prompts or otherwise alter the text.
570 to remove prompts or otherwise alter the text.
571 """
571 """
572 self._control.copy()
572 self._control.copy()
573
573
574 def execute_file(self, path, hidden=False):
574 def execute_file(self, path, hidden=False):
575 """ Attempts to execute file with 'path'. If 'hidden', no output is
575 """ Attempts to execute file with 'path'. If 'hidden', no output is
576 shown.
576 shown.
577 """
577 """
578 self.execute('execfile(%r)' % path, hidden=hidden)
578 self.execute('execfile(%r)' % path, hidden=hidden)
579
579
580 def interrupt_kernel(self):
580 def interrupt_kernel(self):
581 """ Attempts to interrupt the running kernel.
581 """ Attempts to interrupt the running kernel.
582
582
583 Also unsets _reading flag, to avoid runtime errors
583 Also unsets _reading flag, to avoid runtime errors
584 if raw_input is called again.
584 if raw_input is called again.
585 """
585 """
586 if self.custom_interrupt:
586 if self.custom_interrupt:
587 self._reading = False
587 self._reading = False
588 self.custom_interrupt_requested.emit()
588 self.custom_interrupt_requested.emit()
589 elif self.kernel_manager:
589 elif self.kernel_manager:
590 self._reading = False
590 self._reading = False
591 self.kernel_manager.interrupt_kernel()
591 self.kernel_manager.interrupt_kernel()
592 else:
592 else:
593 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
593 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
594
594
595 def reset(self, clear=False):
595 def reset(self, clear=False):
596 """ Resets the widget to its initial state if ``clear`` parameter
596 """ Resets the widget to its initial state if ``clear`` parameter
597 is True, otherwise
597 is True, otherwise
598 prints a visual indication of the fact that the kernel restarted, but
598 prints a visual indication of the fact that the kernel restarted, but
599 does not clear the traces from previous usage of the kernel before it
599 does not clear the traces from previous usage of the kernel before it
600 was restarted. With ``clear=True``, it is similar to ``%clear``, but
600 was restarted. With ``clear=True``, it is similar to ``%clear``, but
601 also re-writes the banner and aborts execution if necessary.
601 also re-writes the banner and aborts execution if necessary.
602 """
602 """
603 if self._executing:
603 if self._executing:
604 self._executing = False
604 self._executing = False
605 self._request_info['execute'] = {}
605 self._request_info['execute'] = {}
606 self._reading = False
606 self._reading = False
607 self._highlighter.highlighting_on = False
607 self._highlighter.highlighting_on = False
608
608
609 if clear:
609 if clear:
610 self._control.clear()
610 self._control.clear()
611 self._append_plain_text(self.banner)
611 self._append_plain_text(self.banner)
612 # update output marker for stdout/stderr, so that startup
612 # update output marker for stdout/stderr, so that startup
613 # messages appear after banner:
613 # messages appear after banner:
614 self._append_before_prompt_pos = self._get_cursor().position()
614 self._append_before_prompt_pos = self._get_cursor().position()
615 self._show_interpreter_prompt()
615 self._show_interpreter_prompt()
616
616
617 def restart_kernel(self, message, now=False):
617 def restart_kernel(self, message, now=False):
618 """ Attempts to restart the running kernel.
618 """ Attempts to restart the running kernel.
619 """
619 """
620 # FIXME: now should be configurable via a checkbox in the dialog. Right
620 # FIXME: now should be configurable via a checkbox in the dialog. Right
621 # now at least the heartbeat path sets it to True and the manual restart
621 # now at least the heartbeat path sets it to True and the manual restart
622 # to False. But those should just be the pre-selected states of a
622 # to False. But those should just be the pre-selected states of a
623 # checkbox that the user could override if so desired. But I don't know
623 # checkbox that the user could override if so desired. But I don't know
624 # enough Qt to go implementing the checkbox now.
624 # enough Qt to go implementing the checkbox now.
625
625
626 if self.custom_restart:
626 if self.custom_restart:
627 self.custom_restart_requested.emit()
627 self.custom_restart_requested.emit()
628 return
628 return
629
629
630 if self.kernel_manager:
630 if self.kernel_manager:
631 # Pause the heart beat channel to prevent further warnings.
631 # Pause the heart beat channel to prevent further warnings.
632 self.kernel_client.hb_channel.pause()
632 self.kernel_client.hb_channel.pause()
633
633
634 # Prompt the user to restart the kernel. Un-pause the heartbeat if
634 # Prompt the user to restart the kernel. Un-pause the heartbeat if
635 # they decline. (If they accept, the heartbeat will be un-paused
635 # they decline. (If they accept, the heartbeat will be un-paused
636 # automatically when the kernel is restarted.)
636 # automatically when the kernel is restarted.)
637 if self.confirm_restart:
637 if self.confirm_restart:
638 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
638 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
639 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
639 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
640 message, buttons)
640 message, buttons)
641 do_restart = result == QtGui.QMessageBox.Yes
641 do_restart = result == QtGui.QMessageBox.Yes
642 else:
642 else:
643 # confirm_restart is False, so we don't need to ask user
643 # confirm_restart is False, so we don't need to ask user
644 # anything, just do the restart
644 # anything, just do the restart
645 do_restart = True
645 do_restart = True
646 if do_restart:
646 if do_restart:
647 try:
647 try:
648 self.kernel_manager.restart_kernel(now=now)
648 self.kernel_manager.restart_kernel(now=now)
649 except RuntimeError as e:
649 except RuntimeError as e:
650 self._append_plain_text(
650 self._append_plain_text(
651 'Error restarting kernel: %s\n' % e,
651 'Error restarting kernel: %s\n' % e,
652 before_prompt=True
652 before_prompt=True
653 )
653 )
654 else:
654 else:
655 self._append_html("<br>Restarting kernel...\n<hr><br>",
655 self._append_html("<br>Restarting kernel...\n<hr><br>",
656 before_prompt=True,
656 before_prompt=True,
657 )
657 )
658 else:
658 else:
659 self.kernel_client.hb_channel.unpause()
659 self.kernel_client.hb_channel.unpause()
660
660
661 else:
661 else:
662 self._append_plain_text(
662 self._append_plain_text(
663 'Cannot restart a Kernel I did not start\n',
663 'Cannot restart a Kernel I did not start\n',
664 before_prompt=True
664 before_prompt=True
665 )
665 )
666
666
667 #---------------------------------------------------------------------------
667 #---------------------------------------------------------------------------
668 # 'FrontendWidget' protected interface
668 # 'FrontendWidget' protected interface
669 #---------------------------------------------------------------------------
669 #---------------------------------------------------------------------------
670
670
671 def _call_tip(self):
671 def _call_tip(self):
672 """ Shows a call tip, if appropriate, at the current cursor location.
672 """ Shows a call tip, if appropriate, at the current cursor location.
673 """
673 """
674 # Decide if it makes sense to show a call tip
674 # Decide if it makes sense to show a call tip
675 if not self.enable_calltips:
675 if not self.enable_calltips:
676 return False
676 return False
677 cursor = self._get_cursor()
677 cursor = self._get_cursor()
678 cursor.movePosition(QtGui.QTextCursor.Left)
678 cursor.movePosition(QtGui.QTextCursor.Left)
679 if cursor.document().characterAt(cursor.position()) != '(':
679 if cursor.document().characterAt(cursor.position()) != '(':
680 return False
680 return False
681 context = self._get_context(cursor)
681 context = self._get_context(cursor)
682 if not context:
682 if not context:
683 return False
683 return False
684
684
685 # Send the metadata request to the kernel
685 # Send the metadata request to the kernel
686 name = '.'.join(context)
686 name = '.'.join(context)
687 msg_id = self.kernel_client.object_info(name)
687 msg_id = self.kernel_client.object_info(name)
688 pos = self._get_cursor().position()
688 pos = self._get_cursor().position()
689 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
689 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
690 return True
690 return True
691
691
692 def _complete(self):
692 def _complete(self):
693 """ Performs completion at the current cursor location.
693 """ Performs completion at the current cursor location.
694 """
694 """
695 context = self._get_context()
695 context = self._get_context()
696 if context:
696 if context:
697 # Send the completion request to the kernel
697 # Send the completion request to the kernel
698 msg_id = self.kernel_client.complete(
698 msg_id = self.kernel_client.complete(
699 '.'.join(context), # text
699 '.'.join(context), # text
700 self._get_input_buffer_cursor_line(), # line
700 self._get_input_buffer_cursor_line(), # line
701 self._get_input_buffer_cursor_column(), # cursor_pos
701 self._get_input_buffer_cursor_column(), # cursor_pos
702 self.input_buffer) # block
702 self.input_buffer) # block
703 pos = self._get_cursor().position()
703 pos = self._get_cursor().position()
704 info = self._CompletionRequest(msg_id, pos)
704 info = self._CompletionRequest(msg_id, pos)
705 self._request_info['complete'] = info
705 self._request_info['complete'] = info
706
706
707 def _get_context(self, cursor=None):
707 def _get_context(self, cursor=None):
708 """ Gets the context for the specified cursor (or the current cursor
708 """ Gets the context for the specified cursor (or the current cursor
709 if none is specified).
709 if none is specified).
710 """
710 """
711 if cursor is None:
711 if cursor is None:
712 cursor = self._get_cursor()
712 cursor = self._get_cursor()
713 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
713 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
714 QtGui.QTextCursor.KeepAnchor)
714 QtGui.QTextCursor.KeepAnchor)
715 text = cursor.selection().toPlainText()
715 text = cursor.selection().toPlainText()
716 return self._completion_lexer.get_context(text)
716 return self._completion_lexer.get_context(text)
717
717
718 def _process_execute_abort(self, msg):
718 def _process_execute_abort(self, msg):
719 """ Process a reply for an aborted execution request.
719 """ Process a reply for an aborted execution request.
720 """
720 """
721 self._append_plain_text("ERROR: execution aborted\n")
721 self._append_plain_text("ERROR: execution aborted\n")
722
722
723 def _process_execute_error(self, msg):
723 def _process_execute_error(self, msg):
724 """ Process a reply for an execution request that resulted in an error.
724 """ Process a reply for an execution request that resulted in an error.
725 """
725 """
726 content = msg['content']
726 content = msg['content']
727 # If a SystemExit is passed along, this means exit() was called - also
727 # If a SystemExit is passed along, this means exit() was called - also
728 # all the ipython %exit magic syntax of '-k' to be used to keep
728 # all the ipython %exit magic syntax of '-k' to be used to keep
729 # the kernel running
729 # the kernel running
730 if content['ename']=='SystemExit':
730 if content['ename']=='SystemExit':
731 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
731 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
732 self._keep_kernel_on_exit = keepkernel
732 self._keep_kernel_on_exit = keepkernel
733 self.exit_requested.emit(self)
733 self.exit_requested.emit(self)
734 else:
734 else:
735 traceback = ''.join(content['traceback'])
735 traceback = ''.join(content['traceback'])
736 self._append_plain_text(traceback)
736 self._append_plain_text(traceback)
737
737
738 def _process_execute_ok(self, msg):
738 def _process_execute_ok(self, msg):
739 """ Process a reply for a successful execution request.
739 """ Process a reply for a successful execution request.
740 """
740 """
741 payload = msg['content']['payload']
741 payload = msg['content']['payload']
742 for item in payload:
742 for item in payload:
743 if not self._process_execute_payload(item):
743 if not self._process_execute_payload(item):
744 warning = 'Warning: received unknown payload of type %s'
744 warning = 'Warning: received unknown payload of type %s'
745 print(warning % repr(item['source']))
745 print(warning % repr(item['source']))
746
746
747 def _process_execute_payload(self, item):
747 def _process_execute_payload(self, item):
748 """ Process a single payload item from the list of payload items in an
748 """ Process a single payload item from the list of payload items in an
749 execution reply. Returns whether the payload was handled.
749 execution reply. Returns whether the payload was handled.
750 """
750 """
751 # The basic FrontendWidget doesn't handle payloads, as they are a
751 # The basic FrontendWidget doesn't handle payloads, as they are a
752 # mechanism for going beyond the standard Python interpreter model.
752 # mechanism for going beyond the standard Python interpreter model.
753 return False
753 return False
754
754
755 def _show_interpreter_prompt(self):
755 def _show_interpreter_prompt(self):
756 """ Shows a prompt for the interpreter.
756 """ Shows a prompt for the interpreter.
757 """
757 """
758 self._show_prompt('>>> ')
758 self._show_prompt('>>> ')
759
759
760 def _show_interpreter_prompt_for_reply(self, msg):
760 def _show_interpreter_prompt_for_reply(self, msg):
761 """ Shows a prompt for the interpreter given an 'execute_reply' message.
761 """ Shows a prompt for the interpreter given an 'execute_reply' message.
762 """
762 """
763 self._show_interpreter_prompt()
763 self._show_interpreter_prompt()
764
764
765 #------ Signal handlers ----------------------------------------------------
765 #------ Signal handlers ----------------------------------------------------
766
766
767 def _document_contents_change(self, position, removed, added):
767 def _document_contents_change(self, position, removed, added):
768 """ Called whenever the document's content changes. Display a call tip
768 """ Called whenever the document's content changes. Display a call tip
769 if appropriate.
769 if appropriate.
770 """
770 """
771 # Calculate where the cursor should be *after* the change:
771 # Calculate where the cursor should be *after* the change:
772 position += added
772 position += added
773
773
774 document = self._control.document()
774 document = self._control.document()
775 if position == self._get_cursor().position():
775 if position == self._get_cursor().position():
776 self._call_tip()
776 self._call_tip()
777
777
778 #------ Trait default initializers -----------------------------------------
778 #------ Trait default initializers -----------------------------------------
779
779
780 def _banner_default(self):
780 def _banner_default(self):
781 """ Returns the standard Python banner.
781 """ Returns the standard Python banner.
782 """
782 """
783 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
783 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
784 '"license" for more information.'
784 '"license" for more information.'
785 return banner % (sys.version, sys.platform)
785 return banner % (sys.version, sys.platform)
@@ -1,388 +1,388 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14 * Paul Ivanov
14 * Paul Ivanov
15
15
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 # stdlib imports
22 # stdlib imports
23 import os
23 import os
24 import signal
24 import signal
25 import sys
25 import sys
26
26
27 # If run on Windows, install an exception hook which pops up a
27 # If run on Windows, install an exception hook which pops up a
28 # message box. Pythonw.exe hides the console, so without this
28 # message box. Pythonw.exe hides the console, so without this
29 # the application silently fails to load.
29 # the application silently fails to load.
30 #
30 #
31 # We always install this handler, because the expectation is for
31 # We always install this handler, because the expectation is for
32 # qtconsole to bring up a GUI even if called from the console.
32 # qtconsole to bring up a GUI even if called from the console.
33 # The old handler is called, so the exception is printed as well.
33 # The old handler is called, so the exception is printed as well.
34 # If desired, check for pythonw with an additional condition
34 # If desired, check for pythonw with an additional condition
35 # (sys.executable.lower().find('pythonw.exe') >= 0).
35 # (sys.executable.lower().find('pythonw.exe') >= 0).
36 if os.name == 'nt':
36 if os.name == 'nt':
37 old_excepthook = sys.excepthook
37 old_excepthook = sys.excepthook
38
38
39 def gui_excepthook(exctype, value, tb):
39 def gui_excepthook(exctype, value, tb):
40 try:
40 try:
41 import ctypes, traceback
41 import ctypes, traceback
42 MB_ICONERROR = 0x00000010L
42 MB_ICONERROR = 0x00000010L
43 title = u'Error starting IPython QtConsole'
43 title = u'Error starting IPython QtConsole'
44 msg = u''.join(traceback.format_exception(exctype, value, tb))
44 msg = u''.join(traceback.format_exception(exctype, value, tb))
45 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
45 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
46 finally:
46 finally:
47 # Also call the old exception hook to let it do
47 # Also call the old exception hook to let it do
48 # its thing too.
48 # its thing too.
49 old_excepthook(exctype, value, tb)
49 old_excepthook(exctype, value, tb)
50
50
51 sys.excepthook = gui_excepthook
51 sys.excepthook = gui_excepthook
52
52
53 # System library imports
53 # System library imports
54 from IPython.external.qt import QtCore, QtGui
54 from IPython.external.qt import QtCore, QtGui
55
55
56 # Local imports
56 # Local imports
57 from IPython.config.application import boolean_flag, catch_config_error
57 from IPython.config.application import boolean_flag, catch_config_error
58 from IPython.core.application import BaseIPythonApplication
58 from IPython.core.application import BaseIPythonApplication
59 from IPython.core.profiledir import ProfileDir
59 from IPython.core.profiledir import ProfileDir
60 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
60 from IPython.qt.console.ipython_widget import IPythonWidget
61 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
61 from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
62 from IPython.frontend.qt.console import styles
62 from IPython.qt.console import styles
63 from IPython.frontend.qt.console.mainwindow import MainWindow
63 from IPython.qt.console.mainwindow import MainWindow
64 from IPython.frontend.qt.client import QtKernelClient
64 from IPython.qt.client import QtKernelClient
65 from IPython.frontend.qt.manager import QtKernelManager
65 from IPython.qt.manager import QtKernelManager
66 from IPython.kernel import tunnel_to_kernel, find_connection_file
66 from IPython.kernel import tunnel_to_kernel, find_connection_file
67 from IPython.utils.traitlets import (
67 from IPython.utils.traitlets import (
68 Dict, List, Unicode, CBool, Any
68 Dict, List, Unicode, CBool, Any
69 )
69 )
70 from IPython.kernel.zmq.session import default_secure
70 from IPython.kernel.zmq.session import default_secure
71
71
72 from IPython.frontend.consoleapp import (
72 from IPython.consoleapp import (
73 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
73 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
74 )
74 )
75
75
76 #-----------------------------------------------------------------------------
76 #-----------------------------------------------------------------------------
77 # Network Constants
77 # Network Constants
78 #-----------------------------------------------------------------------------
78 #-----------------------------------------------------------------------------
79
79
80 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
80 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
81
81
82 #-----------------------------------------------------------------------------
82 #-----------------------------------------------------------------------------
83 # Globals
83 # Globals
84 #-----------------------------------------------------------------------------
84 #-----------------------------------------------------------------------------
85
85
86 _examples = """
86 _examples = """
87 ipython qtconsole # start the qtconsole
87 ipython qtconsole # start the qtconsole
88 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
88 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
89 """
89 """
90
90
91 #-----------------------------------------------------------------------------
91 #-----------------------------------------------------------------------------
92 # Aliases and Flags
92 # Aliases and Flags
93 #-----------------------------------------------------------------------------
93 #-----------------------------------------------------------------------------
94
94
95 # start with copy of flags
95 # start with copy of flags
96 flags = dict(flags)
96 flags = dict(flags)
97 qt_flags = {
97 qt_flags = {
98 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
98 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
99 "Disable rich text support."),
99 "Disable rich text support."),
100 }
100 }
101
101
102 # and app_flags from the Console Mixin
102 # and app_flags from the Console Mixin
103 qt_flags.update(app_flags)
103 qt_flags.update(app_flags)
104 # add frontend flags to the full set
104 # add frontend flags to the full set
105 flags.update(qt_flags)
105 flags.update(qt_flags)
106
106
107 # start with copy of front&backend aliases list
107 # start with copy of front&backend aliases list
108 aliases = dict(aliases)
108 aliases = dict(aliases)
109 qt_aliases = dict(
109 qt_aliases = dict(
110 style = 'IPythonWidget.syntax_style',
110 style = 'IPythonWidget.syntax_style',
111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
111 stylesheet = 'IPythonQtConsoleApp.stylesheet',
112 colors = 'ZMQInteractiveShell.colors',
112 colors = 'ZMQInteractiveShell.colors',
113
113
114 editor = 'IPythonWidget.editor',
114 editor = 'IPythonWidget.editor',
115 paging = 'ConsoleWidget.paging',
115 paging = 'ConsoleWidget.paging',
116 )
116 )
117 # and app_aliases from the Console Mixin
117 # and app_aliases from the Console Mixin
118 qt_aliases.update(app_aliases)
118 qt_aliases.update(app_aliases)
119 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
119 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
120 # add frontend aliases to the full set
120 # add frontend aliases to the full set
121 aliases.update(qt_aliases)
121 aliases.update(qt_aliases)
122
122
123 # get flags&aliases into sets, and remove a couple that
123 # get flags&aliases into sets, and remove a couple that
124 # shouldn't be scrubbed from backend flags:
124 # shouldn't be scrubbed from backend flags:
125 qt_aliases = set(qt_aliases.keys())
125 qt_aliases = set(qt_aliases.keys())
126 qt_aliases.remove('colors')
126 qt_aliases.remove('colors')
127 qt_flags = set(qt_flags.keys())
127 qt_flags = set(qt_flags.keys())
128
128
129 #-----------------------------------------------------------------------------
129 #-----------------------------------------------------------------------------
130 # Classes
130 # Classes
131 #-----------------------------------------------------------------------------
131 #-----------------------------------------------------------------------------
132
132
133 #-----------------------------------------------------------------------------
133 #-----------------------------------------------------------------------------
134 # IPythonQtConsole
134 # IPythonQtConsole
135 #-----------------------------------------------------------------------------
135 #-----------------------------------------------------------------------------
136
136
137
137
138 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
138 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
139 name = 'ipython-qtconsole'
139 name = 'ipython-qtconsole'
140
140
141 description = """
141 description = """
142 The IPython QtConsole.
142 The IPython QtConsole.
143
143
144 This launches a Console-style application using Qt. It is not a full
144 This launches a Console-style application using Qt. It is not a full
145 console, in that launched terminal subprocesses will not be able to accept
145 console, in that launched terminal subprocesses will not be able to accept
146 input.
146 input.
147
147
148 The QtConsole supports various extra features beyond the Terminal IPython
148 The QtConsole supports various extra features beyond the Terminal IPython
149 shell, such as inline plotting with matplotlib, via:
149 shell, such as inline plotting with matplotlib, via:
150
150
151 ipython qtconsole --pylab=inline
151 ipython qtconsole --pylab=inline
152
152
153 as well as saving your session as HTML, and printing the output.
153 as well as saving your session as HTML, and printing the output.
154
154
155 """
155 """
156 examples = _examples
156 examples = _examples
157
157
158 classes = [IPythonWidget] + IPythonConsoleApp.classes
158 classes = [IPythonWidget] + IPythonConsoleApp.classes
159 flags = Dict(flags)
159 flags = Dict(flags)
160 aliases = Dict(aliases)
160 aliases = Dict(aliases)
161 frontend_flags = Any(qt_flags)
161 frontend_flags = Any(qt_flags)
162 frontend_aliases = Any(qt_aliases)
162 frontend_aliases = Any(qt_aliases)
163 kernel_client_class = QtKernelClient
163 kernel_client_class = QtKernelClient
164 kernel_manager_class = QtKernelManager
164 kernel_manager_class = QtKernelManager
165
165
166 stylesheet = Unicode('', config=True,
166 stylesheet = Unicode('', config=True,
167 help="path to a custom CSS stylesheet")
167 help="path to a custom CSS stylesheet")
168
168
169 hide_menubar = CBool(False, config=True,
169 hide_menubar = CBool(False, config=True,
170 help="Start the console window with the menu bar hidden.")
170 help="Start the console window with the menu bar hidden.")
171
171
172 maximize = CBool(False, config=True,
172 maximize = CBool(False, config=True,
173 help="Start the console window maximized.")
173 help="Start the console window maximized.")
174
174
175 plain = CBool(False, config=True,
175 plain = CBool(False, config=True,
176 help="Use a plaintext widget instead of rich text (plain can't print/save).")
176 help="Use a plaintext widget instead of rich text (plain can't print/save).")
177
177
178 def _plain_changed(self, name, old, new):
178 def _plain_changed(self, name, old, new):
179 kind = 'plain' if new else 'rich'
179 kind = 'plain' if new else 'rich'
180 self.config.ConsoleWidget.kind = kind
180 self.config.ConsoleWidget.kind = kind
181 if new:
181 if new:
182 self.widget_factory = IPythonWidget
182 self.widget_factory = IPythonWidget
183 else:
183 else:
184 self.widget_factory = RichIPythonWidget
184 self.widget_factory = RichIPythonWidget
185
185
186 # the factory for creating a widget
186 # the factory for creating a widget
187 widget_factory = Any(RichIPythonWidget)
187 widget_factory = Any(RichIPythonWidget)
188
188
189 def parse_command_line(self, argv=None):
189 def parse_command_line(self, argv=None):
190 super(IPythonQtConsoleApp, self).parse_command_line(argv)
190 super(IPythonQtConsoleApp, self).parse_command_line(argv)
191 self.build_kernel_argv(argv)
191 self.build_kernel_argv(argv)
192
192
193
193
194 def new_frontend_master(self):
194 def new_frontend_master(self):
195 """ Create and return new frontend attached to new kernel, launched on localhost.
195 """ Create and return new frontend attached to new kernel, launched on localhost.
196 """
196 """
197 kernel_manager = self.kernel_manager_class(
197 kernel_manager = self.kernel_manager_class(
198 connection_file=self._new_connection_file(),
198 connection_file=self._new_connection_file(),
199 config=self.config,
199 config=self.config,
200 autorestart=True,
200 autorestart=True,
201 )
201 )
202 # start the kernel
202 # start the kernel
203 kwargs = dict()
203 kwargs = dict()
204 kwargs['extra_arguments'] = self.kernel_argv
204 kwargs['extra_arguments'] = self.kernel_argv
205 kernel_manager.start_kernel(**kwargs)
205 kernel_manager.start_kernel(**kwargs)
206 kernel_manager.client_factory = self.kernel_client_class
206 kernel_manager.client_factory = self.kernel_client_class
207 kernel_client = kernel_manager.client()
207 kernel_client = kernel_manager.client()
208 kernel_client.start_channels(shell=True, iopub=True)
208 kernel_client.start_channels(shell=True, iopub=True)
209 widget = self.widget_factory(config=self.config,
209 widget = self.widget_factory(config=self.config,
210 local_kernel=True)
210 local_kernel=True)
211 self.init_colors(widget)
211 self.init_colors(widget)
212 widget.kernel_manager = kernel_manager
212 widget.kernel_manager = kernel_manager
213 widget.kernel_client = kernel_client
213 widget.kernel_client = kernel_client
214 widget._existing = False
214 widget._existing = False
215 widget._may_close = True
215 widget._may_close = True
216 widget._confirm_exit = self.confirm_exit
216 widget._confirm_exit = self.confirm_exit
217 return widget
217 return widget
218
218
219 def new_frontend_slave(self, current_widget):
219 def new_frontend_slave(self, current_widget):
220 """Create and return a new frontend attached to an existing kernel.
220 """Create and return a new frontend attached to an existing kernel.
221
221
222 Parameters
222 Parameters
223 ----------
223 ----------
224 current_widget : IPythonWidget
224 current_widget : IPythonWidget
225 The IPythonWidget whose kernel this frontend is to share
225 The IPythonWidget whose kernel this frontend is to share
226 """
226 """
227 kernel_client = self.kernel_client_class(
227 kernel_client = self.kernel_client_class(
228 connection_file=current_widget.kernel_client.connection_file,
228 connection_file=current_widget.kernel_client.connection_file,
229 config = self.config,
229 config = self.config,
230 )
230 )
231 kernel_client.load_connection_file()
231 kernel_client.load_connection_file()
232 kernel_client.start_channels()
232 kernel_client.start_channels()
233 widget = self.widget_factory(config=self.config,
233 widget = self.widget_factory(config=self.config,
234 local_kernel=False)
234 local_kernel=False)
235 self.init_colors(widget)
235 self.init_colors(widget)
236 widget._existing = True
236 widget._existing = True
237 widget._may_close = False
237 widget._may_close = False
238 widget._confirm_exit = False
238 widget._confirm_exit = False
239 widget.kernel_client = kernel_client
239 widget.kernel_client = kernel_client
240 widget.kernel_manager = current_widget.kernel_manager
240 widget.kernel_manager = current_widget.kernel_manager
241 return widget
241 return widget
242
242
243 def init_qt_app(self):
243 def init_qt_app(self):
244 # separate from qt_elements, because it must run first
244 # separate from qt_elements, because it must run first
245 self.app = QtGui.QApplication([])
245 self.app = QtGui.QApplication([])
246
246
247 def init_qt_elements(self):
247 def init_qt_elements(self):
248 # Create the widget.
248 # Create the widget.
249
249
250 base_path = os.path.abspath(os.path.dirname(__file__))
250 base_path = os.path.abspath(os.path.dirname(__file__))
251 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
251 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
252 self.app.icon = QtGui.QIcon(icon_path)
252 self.app.icon = QtGui.QIcon(icon_path)
253 QtGui.QApplication.setWindowIcon(self.app.icon)
253 QtGui.QApplication.setWindowIcon(self.app.icon)
254
254
255 try:
255 try:
256 ip = self.config.KernelManager.ip
256 ip = self.config.KernelManager.ip
257 except AttributeError:
257 except AttributeError:
258 ip = LOCALHOST
258 ip = LOCALHOST
259 local_kernel = (not self.existing) or ip in LOCAL_IPS
259 local_kernel = (not self.existing) or ip in LOCAL_IPS
260 self.widget = self.widget_factory(config=self.config,
260 self.widget = self.widget_factory(config=self.config,
261 local_kernel=local_kernel)
261 local_kernel=local_kernel)
262 self.init_colors(self.widget)
262 self.init_colors(self.widget)
263 self.widget._existing = self.existing
263 self.widget._existing = self.existing
264 self.widget._may_close = not self.existing
264 self.widget._may_close = not self.existing
265 self.widget._confirm_exit = self.confirm_exit
265 self.widget._confirm_exit = self.confirm_exit
266
266
267 self.widget.kernel_manager = self.kernel_manager
267 self.widget.kernel_manager = self.kernel_manager
268 self.widget.kernel_client = self.kernel_client
268 self.widget.kernel_client = self.kernel_client
269 self.window = MainWindow(self.app,
269 self.window = MainWindow(self.app,
270 confirm_exit=self.confirm_exit,
270 confirm_exit=self.confirm_exit,
271 new_frontend_factory=self.new_frontend_master,
271 new_frontend_factory=self.new_frontend_master,
272 slave_frontend_factory=self.new_frontend_slave,
272 slave_frontend_factory=self.new_frontend_slave,
273 )
273 )
274 self.window.log = self.log
274 self.window.log = self.log
275 self.window.add_tab_with_frontend(self.widget)
275 self.window.add_tab_with_frontend(self.widget)
276 self.window.init_menu_bar()
276 self.window.init_menu_bar()
277
277
278 # Ignore on OSX, where there is always a menu bar
278 # Ignore on OSX, where there is always a menu bar
279 if sys.platform != 'darwin' and self.hide_menubar:
279 if sys.platform != 'darwin' and self.hide_menubar:
280 self.window.menuBar().setVisible(False)
280 self.window.menuBar().setVisible(False)
281
281
282 self.window.setWindowTitle('IPython')
282 self.window.setWindowTitle('IPython')
283
283
284 def init_colors(self, widget):
284 def init_colors(self, widget):
285 """Configure the coloring of the widget"""
285 """Configure the coloring of the widget"""
286 # Note: This will be dramatically simplified when colors
286 # Note: This will be dramatically simplified when colors
287 # are removed from the backend.
287 # are removed from the backend.
288
288
289 # parse the colors arg down to current known labels
289 # parse the colors arg down to current known labels
290 try:
290 try:
291 colors = self.config.ZMQInteractiveShell.colors
291 colors = self.config.ZMQInteractiveShell.colors
292 except AttributeError:
292 except AttributeError:
293 colors = None
293 colors = None
294 try:
294 try:
295 style = self.config.IPythonWidget.syntax_style
295 style = self.config.IPythonWidget.syntax_style
296 except AttributeError:
296 except AttributeError:
297 style = None
297 style = None
298 try:
298 try:
299 sheet = self.config.IPythonWidget.style_sheet
299 sheet = self.config.IPythonWidget.style_sheet
300 except AttributeError:
300 except AttributeError:
301 sheet = None
301 sheet = None
302
302
303 # find the value for colors:
303 # find the value for colors:
304 if colors:
304 if colors:
305 colors=colors.lower()
305 colors=colors.lower()
306 if colors in ('lightbg', 'light'):
306 if colors in ('lightbg', 'light'):
307 colors='lightbg'
307 colors='lightbg'
308 elif colors in ('dark', 'linux'):
308 elif colors in ('dark', 'linux'):
309 colors='linux'
309 colors='linux'
310 else:
310 else:
311 colors='nocolor'
311 colors='nocolor'
312 elif style:
312 elif style:
313 if style=='bw':
313 if style=='bw':
314 colors='nocolor'
314 colors='nocolor'
315 elif styles.dark_style(style):
315 elif styles.dark_style(style):
316 colors='linux'
316 colors='linux'
317 else:
317 else:
318 colors='lightbg'
318 colors='lightbg'
319 else:
319 else:
320 colors=None
320 colors=None
321
321
322 # Configure the style
322 # Configure the style
323 if style:
323 if style:
324 widget.style_sheet = styles.sheet_from_template(style, colors)
324 widget.style_sheet = styles.sheet_from_template(style, colors)
325 widget.syntax_style = style
325 widget.syntax_style = style
326 widget._syntax_style_changed()
326 widget._syntax_style_changed()
327 widget._style_sheet_changed()
327 widget._style_sheet_changed()
328 elif colors:
328 elif colors:
329 # use a default dark/light/bw style
329 # use a default dark/light/bw style
330 widget.set_default_style(colors=colors)
330 widget.set_default_style(colors=colors)
331
331
332 if self.stylesheet:
332 if self.stylesheet:
333 # we got an explicit stylesheet
333 # we got an explicit stylesheet
334 if os.path.isfile(self.stylesheet):
334 if os.path.isfile(self.stylesheet):
335 with open(self.stylesheet) as f:
335 with open(self.stylesheet) as f:
336 sheet = f.read()
336 sheet = f.read()
337 else:
337 else:
338 raise IOError("Stylesheet %r not found." % self.stylesheet)
338 raise IOError("Stylesheet %r not found." % self.stylesheet)
339 if sheet:
339 if sheet:
340 widget.style_sheet = sheet
340 widget.style_sheet = sheet
341 widget._style_sheet_changed()
341 widget._style_sheet_changed()
342
342
343
343
344 def init_signal(self):
344 def init_signal(self):
345 """allow clean shutdown on sigint"""
345 """allow clean shutdown on sigint"""
346 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
346 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
347 # need a timer, so that QApplication doesn't block until a real
347 # need a timer, so that QApplication doesn't block until a real
348 # Qt event fires (can require mouse movement)
348 # Qt event fires (can require mouse movement)
349 # timer trick from http://stackoverflow.com/q/4938723/938949
349 # timer trick from http://stackoverflow.com/q/4938723/938949
350 timer = QtCore.QTimer()
350 timer = QtCore.QTimer()
351 # Let the interpreter run each 200 ms:
351 # Let the interpreter run each 200 ms:
352 timer.timeout.connect(lambda: None)
352 timer.timeout.connect(lambda: None)
353 timer.start(200)
353 timer.start(200)
354 # hold onto ref, so the timer doesn't get cleaned up
354 # hold onto ref, so the timer doesn't get cleaned up
355 self._sigint_timer = timer
355 self._sigint_timer = timer
356
356
357 @catch_config_error
357 @catch_config_error
358 def initialize(self, argv=None):
358 def initialize(self, argv=None):
359 self.init_qt_app()
359 self.init_qt_app()
360 super(IPythonQtConsoleApp, self).initialize(argv)
360 super(IPythonQtConsoleApp, self).initialize(argv)
361 IPythonConsoleApp.initialize(self,argv)
361 IPythonConsoleApp.initialize(self,argv)
362 self.init_qt_elements()
362 self.init_qt_elements()
363 self.init_signal()
363 self.init_signal()
364
364
365 def start(self):
365 def start(self):
366
366
367 # draw the window
367 # draw the window
368 if self.maximize:
368 if self.maximize:
369 self.window.showMaximized()
369 self.window.showMaximized()
370 else:
370 else:
371 self.window.show()
371 self.window.show()
372 self.window.raise_()
372 self.window.raise_()
373
373
374 # Start the application main loop.
374 # Start the application main loop.
375 self.app.exec_()
375 self.app.exec_()
376
376
377 #-----------------------------------------------------------------------------
377 #-----------------------------------------------------------------------------
378 # Main entry point
378 # Main entry point
379 #-----------------------------------------------------------------------------
379 #-----------------------------------------------------------------------------
380
380
381 def main():
381 def main():
382 app = IPythonQtConsoleApp()
382 app = IPythonQtConsoleApp()
383 app.initialize()
383 app.initialize()
384 app.start()
384 app.start()
385
385
386
386
387 if __name__ == '__main__':
387 if __name__ == '__main__':
388 main()
388 main()
@@ -1,325 +1,325 b''
1 #-----------------------------------------------------------------------------
1 #-----------------------------------------------------------------------------
2 # Copyright (c) 2010, IPython Development Team.
2 # Copyright (c) 2010, IPython Development Team.
3 #
3 #
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5 #
5 #
6 # The full license is in the file COPYING.txt, distributed with this software.
6 # The full license is in the file COPYING.txt, distributed with this software.
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard libary imports.
9 # Standard libary imports.
10 from base64 import decodestring
10 from base64 import decodestring
11 import os
11 import os
12 import re
12 import re
13
13
14 # System libary imports.
14 # System libary imports.
15 from IPython.external.qt import QtCore, QtGui
15 from IPython.external.qt import QtCore, QtGui
16
16
17 # Local imports
17 # Local imports
18 from IPython.utils.traitlets import Bool
18 from IPython.utils.traitlets import Bool
19 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
19 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
20 from ipython_widget import IPythonWidget
20 from ipython_widget import IPythonWidget
21
21
22
22
23 class RichIPythonWidget(IPythonWidget):
23 class RichIPythonWidget(IPythonWidget):
24 """ An IPythonWidget that supports rich text, including lists, images, and
24 """ An IPythonWidget that supports rich text, including lists, images, and
25 tables. Note that raw performance will be reduced compared to the plain
25 tables. Note that raw performance will be reduced compared to the plain
26 text version.
26 text version.
27 """
27 """
28
28
29 # RichIPythonWidget protected class variables.
29 # RichIPythonWidget protected class variables.
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
30 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
31 _jpg_supported = Bool(False)
31 _jpg_supported = Bool(False)
32
32
33 # Used to determine whether a given html export attempt has already
33 # Used to determine whether a given html export attempt has already
34 # displayed a warning about being unable to convert a png to svg.
34 # displayed a warning about being unable to convert a png to svg.
35 _svg_warning_displayed = False
35 _svg_warning_displayed = False
36
36
37 #---------------------------------------------------------------------------
37 #---------------------------------------------------------------------------
38 # 'object' interface
38 # 'object' interface
39 #---------------------------------------------------------------------------
39 #---------------------------------------------------------------------------
40
40
41 def __init__(self, *args, **kw):
41 def __init__(self, *args, **kw):
42 """ Create a RichIPythonWidget.
42 """ Create a RichIPythonWidget.
43 """
43 """
44 kw['kind'] = 'rich'
44 kw['kind'] = 'rich'
45 super(RichIPythonWidget, self).__init__(*args, **kw)
45 super(RichIPythonWidget, self).__init__(*args, **kw)
46
46
47 # Configure the ConsoleWidget HTML exporter for our formats.
47 # Configure the ConsoleWidget HTML exporter for our formats.
48 self._html_exporter.image_tag = self._get_image_tag
48 self._html_exporter.image_tag = self._get_image_tag
49
49
50 # Dictionary for resolving document resource names to SVG data.
50 # Dictionary for resolving document resource names to SVG data.
51 self._name_to_svg_map = {}
51 self._name_to_svg_map = {}
52
52
53 # Do we support jpg ?
53 # Do we support jpg ?
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
54 # it seems that sometime jpg support is a plugin of QT, so try to assume
55 # it is not always supported.
55 # it is not always supported.
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
56 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
57 self._jpg_supported = 'jpeg' in _supported_format
57 self._jpg_supported = 'jpeg' in _supported_format
58
58
59
59
60 #---------------------------------------------------------------------------
60 #---------------------------------------------------------------------------
61 # 'ConsoleWidget' public interface overides
61 # 'ConsoleWidget' public interface overides
62 #---------------------------------------------------------------------------
62 #---------------------------------------------------------------------------
63
63
64 def export_html(self):
64 def export_html(self):
65 """ Shows a dialog to export HTML/XML in various formats.
65 """ Shows a dialog to export HTML/XML in various formats.
66
66
67 Overridden in order to reset the _svg_warning_displayed flag prior
67 Overridden in order to reset the _svg_warning_displayed flag prior
68 to the export running.
68 to the export running.
69 """
69 """
70 self._svg_warning_displayed = False
70 self._svg_warning_displayed = False
71 super(RichIPythonWidget, self).export_html()
71 super(RichIPythonWidget, self).export_html()
72
72
73
73
74 #---------------------------------------------------------------------------
74 #---------------------------------------------------------------------------
75 # 'ConsoleWidget' protected interface
75 # 'ConsoleWidget' protected interface
76 #---------------------------------------------------------------------------
76 #---------------------------------------------------------------------------
77
77
78 def _context_menu_make(self, pos):
78 def _context_menu_make(self, pos):
79 """ Reimplemented to return a custom context menu for images.
79 """ Reimplemented to return a custom context menu for images.
80 """
80 """
81 format = self._control.cursorForPosition(pos).charFormat()
81 format = self._control.cursorForPosition(pos).charFormat()
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
82 name = format.stringProperty(QtGui.QTextFormat.ImageName)
83 if name:
83 if name:
84 menu = QtGui.QMenu()
84 menu = QtGui.QMenu()
85
85
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
86 menu.addAction('Copy Image', lambda: self._copy_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
87 menu.addAction('Save Image As...', lambda: self._save_image(name))
88 menu.addSeparator()
88 menu.addSeparator()
89
89
90 svg = self._name_to_svg_map.get(name, None)
90 svg = self._name_to_svg_map.get(name, None)
91 if svg is not None:
91 if svg is not None:
92 menu.addSeparator()
92 menu.addSeparator()
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
93 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
94 menu.addAction('Save SVG As...',
94 menu.addAction('Save SVG As...',
95 lambda: save_svg(svg, self._control))
95 lambda: save_svg(svg, self._control))
96 else:
96 else:
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
97 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
98 return menu
98 return menu
99
99
100 #---------------------------------------------------------------------------
100 #---------------------------------------------------------------------------
101 # 'BaseFrontendMixin' abstract interface
101 # 'BaseFrontendMixin' abstract interface
102 #---------------------------------------------------------------------------
102 #---------------------------------------------------------------------------
103 def _pre_image_append(self, msg, prompt_number):
103 def _pre_image_append(self, msg, prompt_number):
104 """ Append the Out[] prompt and make the output nicer
104 """ Append the Out[] prompt and make the output nicer
105
105
106 Shared code for some the following if statement
106 Shared code for some the following if statement
107 """
107 """
108 self.log.debug("pyout: %s", msg.get('content', ''))
108 self.log.debug("pyout: %s", msg.get('content', ''))
109 self._append_plain_text(self.output_sep, True)
109 self._append_plain_text(self.output_sep, True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
110 self._append_html(self._make_out_prompt(prompt_number), True)
111 self._append_plain_text('\n', True)
111 self._append_plain_text('\n', True)
112
112
113 def _handle_pyout(self, msg):
113 def _handle_pyout(self, msg):
114 """ Overridden to handle rich data types, like SVG.
114 """ Overridden to handle rich data types, like SVG.
115 """
115 """
116 if not self._hidden and self._is_from_this_session(msg):
116 if not self._hidden and self._is_from_this_session(msg):
117 content = msg['content']
117 content = msg['content']
118 prompt_number = content.get('execution_count', 0)
118 prompt_number = content.get('execution_count', 0)
119 data = content['data']
119 data = content['data']
120 if 'image/svg+xml' in data:
120 if 'image/svg+xml' in data:
121 self._pre_image_append(msg, prompt_number)
121 self._pre_image_append(msg, prompt_number)
122 self._append_svg(data['image/svg+xml'], True)
122 self._append_svg(data['image/svg+xml'], True)
123 self._append_html(self.output_sep2, True)
123 self._append_html(self.output_sep2, True)
124 elif 'image/png' in data:
124 elif 'image/png' in data:
125 self._pre_image_append(msg, prompt_number)
125 self._pre_image_append(msg, prompt_number)
126 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
126 self._append_png(decodestring(data['image/png'].encode('ascii')), True)
127 self._append_html(self.output_sep2, True)
127 self._append_html(self.output_sep2, True)
128 elif 'image/jpeg' in data and self._jpg_supported:
128 elif 'image/jpeg' in data and self._jpg_supported:
129 self._pre_image_append(msg, prompt_number)
129 self._pre_image_append(msg, prompt_number)
130 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
130 self._append_jpg(decodestring(data['image/jpeg'].encode('ascii')), True)
131 self._append_html(self.output_sep2, True)
131 self._append_html(self.output_sep2, True)
132 else:
132 else:
133 # Default back to the plain text representation.
133 # Default back to the plain text representation.
134 return super(RichIPythonWidget, self)._handle_pyout(msg)
134 return super(RichIPythonWidget, self)._handle_pyout(msg)
135
135
136 def _handle_display_data(self, msg):
136 def _handle_display_data(self, msg):
137 """ Overridden to handle rich data types, like SVG.
137 """ Overridden to handle rich data types, like SVG.
138 """
138 """
139 if not self._hidden and self._is_from_this_session(msg):
139 if not self._hidden and self._is_from_this_session(msg):
140 source = msg['content']['source']
140 source = msg['content']['source']
141 data = msg['content']['data']
141 data = msg['content']['data']
142 metadata = msg['content']['metadata']
142 metadata = msg['content']['metadata']
143 # Try to use the svg or html representations.
143 # Try to use the svg or html representations.
144 # FIXME: Is this the right ordering of things to try?
144 # FIXME: Is this the right ordering of things to try?
145 if 'image/svg+xml' in data:
145 if 'image/svg+xml' in data:
146 self.log.debug("display: %s", msg.get('content', ''))
146 self.log.debug("display: %s", msg.get('content', ''))
147 svg = data['image/svg+xml']
147 svg = data['image/svg+xml']
148 self._append_svg(svg, True)
148 self._append_svg(svg, True)
149 elif 'image/png' in data:
149 elif 'image/png' in data:
150 self.log.debug("display: %s", msg.get('content', ''))
150 self.log.debug("display: %s", msg.get('content', ''))
151 # PNG data is base64 encoded as it passes over the network
151 # PNG data is base64 encoded as it passes over the network
152 # in a JSON structure so we decode it.
152 # in a JSON structure so we decode it.
153 png = decodestring(data['image/png'].encode('ascii'))
153 png = decodestring(data['image/png'].encode('ascii'))
154 self._append_png(png, True)
154 self._append_png(png, True)
155 elif 'image/jpeg' in data and self._jpg_supported:
155 elif 'image/jpeg' in data and self._jpg_supported:
156 self.log.debug("display: %s", msg.get('content', ''))
156 self.log.debug("display: %s", msg.get('content', ''))
157 jpg = decodestring(data['image/jpeg'].encode('ascii'))
157 jpg = decodestring(data['image/jpeg'].encode('ascii'))
158 self._append_jpg(jpg, True)
158 self._append_jpg(jpg, True)
159 else:
159 else:
160 # Default back to the plain text representation.
160 # Default back to the plain text representation.
161 return super(RichIPythonWidget, self)._handle_display_data(msg)
161 return super(RichIPythonWidget, self)._handle_display_data(msg)
162
162
163 #---------------------------------------------------------------------------
163 #---------------------------------------------------------------------------
164 # 'RichIPythonWidget' protected interface
164 # 'RichIPythonWidget' protected interface
165 #---------------------------------------------------------------------------
165 #---------------------------------------------------------------------------
166
166
167 def _append_jpg(self, jpg, before_prompt=False):
167 def _append_jpg(self, jpg, before_prompt=False):
168 """ Append raw JPG data to the widget."""
168 """ Append raw JPG data to the widget."""
169 self._append_custom(self._insert_jpg, jpg, before_prompt)
169 self._append_custom(self._insert_jpg, jpg, before_prompt)
170
170
171 def _append_png(self, png, before_prompt=False):
171 def _append_png(self, png, before_prompt=False):
172 """ Append raw PNG data to the widget.
172 """ Append raw PNG data to the widget.
173 """
173 """
174 self._append_custom(self._insert_png, png, before_prompt)
174 self._append_custom(self._insert_png, png, before_prompt)
175
175
176 def _append_svg(self, svg, before_prompt=False):
176 def _append_svg(self, svg, before_prompt=False):
177 """ Append raw SVG data to the widget.
177 """ Append raw SVG data to the widget.
178 """
178 """
179 self._append_custom(self._insert_svg, svg, before_prompt)
179 self._append_custom(self._insert_svg, svg, before_prompt)
180
180
181 def _add_image(self, image):
181 def _add_image(self, image):
182 """ Adds the specified QImage to the document and returns a
182 """ Adds the specified QImage to the document and returns a
183 QTextImageFormat that references it.
183 QTextImageFormat that references it.
184 """
184 """
185 document = self._control.document()
185 document = self._control.document()
186 name = str(image.cacheKey())
186 name = str(image.cacheKey())
187 document.addResource(QtGui.QTextDocument.ImageResource,
187 document.addResource(QtGui.QTextDocument.ImageResource,
188 QtCore.QUrl(name), image)
188 QtCore.QUrl(name), image)
189 format = QtGui.QTextImageFormat()
189 format = QtGui.QTextImageFormat()
190 format.setName(name)
190 format.setName(name)
191 return format
191 return format
192
192
193 def _copy_image(self, name):
193 def _copy_image(self, name):
194 """ Copies the ImageResource with 'name' to the clipboard.
194 """ Copies the ImageResource with 'name' to the clipboard.
195 """
195 """
196 image = self._get_image(name)
196 image = self._get_image(name)
197 QtGui.QApplication.clipboard().setImage(image)
197 QtGui.QApplication.clipboard().setImage(image)
198
198
199 def _get_image(self, name):
199 def _get_image(self, name):
200 """ Returns the QImage stored as the ImageResource with 'name'.
200 """ Returns the QImage stored as the ImageResource with 'name'.
201 """
201 """
202 document = self._control.document()
202 document = self._control.document()
203 image = document.resource(QtGui.QTextDocument.ImageResource,
203 image = document.resource(QtGui.QTextDocument.ImageResource,
204 QtCore.QUrl(name))
204 QtCore.QUrl(name))
205 return image
205 return image
206
206
207 def _get_image_tag(self, match, path = None, format = "png"):
207 def _get_image_tag(self, match, path = None, format = "png"):
208 """ Return (X)HTML mark-up for the image-tag given by match.
208 """ Return (X)HTML mark-up for the image-tag given by match.
209
209
210 Parameters
210 Parameters
211 ----------
211 ----------
212 match : re.SRE_Match
212 match : re.SRE_Match
213 A match to an HTML image tag as exported by Qt, with
213 A match to an HTML image tag as exported by Qt, with
214 match.group("Name") containing the matched image ID.
214 match.group("Name") containing the matched image ID.
215
215
216 path : string|None, optional [default None]
216 path : string|None, optional [default None]
217 If not None, specifies a path to which supporting files may be
217 If not None, specifies a path to which supporting files may be
218 written (e.g., for linked images). If None, all images are to be
218 written (e.g., for linked images). If None, all images are to be
219 included inline.
219 included inline.
220
220
221 format : "png"|"svg"|"jpg", optional [default "png"]
221 format : "png"|"svg"|"jpg", optional [default "png"]
222 Format for returned or referenced images.
222 Format for returned or referenced images.
223 """
223 """
224 if format in ("png","jpg"):
224 if format in ("png","jpg"):
225 try:
225 try:
226 image = self._get_image(match.group("name"))
226 image = self._get_image(match.group("name"))
227 except KeyError:
227 except KeyError:
228 return "<b>Couldn't find image %s</b>" % match.group("name")
228 return "<b>Couldn't find image %s</b>" % match.group("name")
229
229
230 if path is not None:
230 if path is not None:
231 if not os.path.exists(path):
231 if not os.path.exists(path):
232 os.mkdir(path)
232 os.mkdir(path)
233 relpath = os.path.basename(path)
233 relpath = os.path.basename(path)
234 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
234 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
235 "PNG"):
235 "PNG"):
236 return '<img src="%s/qt_img%s.%s">' % (relpath,
236 return '<img src="%s/qt_img%s.%s">' % (relpath,
237 match.group("name"),format)
237 match.group("name"),format)
238 else:
238 else:
239 return "<b>Couldn't save image!</b>"
239 return "<b>Couldn't save image!</b>"
240 else:
240 else:
241 ba = QtCore.QByteArray()
241 ba = QtCore.QByteArray()
242 buffer_ = QtCore.QBuffer(ba)
242 buffer_ = QtCore.QBuffer(ba)
243 buffer_.open(QtCore.QIODevice.WriteOnly)
243 buffer_.open(QtCore.QIODevice.WriteOnly)
244 image.save(buffer_, format.upper())
244 image.save(buffer_, format.upper())
245 buffer_.close()
245 buffer_.close()
246 return '<img src="data:image/%s;base64,\n%s\n" />' % (
246 return '<img src="data:image/%s;base64,\n%s\n" />' % (
247 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
247 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
248
248
249 elif format == "svg":
249 elif format == "svg":
250 try:
250 try:
251 svg = str(self._name_to_svg_map[match.group("name")])
251 svg = str(self._name_to_svg_map[match.group("name")])
252 except KeyError:
252 except KeyError:
253 if not self._svg_warning_displayed:
253 if not self._svg_warning_displayed:
254 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
254 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
255 'Cannot convert a PNG to SVG. To fix this, add this '
255 'Cannot convert a PNG to SVG. To fix this, add this '
256 'to your ipython config:\n\n'
256 'to your ipython config:\n\n'
257 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
257 '\tc.InlineBackendConfig.figure_format = \'svg\'\n\n'
258 'And regenerate the figures.',
258 'And regenerate the figures.',
259 QtGui.QMessageBox.Ok)
259 QtGui.QMessageBox.Ok)
260 self._svg_warning_displayed = True
260 self._svg_warning_displayed = True
261 return ("<b>Cannot convert a PNG to SVG.</b> "
261 return ("<b>Cannot convert a PNG to SVG.</b> "
262 "To fix this, add this to your config: "
262 "To fix this, add this to your config: "
263 "<span>c.InlineBackendConfig.figure_format = 'svg'</span> "
263 "<span>c.InlineBackendConfig.figure_format = 'svg'</span> "
264 "and regenerate the figures.")
264 "and regenerate the figures.")
265
265
266 # Not currently checking path, because it's tricky to find a
266 # Not currently checking path, because it's tricky to find a
267 # cross-browser way to embed external SVG images (e.g., via
267 # cross-browser way to embed external SVG images (e.g., via
268 # object or embed tags).
268 # object or embed tags).
269
269
270 # Chop stand-alone header from matplotlib SVG
270 # Chop stand-alone header from matplotlib SVG
271 offset = svg.find("<svg")
271 offset = svg.find("<svg")
272 assert(offset > -1)
272 assert(offset > -1)
273
273
274 return svg[offset:]
274 return svg[offset:]
275
275
276 else:
276 else:
277 return '<b>Unrecognized image format</b>'
277 return '<b>Unrecognized image format</b>'
278
278
279 def _insert_jpg(self, cursor, jpg):
279 def _insert_jpg(self, cursor, jpg):
280 """ Insert raw PNG data into the widget."""
280 """ Insert raw PNG data into the widget."""
281 self._insert_img(cursor, jpg, 'jpg')
281 self._insert_img(cursor, jpg, 'jpg')
282
282
283 def _insert_png(self, cursor, png):
283 def _insert_png(self, cursor, png):
284 """ Insert raw PNG data into the widget.
284 """ Insert raw PNG data into the widget.
285 """
285 """
286 self._insert_img(cursor, png, 'png')
286 self._insert_img(cursor, png, 'png')
287
287
288 def _insert_img(self, cursor, img, fmt):
288 def _insert_img(self, cursor, img, fmt):
289 """ insert a raw image, jpg or png """
289 """ insert a raw image, jpg or png """
290 try:
290 try:
291 image = QtGui.QImage()
291 image = QtGui.QImage()
292 image.loadFromData(img, fmt.upper())
292 image.loadFromData(img, fmt.upper())
293 except ValueError:
293 except ValueError:
294 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
294 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
295 else:
295 else:
296 format = self._add_image(image)
296 format = self._add_image(image)
297 cursor.insertBlock()
297 cursor.insertBlock()
298 cursor.insertImage(format)
298 cursor.insertImage(format)
299 cursor.insertBlock()
299 cursor.insertBlock()
300
300
301 def _insert_svg(self, cursor, svg):
301 def _insert_svg(self, cursor, svg):
302 """ Insert raw SVG data into the widet.
302 """ Insert raw SVG data into the widet.
303 """
303 """
304 try:
304 try:
305 image = svg_to_image(svg)
305 image = svg_to_image(svg)
306 except ValueError:
306 except ValueError:
307 self._insert_plain_text(cursor, 'Received invalid SVG data.')
307 self._insert_plain_text(cursor, 'Received invalid SVG data.')
308 else:
308 else:
309 format = self._add_image(image)
309 format = self._add_image(image)
310 self._name_to_svg_map[format.name()] = svg
310 self._name_to_svg_map[format.name()] = svg
311 cursor.insertBlock()
311 cursor.insertBlock()
312 cursor.insertImage(format)
312 cursor.insertImage(format)
313 cursor.insertBlock()
313 cursor.insertBlock()
314
314
315 def _save_image(self, name, format='PNG'):
315 def _save_image(self, name, format='PNG'):
316 """ Shows a save dialog for the ImageResource with 'name'.
316 """ Shows a save dialog for the ImageResource with 'name'.
317 """
317 """
318 dialog = QtGui.QFileDialog(self._control, 'Save Image')
318 dialog = QtGui.QFileDialog(self._control, 'Save Image')
319 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
319 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
320 dialog.setDefaultSuffix(format.lower())
320 dialog.setDefaultSuffix(format.lower())
321 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
321 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
322 if dialog.exec_():
322 if dialog.exec_():
323 filename = dialog.selectedFiles()[0]
323 filename = dialog.selectedFiles()[0]
324 image = self._get_image(name)
324 image = self._get_image(name)
325 image.save(filename, format)
325 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now