##// END OF EJS Templates
Merge pull request #6123 from minrk/zmq-console-echo-other...
Thomas Kluyver -
r18432:5ef19765 merge
parent child Browse files
Show More
@@ -1,150 +1,158 b''
1 """ Defines a convenient mix-in class for implementing Qt frontends.
1 """ Defines a convenient mix-in class for implementing Qt frontends.
2 """
2 """
3
3
4 class BaseFrontendMixin(object):
4 class BaseFrontendMixin(object):
5 """ A mix-in class for implementing Qt frontends.
5 """ A mix-in class for implementing Qt frontends.
6
6
7 To handle messages of a particular type, frontends need only define an
7 To handle messages of a particular type, frontends need only define an
8 appropriate handler method. For example, to handle 'stream' messaged, define
8 appropriate handler method. For example, to handle 'stream' messaged, define
9 a '_handle_stream(msg)' method.
9 a '_handle_stream(msg)' method.
10 """
10 """
11
11
12 #---------------------------------------------------------------------------
12 #---------------------------------------------------------------------------
13 # 'BaseFrontendMixin' concrete interface
13 # 'BaseFrontendMixin' concrete interface
14 #---------------------------------------------------------------------------
14 #---------------------------------------------------------------------------
15 _kernel_client = None
15 _kernel_client = None
16 _kernel_manager = None
16 _kernel_manager = None
17
17
18 @property
18 @property
19 def kernel_client(self):
19 def kernel_client(self):
20 """Returns the current kernel client."""
20 """Returns the current kernel client."""
21 return self._kernel_client
21 return self._kernel_client
22
22
23 @kernel_client.setter
23 @kernel_client.setter
24 def kernel_client(self, kernel_client):
24 def kernel_client(self, kernel_client):
25 """Disconnect from the current kernel client (if any) and set a new
25 """Disconnect from the current kernel client (if any) and set a new
26 kernel client.
26 kernel client.
27 """
27 """
28 # Disconnect the old kernel client, if necessary.
28 # Disconnect the old kernel client, if necessary.
29 old_client = self._kernel_client
29 old_client = self._kernel_client
30 if old_client is not None:
30 if old_client is not None:
31 old_client.started_channels.disconnect(self._started_channels)
31 old_client.started_channels.disconnect(self._started_channels)
32 old_client.stopped_channels.disconnect(self._stopped_channels)
32 old_client.stopped_channels.disconnect(self._stopped_channels)
33
33
34 # Disconnect the old kernel client's channels.
34 # Disconnect the old kernel client's channels.
35 old_client.iopub_channel.message_received.disconnect(self._dispatch)
35 old_client.iopub_channel.message_received.disconnect(self._dispatch)
36 old_client.shell_channel.message_received.disconnect(self._dispatch)
36 old_client.shell_channel.message_received.disconnect(self._dispatch)
37 old_client.stdin_channel.message_received.disconnect(self._dispatch)
37 old_client.stdin_channel.message_received.disconnect(self._dispatch)
38 old_client.hb_channel.kernel_died.disconnect(
38 old_client.hb_channel.kernel_died.disconnect(
39 self._handle_kernel_died)
39 self._handle_kernel_died)
40
40
41 # Handle the case where the old kernel client is still listening.
41 # Handle the case where the old kernel client is still listening.
42 if old_client.channels_running:
42 if old_client.channels_running:
43 self._stopped_channels()
43 self._stopped_channels()
44
44
45 # Set the new kernel client.
45 # Set the new kernel client.
46 self._kernel_client = kernel_client
46 self._kernel_client = kernel_client
47 if kernel_client is None:
47 if kernel_client is None:
48 return
48 return
49
49
50 # Connect the new kernel client.
50 # Connect the new kernel client.
51 kernel_client.started_channels.connect(self._started_channels)
51 kernel_client.started_channels.connect(self._started_channels)
52 kernel_client.stopped_channels.connect(self._stopped_channels)
52 kernel_client.stopped_channels.connect(self._stopped_channels)
53
53
54 # Connect the new kernel client's channels.
54 # Connect the new kernel client's channels.
55 kernel_client.iopub_channel.message_received.connect(self._dispatch)
55 kernel_client.iopub_channel.message_received.connect(self._dispatch)
56 kernel_client.shell_channel.message_received.connect(self._dispatch)
56 kernel_client.shell_channel.message_received.connect(self._dispatch)
57 kernel_client.stdin_channel.message_received.connect(self._dispatch)
57 kernel_client.stdin_channel.message_received.connect(self._dispatch)
58 # hb_channel
58 # hb_channel
59 kernel_client.hb_channel.kernel_died.connect(self._handle_kernel_died)
59 kernel_client.hb_channel.kernel_died.connect(self._handle_kernel_died)
60
60
61 # Handle the case where the kernel client started channels before
61 # Handle the case where the kernel client started channels before
62 # we connected.
62 # we connected.
63 if kernel_client.channels_running:
63 if kernel_client.channels_running:
64 self._started_channels()
64 self._started_channels()
65
65
66 @property
66 @property
67 def kernel_manager(self):
67 def kernel_manager(self):
68 """The kernel manager, if any"""
68 """The kernel manager, if any"""
69 return self._kernel_manager
69 return self._kernel_manager
70
70
71 @kernel_manager.setter
71 @kernel_manager.setter
72 def kernel_manager(self, kernel_manager):
72 def kernel_manager(self, kernel_manager):
73 old_man = self._kernel_manager
73 old_man = self._kernel_manager
74 if old_man is not None:
74 if old_man is not None:
75 old_man.kernel_restarted.disconnect(self._handle_kernel_restarted)
75 old_man.kernel_restarted.disconnect(self._handle_kernel_restarted)
76
76
77 self._kernel_manager = kernel_manager
77 self._kernel_manager = kernel_manager
78 if kernel_manager is None:
78 if kernel_manager is None:
79 return
79 return
80
80
81 kernel_manager.kernel_restarted.connect(self._handle_kernel_restarted)
81 kernel_manager.kernel_restarted.connect(self._handle_kernel_restarted)
82
82
83 #---------------------------------------------------------------------------
83 #---------------------------------------------------------------------------
84 # 'BaseFrontendMixin' abstract interface
84 # 'BaseFrontendMixin' abstract interface
85 #---------------------------------------------------------------------------
85 #---------------------------------------------------------------------------
86
86
87 def _handle_kernel_died(self, since_last_heartbeat):
87 def _handle_kernel_died(self, since_last_heartbeat):
88 """ This is called when the ``kernel_died`` signal is emitted.
88 """ This is called when the ``kernel_died`` signal is emitted.
89
89
90 This method is called when the kernel heartbeat has not been
90 This method is called when the kernel heartbeat has not been
91 active for a certain amount of time.
91 active for a certain amount of time.
92 This is a strictly passive notification -
92 This is a strictly passive notification -
93 the kernel is likely being restarted by its KernelManager.
93 the kernel is likely being restarted by its KernelManager.
94
94
95 Parameters
95 Parameters
96 ----------
96 ----------
97 since_last_heartbeat : float
97 since_last_heartbeat : float
98 The time since the heartbeat was last received.
98 The time since the heartbeat was last received.
99 """
99 """
100
100
101 def _handle_kernel_restarted(self):
101 def _handle_kernel_restarted(self):
102 """ This is called when the ``kernel_restarted`` signal is emitted.
102 """ This is called when the ``kernel_restarted`` signal is emitted.
103
103
104 This method is called when the kernel has been restarted by the
104 This method is called when the kernel has been restarted by the
105 autorestart mechanism.
105 autorestart mechanism.
106
106
107 Parameters
107 Parameters
108 ----------
108 ----------
109 since_last_heartbeat : float
109 since_last_heartbeat : float
110 The time since the heartbeat was last received.
110 The time since the heartbeat was last received.
111 """
111 """
112 def _started_kernel(self):
112 def _started_kernel(self):
113 """Called when the KernelManager starts (or restarts) the kernel subprocess.
113 """Called when the KernelManager starts (or restarts) the kernel subprocess.
114 Channels may or may not be running at this point.
114 Channels may or may not be running at this point.
115 """
115 """
116
116
117 def _started_channels(self):
117 def _started_channels(self):
118 """ Called when the KernelManager channels have started listening or
118 """ Called when the KernelManager channels have started listening or
119 when the frontend is assigned an already listening KernelManager.
119 when the frontend is assigned an already listening KernelManager.
120 """
120 """
121
121
122 def _stopped_channels(self):
122 def _stopped_channels(self):
123 """ Called when the KernelManager channels have stopped listening or
123 """ Called when the KernelManager channels have stopped listening or
124 when a listening KernelManager is removed from the frontend.
124 when a listening KernelManager is removed from the frontend.
125 """
125 """
126
126
127 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
128 # 'BaseFrontendMixin' protected interface
128 # 'BaseFrontendMixin' protected interface
129 #---------------------------------------------------------------------------
129 #---------------------------------------------------------------------------
130
130
131 def _dispatch(self, msg):
131 def _dispatch(self, msg):
132 """ Calls the frontend handler associated with the message type of the
132 """ Calls the frontend handler associated with the message type of the
133 given message.
133 given message.
134 """
134 """
135 msg_type = msg['header']['msg_type']
135 msg_type = msg['header']['msg_type']
136 handler = getattr(self, '_handle_' + msg_type, None)
136 handler = getattr(self, '_handle_' + msg_type, None)
137 if handler:
137 if handler:
138 handler(msg)
138 handler(msg)
139
139
140 def _is_from_this_session(self, msg):
140 def from_here(self, msg):
141 """ Returns whether a reply from the kernel originated from a request
141 """Return whether a message is from this session"""
142 from this frontend.
142 session_id = self._kernel_client.session.session
143 """
143 return msg['parent_header'].get("session", session_id) == session_id
144 session = self._kernel_client.session.session
144
145 parent = msg['parent_header']
145 def include_output(self, msg):
146 if not parent:
146 """Return whether we should include a given output message"""
147 # if the message has no parent, assume it is meant for all frontends
147 if self._hidden:
148 return False
149 from_here = self.from_here(msg)
150 if msg['msg_type'] == 'execute_input':
151 # only echo inputs not from here
152 return self.include_other_output and not from_here
153
154 if self.include_other_output:
148 return True
155 return True
149 else:
156 else:
150 return parent.get('session') == session
157 return from_here
158
@@ -1,2155 +1,2163 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 import time
12 import time
13 from unicodedata import category
13 from unicodedata import category
14 import webbrowser
14 import webbrowser
15
15
16 # System library imports
16 # System library imports
17 from IPython.external.qt import QtCore, QtGui
17 from IPython.external.qt import QtCore, QtGui
18
18
19 # Local imports
19 # Local imports
20 from IPython.config.configurable import LoggingConfigurable
20 from IPython.config.configurable import LoggingConfigurable
21 from IPython.core.inputsplitter import ESC_SEQUENCES
21 from IPython.core.inputsplitter import ESC_SEQUENCES
22 from IPython.qt.rich_text import HtmlExporter
22 from IPython.qt.rich_text import HtmlExporter
23 from IPython.qt.util import MetaQObjectHasTraits, get_font
23 from IPython.qt.util import MetaQObjectHasTraits, get_font
24 from IPython.utils.text import columnize
24 from IPython.utils.text import columnize
25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
25 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
26 from .ansi_code_processor import QtAnsiCodeProcessor
26 from .ansi_code_processor import QtAnsiCodeProcessor
27 from .completion_widget import CompletionWidget
27 from .completion_widget import CompletionWidget
28 from .completion_html import CompletionHtml
28 from .completion_html import CompletionHtml
29 from .completion_plain import CompletionPlain
29 from .completion_plain import CompletionPlain
30 from .kill_ring import QtKillRing
30 from .kill_ring import QtKillRing
31
31
32
32
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 # Functions
34 # Functions
35 #-----------------------------------------------------------------------------
35 #-----------------------------------------------------------------------------
36
36
37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
37 ESCAPE_CHARS = ''.join(ESC_SEQUENCES)
38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
38 ESCAPE_RE = re.compile("^["+ESCAPE_CHARS+"]+")
39
39
40 def commonprefix(items):
40 def commonprefix(items):
41 """Get common prefix for completions
41 """Get common prefix for completions
42
42
43 Return the longest common prefix of a list of strings, but with special
43 Return the longest common prefix of a list of strings, but with special
44 treatment of escape characters that might precede commands in IPython,
44 treatment of escape characters that might precede commands in IPython,
45 such as %magic functions. Used in tab completion.
45 such as %magic functions. Used in tab completion.
46
46
47 For a more general function, see os.path.commonprefix
47 For a more general function, see os.path.commonprefix
48 """
48 """
49 # the last item will always have the least leading % symbol
49 # the last item will always have the least leading % symbol
50 # min / max are first/last in alphabetical order
50 # min / max are first/last in alphabetical order
51 first_match = ESCAPE_RE.match(min(items))
51 first_match = ESCAPE_RE.match(min(items))
52 last_match = ESCAPE_RE.match(max(items))
52 last_match = ESCAPE_RE.match(max(items))
53 # common suffix is (common prefix of reversed items) reversed
53 # common suffix is (common prefix of reversed items) reversed
54 if first_match and last_match:
54 if first_match and last_match:
55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
55 prefix = os.path.commonprefix((first_match.group(0)[::-1], last_match.group(0)[::-1]))[::-1]
56 else:
56 else:
57 prefix = ''
57 prefix = ''
58
58
59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
59 items = [s.lstrip(ESCAPE_CHARS) for s in items]
60 return prefix+os.path.commonprefix(items)
60 return prefix+os.path.commonprefix(items)
61
61
62 def is_letter_or_number(char):
62 def is_letter_or_number(char):
63 """ Returns whether the specified unicode character is a letter or a number.
63 """ Returns whether the specified unicode character is a letter or a number.
64 """
64 """
65 cat = category(char)
65 cat = category(char)
66 return cat.startswith('L') or cat.startswith('N')
66 return cat.startswith('L') or cat.startswith('N')
67
67
68 #-----------------------------------------------------------------------------
68 #-----------------------------------------------------------------------------
69 # Classes
69 # Classes
70 #-----------------------------------------------------------------------------
70 #-----------------------------------------------------------------------------
71
71
72 class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui.QWidget), {})):
72 class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, QtGui.QWidget), {})):
73 """ An abstract base class for console-type widgets. This class has
73 """ An abstract base class for console-type widgets. This class has
74 functionality for:
74 functionality for:
75
75
76 * Maintaining a prompt and editing region
76 * Maintaining a prompt and editing region
77 * Providing the traditional Unix-style console keyboard shortcuts
77 * Providing the traditional Unix-style console keyboard shortcuts
78 * Performing tab completion
78 * Performing tab completion
79 * Paging text
79 * Paging text
80 * Handling ANSI escape codes
80 * Handling ANSI escape codes
81
81
82 ConsoleWidget also provides a number of utility methods that will be
82 ConsoleWidget also provides a number of utility methods that will be
83 convenient to implementors of a console-style widget.
83 convenient to implementors of a console-style widget.
84 """
84 """
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 execute_on_complete_input = Bool(True, config=True,
97 execute_on_complete_input = Bool(True, config=True,
98 help="""Whether to automatically execute on syntactically complete input.
98 help="""Whether to automatically execute on syntactically complete input.
99
99
100 If False, Shift-Enter is required to submit each execution.
100 If False, Shift-Enter is required to submit each execution.
101 Disabling this is mainly useful for non-Python kernels,
101 Disabling this is mainly useful for non-Python kernels,
102 where the completion check would be wrong.
102 where the completion check would be wrong.
103 """
103 """
104 )
104 )
105 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
105 gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
106 default_value = 'ncurses',
106 default_value = 'ncurses',
107 help="""
107 help="""
108 The type of completer to use. Valid values are:
108 The type of completer to use. Valid values are:
109
109
110 'plain' : Show the available completion as a text list
110 'plain' : Show the available completion as a text list
111 Below the editing area.
111 Below the editing area.
112 'droplist': Show the completion in a drop down list navigable
112 'droplist': Show the completion in a drop down list navigable
113 by the arrow keys, and from which you can select
113 by the arrow keys, and from which you can select
114 completion by pressing Return.
114 completion by pressing Return.
115 'ncurses' : Show the completion as a text list which is navigable by
115 'ncurses' : Show the completion as a text list which is navigable by
116 `tab` and arrow keys.
116 `tab` and arrow keys.
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 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
120 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
121 help="""
121 help="""
122 The type of underlying text widget to use. Valid values are 'plain',
122 The type of underlying text widget to use. Valid values are 'plain',
123 which specifies a QPlainTextEdit, and 'rich', which specifies a
123 which specifies a QPlainTextEdit, and 'rich', which specifies a
124 QTextEdit.
124 QTextEdit.
125 """
125 """
126 )
126 )
127 # NOTE: this value can only be specified during initialization.
127 # NOTE: this value can only be specified during initialization.
128 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
128 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
129 default_value='inside', config=True,
129 default_value='inside', config=True,
130 help="""
130 help="""
131 The type of paging to use. Valid values are:
131 The type of paging to use. Valid values are:
132
132
133 'inside'
133 'inside'
134 The widget pages like a traditional terminal.
134 The widget pages like a traditional terminal.
135 'hsplit'
135 'hsplit'
136 When paging is requested, the widget is split horizontally. The top
136 When paging is requested, the widget is split horizontally. The top
137 pane contains the console, and the bottom pane contains the paged text.
137 pane contains the console, and the bottom pane contains the paged text.
138 'vsplit'
138 'vsplit'
139 Similar to 'hsplit', except that a vertical splitter is used.
139 Similar to 'hsplit', except that a vertical splitter is used.
140 'custom'
140 'custom'
141 No action is taken by the widget beyond emitting a
141 No action is taken by the widget beyond emitting a
142 'custom_page_requested(str)' signal.
142 'custom_page_requested(str)' signal.
143 'none'
143 'none'
144 The text is written directly to the console.
144 The text is written directly to the console.
145 """)
145 """)
146
146
147 font_family = Unicode(config=True,
147 font_family = Unicode(config=True,
148 help="""The font family to use for the console.
148 help="""The font family to use for the console.
149 On OSX this defaults to Monaco, on Windows the default is
149 On OSX this defaults to Monaco, on Windows the default is
150 Consolas with fallback of Courier, and on other platforms
150 Consolas with fallback of Courier, and on other platforms
151 the default is Monospace.
151 the default is Monospace.
152 """)
152 """)
153 def _font_family_default(self):
153 def _font_family_default(self):
154 if sys.platform == 'win32':
154 if sys.platform == 'win32':
155 # Consolas ships with Vista/Win7, fallback to Courier if needed
155 # Consolas ships with Vista/Win7, fallback to Courier if needed
156 return 'Consolas'
156 return 'Consolas'
157 elif sys.platform == 'darwin':
157 elif sys.platform == 'darwin':
158 # OSX always has Monaco, no need for a fallback
158 # OSX always has Monaco, no need for a fallback
159 return 'Monaco'
159 return 'Monaco'
160 else:
160 else:
161 # Monospace should always exist, no need for a fallback
161 # Monospace should always exist, no need for a fallback
162 return 'Monospace'
162 return 'Monospace'
163
163
164 font_size = Integer(config=True,
164 font_size = Integer(config=True,
165 help="""The font size. If unconfigured, Qt will be entrusted
165 help="""The font size. If unconfigured, Qt will be entrusted
166 with the size of the font.
166 with the size of the font.
167 """)
167 """)
168
168
169 width = Integer(81, config=True,
169 width = Integer(81, config=True,
170 help="""The width of the console at start time in number
170 help="""The width of the console at start time in number
171 of characters (will double with `hsplit` paging)
171 of characters (will double with `hsplit` paging)
172 """)
172 """)
173
173
174 height = Integer(25, config=True,
174 height = Integer(25, config=True,
175 help="""The height of the console at start time in number
175 help="""The height of the console at start time in number
176 of characters (will double with `vsplit` paging)
176 of characters (will double with `vsplit` paging)
177 """)
177 """)
178
178
179 # Whether to override ShortcutEvents for the keybindings defined by this
179 # Whether to override ShortcutEvents for the keybindings defined by this
180 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
180 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
181 # priority (when it has focus) over, e.g., window-level menu shortcuts.
181 # priority (when it has focus) over, e.g., window-level menu shortcuts.
182 override_shortcuts = Bool(False)
182 override_shortcuts = Bool(False)
183
183
184 # ------ Custom Qt Widgets -------------------------------------------------
184 # ------ Custom Qt Widgets -------------------------------------------------
185
185
186 # For other projects to easily override the Qt widgets used by the console
186 # For other projects to easily override the Qt widgets used by the console
187 # (e.g. Spyder)
187 # (e.g. Spyder)
188 custom_control = None
188 custom_control = None
189 custom_page_control = None
189 custom_page_control = None
190
190
191 #------ Signals ------------------------------------------------------------
191 #------ Signals ------------------------------------------------------------
192
192
193 # Signals that indicate ConsoleWidget state.
193 # Signals that indicate ConsoleWidget state.
194 copy_available = QtCore.Signal(bool)
194 copy_available = QtCore.Signal(bool)
195 redo_available = QtCore.Signal(bool)
195 redo_available = QtCore.Signal(bool)
196 undo_available = QtCore.Signal(bool)
196 undo_available = QtCore.Signal(bool)
197
197
198 # Signal emitted when paging is needed and the paging style has been
198 # Signal emitted when paging is needed and the paging style has been
199 # specified as 'custom'.
199 # specified as 'custom'.
200 custom_page_requested = QtCore.Signal(object)
200 custom_page_requested = QtCore.Signal(object)
201
201
202 # Signal emitted when the font is changed.
202 # Signal emitted when the font is changed.
203 font_changed = QtCore.Signal(QtGui.QFont)
203 font_changed = QtCore.Signal(QtGui.QFont)
204
204
205 #------ Protected class variables ------------------------------------------
205 #------ Protected class variables ------------------------------------------
206
206
207 # control handles
207 # control handles
208 _control = None
208 _control = None
209 _page_control = None
209 _page_control = None
210 _splitter = None
210 _splitter = None
211
211
212 # When the control key is down, these keys are mapped.
212 # When the control key is down, these keys are mapped.
213 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
213 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
214 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
214 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
215 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
215 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
216 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
216 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
217 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
217 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
218 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
218 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
219 if not sys.platform == 'darwin':
219 if not sys.platform == 'darwin':
220 # On OS X, Ctrl-E already does the right thing, whereas End moves the
220 # On OS X, Ctrl-E already does the right thing, whereas End moves the
221 # cursor to the bottom of the buffer.
221 # cursor to the bottom of the buffer.
222 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
222 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
223
223
224 # The shortcuts defined by this widget. We need to keep track of these to
224 # The shortcuts defined by this widget. We need to keep track of these to
225 # support 'override_shortcuts' above.
225 # support 'override_shortcuts' above.
226 _shortcuts = set(_ctrl_down_remap.keys()) | \
226 _shortcuts = set(_ctrl_down_remap.keys()) | \
227 { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
227 { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
228 QtCore.Qt.Key_V }
228 QtCore.Qt.Key_V }
229
229
230 _temp_buffer_filled = False
230 _temp_buffer_filled = False
231
231
232 #---------------------------------------------------------------------------
232 #---------------------------------------------------------------------------
233 # 'QObject' interface
233 # 'QObject' interface
234 #---------------------------------------------------------------------------
234 #---------------------------------------------------------------------------
235
235
236 def __init__(self, parent=None, **kw):
236 def __init__(self, parent=None, **kw):
237 """ Create a ConsoleWidget.
237 """ Create a ConsoleWidget.
238
238
239 Parameters
239 Parameters
240 ----------
240 ----------
241 parent : QWidget, optional [default None]
241 parent : QWidget, optional [default None]
242 The parent for this widget.
242 The parent for this widget.
243 """
243 """
244 QtGui.QWidget.__init__(self, parent)
244 QtGui.QWidget.__init__(self, parent)
245 LoggingConfigurable.__init__(self, **kw)
245 LoggingConfigurable.__init__(self, **kw)
246
246
247 # While scrolling the pager on Mac OS X, it tears badly. The
247 # While scrolling the pager on Mac OS X, it tears badly. The
248 # NativeGesture is platform and perhaps build-specific hence
248 # NativeGesture is platform and perhaps build-specific hence
249 # we take adequate precautions here.
249 # we take adequate precautions here.
250 self._pager_scroll_events = [QtCore.QEvent.Wheel]
250 self._pager_scroll_events = [QtCore.QEvent.Wheel]
251 if hasattr(QtCore.QEvent, 'NativeGesture'):
251 if hasattr(QtCore.QEvent, 'NativeGesture'):
252 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
252 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
253
253
254 # Create the layout and underlying text widget.
254 # Create the layout and underlying text widget.
255 layout = QtGui.QStackedLayout(self)
255 layout = QtGui.QStackedLayout(self)
256 layout.setContentsMargins(0, 0, 0, 0)
256 layout.setContentsMargins(0, 0, 0, 0)
257 self._control = self._create_control()
257 self._control = self._create_control()
258 if self.paging in ('hsplit', 'vsplit'):
258 if self.paging in ('hsplit', 'vsplit'):
259 self._splitter = QtGui.QSplitter()
259 self._splitter = QtGui.QSplitter()
260 if self.paging == 'hsplit':
260 if self.paging == 'hsplit':
261 self._splitter.setOrientation(QtCore.Qt.Horizontal)
261 self._splitter.setOrientation(QtCore.Qt.Horizontal)
262 else:
262 else:
263 self._splitter.setOrientation(QtCore.Qt.Vertical)
263 self._splitter.setOrientation(QtCore.Qt.Vertical)
264 self._splitter.addWidget(self._control)
264 self._splitter.addWidget(self._control)
265 layout.addWidget(self._splitter)
265 layout.addWidget(self._splitter)
266 else:
266 else:
267 layout.addWidget(self._control)
267 layout.addWidget(self._control)
268
268
269 # Create the paging widget, if necessary.
269 # Create the paging widget, if necessary.
270 if self.paging in ('inside', 'hsplit', 'vsplit'):
270 if self.paging in ('inside', 'hsplit', 'vsplit'):
271 self._page_control = self._create_page_control()
271 self._page_control = self._create_page_control()
272 if self._splitter:
272 if self._splitter:
273 self._page_control.hide()
273 self._page_control.hide()
274 self._splitter.addWidget(self._page_control)
274 self._splitter.addWidget(self._page_control)
275 else:
275 else:
276 layout.addWidget(self._page_control)
276 layout.addWidget(self._page_control)
277
277
278 # Initialize protected variables. Some variables contain useful state
278 # Initialize protected variables. Some variables contain useful state
279 # information for subclasses; they should be considered read-only.
279 # information for subclasses; they should be considered read-only.
280 self._append_before_prompt_pos = 0
280 self._append_before_prompt_pos = 0
281 self._ansi_processor = QtAnsiCodeProcessor()
281 self._ansi_processor = QtAnsiCodeProcessor()
282 if self.gui_completion == 'ncurses':
282 if self.gui_completion == 'ncurses':
283 self._completion_widget = CompletionHtml(self)
283 self._completion_widget = CompletionHtml(self)
284 elif self.gui_completion == 'droplist':
284 elif self.gui_completion == 'droplist':
285 self._completion_widget = CompletionWidget(self)
285 self._completion_widget = CompletionWidget(self)
286 elif self.gui_completion == 'plain':
286 elif self.gui_completion == 'plain':
287 self._completion_widget = CompletionPlain(self)
287 self._completion_widget = CompletionPlain(self)
288
288
289 self._continuation_prompt = '> '
289 self._continuation_prompt = '> '
290 self._continuation_prompt_html = None
290 self._continuation_prompt_html = None
291 self._executing = False
291 self._executing = False
292 self._filter_resize = False
292 self._filter_resize = False
293 self._html_exporter = HtmlExporter(self._control)
293 self._html_exporter = HtmlExporter(self._control)
294 self._input_buffer_executing = ''
294 self._input_buffer_executing = ''
295 self._input_buffer_pending = ''
295 self._input_buffer_pending = ''
296 self._kill_ring = QtKillRing(self._control)
296 self._kill_ring = QtKillRing(self._control)
297 self._prompt = ''
297 self._prompt = ''
298 self._prompt_html = None
298 self._prompt_html = None
299 self._prompt_pos = 0
299 self._prompt_pos = 0
300 self._prompt_sep = ''
300 self._prompt_sep = ''
301 self._reading = False
301 self._reading = False
302 self._reading_callback = None
302 self._reading_callback = None
303 self._tab_width = 8
303 self._tab_width = 8
304
304
305 # List of strings pending to be appended as plain text in the widget.
305 # List of strings pending to be appended as plain text in the widget.
306 # The text is not immediately inserted when available to not
306 # The text is not immediately inserted when available to not
307 # choke the Qt event loop with paint events for the widget in
307 # choke the Qt event loop with paint events for the widget in
308 # case of lots of output from kernel.
308 # case of lots of output from kernel.
309 self._pending_insert_text = []
309 self._pending_insert_text = []
310
310
311 # Timer to flush the pending stream messages. The interval is adjusted
311 # Timer to flush the pending stream messages. The interval is adjusted
312 # later based on actual time taken for flushing a screen (buffer_size)
312 # later based on actual time taken for flushing a screen (buffer_size)
313 # of output text.
313 # of output text.
314 self._pending_text_flush_interval = QtCore.QTimer(self._control)
314 self._pending_text_flush_interval = QtCore.QTimer(self._control)
315 self._pending_text_flush_interval.setInterval(100)
315 self._pending_text_flush_interval.setInterval(100)
316 self._pending_text_flush_interval.setSingleShot(True)
316 self._pending_text_flush_interval.setSingleShot(True)
317 self._pending_text_flush_interval.timeout.connect(
317 self._pending_text_flush_interval.timeout.connect(
318 self._on_flush_pending_stream_timer)
318 self._on_flush_pending_stream_timer)
319
319
320 # Set a monospaced font.
320 # Set a monospaced font.
321 self.reset_font()
321 self.reset_font()
322
322
323 # Configure actions.
323 # Configure actions.
324 action = QtGui.QAction('Print', None)
324 action = QtGui.QAction('Print', None)
325 action.setEnabled(True)
325 action.setEnabled(True)
326 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
326 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
327 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
327 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
328 # Only override the default if there is a collision.
328 # Only override the default if there is a collision.
329 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
329 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
330 printkey = "Ctrl+Shift+P"
330 printkey = "Ctrl+Shift+P"
331 action.setShortcut(printkey)
331 action.setShortcut(printkey)
332 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
332 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
333 action.triggered.connect(self.print_)
333 action.triggered.connect(self.print_)
334 self.addAction(action)
334 self.addAction(action)
335 self.print_action = action
335 self.print_action = action
336
336
337 action = QtGui.QAction('Save as HTML/XML', None)
337 action = QtGui.QAction('Save as HTML/XML', None)
338 action.setShortcut(QtGui.QKeySequence.Save)
338 action.setShortcut(QtGui.QKeySequence.Save)
339 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
339 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
340 action.triggered.connect(self.export_html)
340 action.triggered.connect(self.export_html)
341 self.addAction(action)
341 self.addAction(action)
342 self.export_action = action
342 self.export_action = action
343
343
344 action = QtGui.QAction('Select All', None)
344 action = QtGui.QAction('Select All', None)
345 action.setEnabled(True)
345 action.setEnabled(True)
346 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
346 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
347 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
347 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
348 # Only override the default if there is a collision.
348 # Only override the default if there is a collision.
349 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
349 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
350 selectall = "Ctrl+Shift+A"
350 selectall = "Ctrl+Shift+A"
351 action.setShortcut(selectall)
351 action.setShortcut(selectall)
352 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
352 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
353 action.triggered.connect(self.select_all)
353 action.triggered.connect(self.select_all)
354 self.addAction(action)
354 self.addAction(action)
355 self.select_all_action = action
355 self.select_all_action = action
356
356
357 self.increase_font_size = QtGui.QAction("Bigger Font",
357 self.increase_font_size = QtGui.QAction("Bigger Font",
358 self,
358 self,
359 shortcut=QtGui.QKeySequence.ZoomIn,
359 shortcut=QtGui.QKeySequence.ZoomIn,
360 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
360 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
361 statusTip="Increase the font size by one point",
361 statusTip="Increase the font size by one point",
362 triggered=self._increase_font_size)
362 triggered=self._increase_font_size)
363 self.addAction(self.increase_font_size)
363 self.addAction(self.increase_font_size)
364
364
365 self.decrease_font_size = QtGui.QAction("Smaller Font",
365 self.decrease_font_size = QtGui.QAction("Smaller Font",
366 self,
366 self,
367 shortcut=QtGui.QKeySequence.ZoomOut,
367 shortcut=QtGui.QKeySequence.ZoomOut,
368 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
368 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
369 statusTip="Decrease the font size by one point",
369 statusTip="Decrease the font size by one point",
370 triggered=self._decrease_font_size)
370 triggered=self._decrease_font_size)
371 self.addAction(self.decrease_font_size)
371 self.addAction(self.decrease_font_size)
372
372
373 self.reset_font_size = QtGui.QAction("Normal Font",
373 self.reset_font_size = QtGui.QAction("Normal Font",
374 self,
374 self,
375 shortcut="Ctrl+0",
375 shortcut="Ctrl+0",
376 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
376 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
377 statusTip="Restore the Normal font size",
377 statusTip="Restore the Normal font size",
378 triggered=self.reset_font)
378 triggered=self.reset_font)
379 self.addAction(self.reset_font_size)
379 self.addAction(self.reset_font_size)
380
380
381 # Accept drag and drop events here. Drops were already turned off
381 # Accept drag and drop events here. Drops were already turned off
382 # in self._control when that widget was created.
382 # in self._control when that widget was created.
383 self.setAcceptDrops(True)
383 self.setAcceptDrops(True)
384
384
385 #---------------------------------------------------------------------------
385 #---------------------------------------------------------------------------
386 # Drag and drop support
386 # Drag and drop support
387 #---------------------------------------------------------------------------
387 #---------------------------------------------------------------------------
388
388
389 def dragEnterEvent(self, e):
389 def dragEnterEvent(self, e):
390 if e.mimeData().hasUrls():
390 if e.mimeData().hasUrls():
391 # The link action should indicate to that the drop will insert
391 # The link action should indicate to that the drop will insert
392 # the file anme.
392 # the file anme.
393 e.setDropAction(QtCore.Qt.LinkAction)
393 e.setDropAction(QtCore.Qt.LinkAction)
394 e.accept()
394 e.accept()
395 elif e.mimeData().hasText():
395 elif e.mimeData().hasText():
396 # By changing the action to copy we don't need to worry about
396 # By changing the action to copy we don't need to worry about
397 # the user accidentally moving text around in the widget.
397 # the user accidentally moving text around in the widget.
398 e.setDropAction(QtCore.Qt.CopyAction)
398 e.setDropAction(QtCore.Qt.CopyAction)
399 e.accept()
399 e.accept()
400
400
401 def dragMoveEvent(self, e):
401 def dragMoveEvent(self, e):
402 if e.mimeData().hasUrls():
402 if e.mimeData().hasUrls():
403 pass
403 pass
404 elif e.mimeData().hasText():
404 elif e.mimeData().hasText():
405 cursor = self._control.cursorForPosition(e.pos())
405 cursor = self._control.cursorForPosition(e.pos())
406 if self._in_buffer(cursor.position()):
406 if self._in_buffer(cursor.position()):
407 e.setDropAction(QtCore.Qt.CopyAction)
407 e.setDropAction(QtCore.Qt.CopyAction)
408 self._control.setTextCursor(cursor)
408 self._control.setTextCursor(cursor)
409 else:
409 else:
410 e.setDropAction(QtCore.Qt.IgnoreAction)
410 e.setDropAction(QtCore.Qt.IgnoreAction)
411 e.accept()
411 e.accept()
412
412
413 def dropEvent(self, e):
413 def dropEvent(self, e):
414 if e.mimeData().hasUrls():
414 if e.mimeData().hasUrls():
415 self._keep_cursor_in_buffer()
415 self._keep_cursor_in_buffer()
416 cursor = self._control.textCursor()
416 cursor = self._control.textCursor()
417 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
417 filenames = [url.toLocalFile() for url in e.mimeData().urls()]
418 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
418 text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
419 for f in filenames)
419 for f in filenames)
420 self._insert_plain_text_into_buffer(cursor, text)
420 self._insert_plain_text_into_buffer(cursor, text)
421 elif e.mimeData().hasText():
421 elif e.mimeData().hasText():
422 cursor = self._control.cursorForPosition(e.pos())
422 cursor = self._control.cursorForPosition(e.pos())
423 if self._in_buffer(cursor.position()):
423 if self._in_buffer(cursor.position()):
424 text = e.mimeData().text()
424 text = e.mimeData().text()
425 self._insert_plain_text_into_buffer(cursor, text)
425 self._insert_plain_text_into_buffer(cursor, text)
426
426
427 def eventFilter(self, obj, event):
427 def eventFilter(self, obj, event):
428 """ Reimplemented to ensure a console-like behavior in the underlying
428 """ Reimplemented to ensure a console-like behavior in the underlying
429 text widgets.
429 text widgets.
430 """
430 """
431 etype = event.type()
431 etype = event.type()
432 if etype == QtCore.QEvent.KeyPress:
432 if etype == QtCore.QEvent.KeyPress:
433
433
434 # Re-map keys for all filtered widgets.
434 # Re-map keys for all filtered widgets.
435 key = event.key()
435 key = event.key()
436 if self._control_key_down(event.modifiers()) and \
436 if self._control_key_down(event.modifiers()) and \
437 key in self._ctrl_down_remap:
437 key in self._ctrl_down_remap:
438 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
438 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
439 self._ctrl_down_remap[key],
439 self._ctrl_down_remap[key],
440 QtCore.Qt.NoModifier)
440 QtCore.Qt.NoModifier)
441 QtGui.qApp.sendEvent(obj, new_event)
441 QtGui.qApp.sendEvent(obj, new_event)
442 return True
442 return True
443
443
444 elif obj == self._control:
444 elif obj == self._control:
445 return self._event_filter_console_keypress(event)
445 return self._event_filter_console_keypress(event)
446
446
447 elif obj == self._page_control:
447 elif obj == self._page_control:
448 return self._event_filter_page_keypress(event)
448 return self._event_filter_page_keypress(event)
449
449
450 # Make middle-click paste safe.
450 # Make middle-click paste safe.
451 elif etype == QtCore.QEvent.MouseButtonRelease and \
451 elif etype == QtCore.QEvent.MouseButtonRelease and \
452 event.button() == QtCore.Qt.MidButton and \
452 event.button() == QtCore.Qt.MidButton and \
453 obj == self._control.viewport():
453 obj == self._control.viewport():
454 cursor = self._control.cursorForPosition(event.pos())
454 cursor = self._control.cursorForPosition(event.pos())
455 self._control.setTextCursor(cursor)
455 self._control.setTextCursor(cursor)
456 self.paste(QtGui.QClipboard.Selection)
456 self.paste(QtGui.QClipboard.Selection)
457 return True
457 return True
458
458
459 # Manually adjust the scrollbars *after* a resize event is dispatched.
459 # Manually adjust the scrollbars *after* a resize event is dispatched.
460 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
460 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
461 self._filter_resize = True
461 self._filter_resize = True
462 QtGui.qApp.sendEvent(obj, event)
462 QtGui.qApp.sendEvent(obj, event)
463 self._adjust_scrollbars()
463 self._adjust_scrollbars()
464 self._filter_resize = False
464 self._filter_resize = False
465 return True
465 return True
466
466
467 # Override shortcuts for all filtered widgets.
467 # Override shortcuts for all filtered widgets.
468 elif etype == QtCore.QEvent.ShortcutOverride and \
468 elif etype == QtCore.QEvent.ShortcutOverride and \
469 self.override_shortcuts and \
469 self.override_shortcuts and \
470 self._control_key_down(event.modifiers()) and \
470 self._control_key_down(event.modifiers()) and \
471 event.key() in self._shortcuts:
471 event.key() in self._shortcuts:
472 event.accept()
472 event.accept()
473
473
474 # Handle scrolling of the vsplit pager. This hack attempts to solve
474 # Handle scrolling of the vsplit pager. This hack attempts to solve
475 # problems with tearing of the help text inside the pager window. This
475 # problems with tearing of the help text inside the pager window. This
476 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
476 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
477 # perfect but makes the pager more usable.
477 # perfect but makes the pager more usable.
478 elif etype in self._pager_scroll_events and \
478 elif etype in self._pager_scroll_events and \
479 obj == self._page_control:
479 obj == self._page_control:
480 self._page_control.repaint()
480 self._page_control.repaint()
481 return True
481 return True
482
482
483 elif etype == QtCore.QEvent.MouseMove:
483 elif etype == QtCore.QEvent.MouseMove:
484 anchor = self._control.anchorAt(event.pos())
484 anchor = self._control.anchorAt(event.pos())
485 QtGui.QToolTip.showText(event.globalPos(), anchor)
485 QtGui.QToolTip.showText(event.globalPos(), anchor)
486
486
487 return super(ConsoleWidget, self).eventFilter(obj, event)
487 return super(ConsoleWidget, self).eventFilter(obj, event)
488
488
489 #---------------------------------------------------------------------------
489 #---------------------------------------------------------------------------
490 # 'QWidget' interface
490 # 'QWidget' interface
491 #---------------------------------------------------------------------------
491 #---------------------------------------------------------------------------
492
492
493 def sizeHint(self):
493 def sizeHint(self):
494 """ Reimplemented to suggest a size that is 80 characters wide and
494 """ Reimplemented to suggest a size that is 80 characters wide and
495 25 lines high.
495 25 lines high.
496 """
496 """
497 font_metrics = QtGui.QFontMetrics(self.font)
497 font_metrics = QtGui.QFontMetrics(self.font)
498 margin = (self._control.frameWidth() +
498 margin = (self._control.frameWidth() +
499 self._control.document().documentMargin()) * 2
499 self._control.document().documentMargin()) * 2
500 style = self.style()
500 style = self.style()
501 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
501 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
502
502
503 # Note 1: Despite my best efforts to take the various margins into
503 # Note 1: Despite my best efforts to take the various margins into
504 # account, the width is still coming out a bit too small, so we include
504 # account, the width is still coming out a bit too small, so we include
505 # a fudge factor of one character here.
505 # a fudge factor of one character here.
506 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
506 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
507 # to a Qt bug on certain Mac OS systems where it returns 0.
507 # to a Qt bug on certain Mac OS systems where it returns 0.
508 width = font_metrics.width(' ') * self.width + margin
508 width = font_metrics.width(' ') * self.width + margin
509 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
509 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
510 if self.paging == 'hsplit':
510 if self.paging == 'hsplit':
511 width = width * 2 + splitwidth
511 width = width * 2 + splitwidth
512
512
513 height = font_metrics.height() * self.height + margin
513 height = font_metrics.height() * self.height + margin
514 if self.paging == 'vsplit':
514 if self.paging == 'vsplit':
515 height = height * 2 + splitwidth
515 height = height * 2 + splitwidth
516
516
517 return QtCore.QSize(width, height)
517 return QtCore.QSize(width, height)
518
518
519 #---------------------------------------------------------------------------
519 #---------------------------------------------------------------------------
520 # 'ConsoleWidget' public interface
520 # 'ConsoleWidget' public interface
521 #---------------------------------------------------------------------------
521 #---------------------------------------------------------------------------
522
522
523 include_other_output = Bool(False, config=True,
524 help="""Whether to include output from clients
525 other than this one sharing the same kernel.
526
527 Outputs are not displayed until enter is pressed.
528 """
529 )
530
523 def can_copy(self):
531 def can_copy(self):
524 """ Returns whether text can be copied to the clipboard.
532 """ Returns whether text can be copied to the clipboard.
525 """
533 """
526 return self._control.textCursor().hasSelection()
534 return self._control.textCursor().hasSelection()
527
535
528 def can_cut(self):
536 def can_cut(self):
529 """ Returns whether text can be cut to the clipboard.
537 """ Returns whether text can be cut to the clipboard.
530 """
538 """
531 cursor = self._control.textCursor()
539 cursor = self._control.textCursor()
532 return (cursor.hasSelection() and
540 return (cursor.hasSelection() and
533 self._in_buffer(cursor.anchor()) and
541 self._in_buffer(cursor.anchor()) and
534 self._in_buffer(cursor.position()))
542 self._in_buffer(cursor.position()))
535
543
536 def can_paste(self):
544 def can_paste(self):
537 """ Returns whether text can be pasted from the clipboard.
545 """ Returns whether text can be pasted from the clipboard.
538 """
546 """
539 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
547 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
540 return bool(QtGui.QApplication.clipboard().text())
548 return bool(QtGui.QApplication.clipboard().text())
541 return False
549 return False
542
550
543 def clear(self, keep_input=True):
551 def clear(self, keep_input=True):
544 """ Clear the console.
552 """ Clear the console.
545
553
546 Parameters
554 Parameters
547 ----------
555 ----------
548 keep_input : bool, optional (default True)
556 keep_input : bool, optional (default True)
549 If set, restores the old input buffer if a new prompt is written.
557 If set, restores the old input buffer if a new prompt is written.
550 """
558 """
551 if self._executing:
559 if self._executing:
552 self._control.clear()
560 self._control.clear()
553 else:
561 else:
554 if keep_input:
562 if keep_input:
555 input_buffer = self.input_buffer
563 input_buffer = self.input_buffer
556 self._control.clear()
564 self._control.clear()
557 self._show_prompt()
565 self._show_prompt()
558 if keep_input:
566 if keep_input:
559 self.input_buffer = input_buffer
567 self.input_buffer = input_buffer
560
568
561 def copy(self):
569 def copy(self):
562 """ Copy the currently selected text to the clipboard.
570 """ Copy the currently selected text to the clipboard.
563 """
571 """
564 self.layout().currentWidget().copy()
572 self.layout().currentWidget().copy()
565
573
566 def copy_anchor(self, anchor):
574 def copy_anchor(self, anchor):
567 """ Copy anchor text to the clipboard
575 """ Copy anchor text to the clipboard
568 """
576 """
569 QtGui.QApplication.clipboard().setText(anchor)
577 QtGui.QApplication.clipboard().setText(anchor)
570
578
571 def cut(self):
579 def cut(self):
572 """ Copy the currently selected text to the clipboard and delete it
580 """ Copy the currently selected text to the clipboard and delete it
573 if it's inside the input buffer.
581 if it's inside the input buffer.
574 """
582 """
575 self.copy()
583 self.copy()
576 if self.can_cut():
584 if self.can_cut():
577 self._control.textCursor().removeSelectedText()
585 self._control.textCursor().removeSelectedText()
578
586
579 def execute(self, source=None, hidden=False, interactive=False):
587 def execute(self, source=None, hidden=False, interactive=False):
580 """ Executes source or the input buffer, possibly prompting for more
588 """ Executes source or the input buffer, possibly prompting for more
581 input.
589 input.
582
590
583 Parameters
591 Parameters
584 ----------
592 ----------
585 source : str, optional
593 source : str, optional
586
594
587 The source to execute. If not specified, the input buffer will be
595 The source to execute. If not specified, the input buffer will be
588 used. If specified and 'hidden' is False, the input buffer will be
596 used. If specified and 'hidden' is False, the input buffer will be
589 replaced with the source before execution.
597 replaced with the source before execution.
590
598
591 hidden : bool, optional (default False)
599 hidden : bool, optional (default False)
592
600
593 If set, no output will be shown and the prompt will not be modified.
601 If set, no output will be shown and the prompt will not be modified.
594 In other words, it will be completely invisible to the user that
602 In other words, it will be completely invisible to the user that
595 an execution has occurred.
603 an execution has occurred.
596
604
597 interactive : bool, optional (default False)
605 interactive : bool, optional (default False)
598
606
599 Whether the console is to treat the source as having been manually
607 Whether the console is to treat the source as having been manually
600 entered by the user. The effect of this parameter depends on the
608 entered by the user. The effect of this parameter depends on the
601 subclass implementation.
609 subclass implementation.
602
610
603 Raises
611 Raises
604 ------
612 ------
605 RuntimeError
613 RuntimeError
606 If incomplete input is given and 'hidden' is True. In this case,
614 If incomplete input is given and 'hidden' is True. In this case,
607 it is not possible to prompt for more input.
615 it is not possible to prompt for more input.
608
616
609 Returns
617 Returns
610 -------
618 -------
611 A boolean indicating whether the source was executed.
619 A boolean indicating whether the source was executed.
612 """
620 """
613 # WARNING: The order in which things happen here is very particular, in
621 # WARNING: The order in which things happen here is very particular, in
614 # large part because our syntax highlighting is fragile. If you change
622 # large part because our syntax highlighting is fragile. If you change
615 # something, test carefully!
623 # something, test carefully!
616
624
617 # Decide what to execute.
625 # Decide what to execute.
618 if source is None:
626 if source is None:
619 source = self.input_buffer
627 source = self.input_buffer
620 if not hidden:
628 if not hidden:
621 # A newline is appended later, but it should be considered part
629 # A newline is appended later, but it should be considered part
622 # of the input buffer.
630 # of the input buffer.
623 source += '\n'
631 source += '\n'
624 elif not hidden:
632 elif not hidden:
625 self.input_buffer = source
633 self.input_buffer = source
626
634
627 # Execute the source or show a continuation prompt if it is incomplete.
635 # Execute the source or show a continuation prompt if it is incomplete.
628 if self.execute_on_complete_input:
636 if self.execute_on_complete_input:
629 complete = self._is_complete(source, interactive)
637 complete = self._is_complete(source, interactive)
630 else:
638 else:
631 complete = not interactive
639 complete = not interactive
632 if hidden:
640 if hidden:
633 if complete or not self.execute_on_complete_input:
641 if complete or not self.execute_on_complete_input:
634 self._execute(source, hidden)
642 self._execute(source, hidden)
635 else:
643 else:
636 error = 'Incomplete noninteractive input: "%s"'
644 error = 'Incomplete noninteractive input: "%s"'
637 raise RuntimeError(error % source)
645 raise RuntimeError(error % source)
638 else:
646 else:
639 if complete:
647 if complete:
640 self._append_plain_text('\n')
648 self._append_plain_text('\n')
641 self._input_buffer_executing = self.input_buffer
649 self._input_buffer_executing = self.input_buffer
642 self._executing = True
650 self._executing = True
643 self._prompt_finished()
651 self._prompt_finished()
644
652
645 # The maximum block count is only in effect during execution.
653 # The maximum block count is only in effect during execution.
646 # This ensures that _prompt_pos does not become invalid due to
654 # This ensures that _prompt_pos does not become invalid due to
647 # text truncation.
655 # text truncation.
648 self._control.document().setMaximumBlockCount(self.buffer_size)
656 self._control.document().setMaximumBlockCount(self.buffer_size)
649
657
650 # Setting a positive maximum block count will automatically
658 # Setting a positive maximum block count will automatically
651 # disable the undo/redo history, but just to be safe:
659 # disable the undo/redo history, but just to be safe:
652 self._control.setUndoRedoEnabled(False)
660 self._control.setUndoRedoEnabled(False)
653
661
654 # Perform actual execution.
662 # Perform actual execution.
655 self._execute(source, hidden)
663 self._execute(source, hidden)
656
664
657 else:
665 else:
658 # Do this inside an edit block so continuation prompts are
666 # Do this inside an edit block so continuation prompts are
659 # removed seamlessly via undo/redo.
667 # removed seamlessly via undo/redo.
660 cursor = self._get_end_cursor()
668 cursor = self._get_end_cursor()
661 cursor.beginEditBlock()
669 cursor.beginEditBlock()
662 cursor.insertText('\n')
670 cursor.insertText('\n')
663 self._insert_continuation_prompt(cursor)
671 self._insert_continuation_prompt(cursor)
664 cursor.endEditBlock()
672 cursor.endEditBlock()
665
673
666 # Do not do this inside the edit block. It works as expected
674 # Do not do this inside the edit block. It works as expected
667 # when using a QPlainTextEdit control, but does not have an
675 # when using a QPlainTextEdit control, but does not have an
668 # effect when using a QTextEdit. I believe this is a Qt bug.
676 # effect when using a QTextEdit. I believe this is a Qt bug.
669 self._control.moveCursor(QtGui.QTextCursor.End)
677 self._control.moveCursor(QtGui.QTextCursor.End)
670
678
671 return complete
679 return complete
672
680
673 def export_html(self):
681 def export_html(self):
674 """ Shows a dialog to export HTML/XML in various formats.
682 """ Shows a dialog to export HTML/XML in various formats.
675 """
683 """
676 self._html_exporter.export()
684 self._html_exporter.export()
677
685
678 def _get_input_buffer(self, force=False):
686 def _get_input_buffer(self, force=False):
679 """ The text that the user has entered entered at the current prompt.
687 """ The text that the user has entered entered at the current prompt.
680
688
681 If the console is currently executing, the text that is executing will
689 If the console is currently executing, the text that is executing will
682 always be returned.
690 always be returned.
683 """
691 """
684 # If we're executing, the input buffer may not even exist anymore due to
692 # If we're executing, the input buffer may not even exist anymore due to
685 # the limit imposed by 'buffer_size'. Therefore, we store it.
693 # the limit imposed by 'buffer_size'. Therefore, we store it.
686 if self._executing and not force:
694 if self._executing and not force:
687 return self._input_buffer_executing
695 return self._input_buffer_executing
688
696
689 cursor = self._get_end_cursor()
697 cursor = self._get_end_cursor()
690 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
698 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
691 input_buffer = cursor.selection().toPlainText()
699 input_buffer = cursor.selection().toPlainText()
692
700
693 # Strip out continuation prompts.
701 # Strip out continuation prompts.
694 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
702 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
695
703
696 def _set_input_buffer(self, string):
704 def _set_input_buffer(self, string):
697 """ Sets the text in the input buffer.
705 """ Sets the text in the input buffer.
698
706
699 If the console is currently executing, this call has no *immediate*
707 If the console is currently executing, this call has no *immediate*
700 effect. When the execution is finished, the input buffer will be updated
708 effect. When the execution is finished, the input buffer will be updated
701 appropriately.
709 appropriately.
702 """
710 """
703 # If we're executing, store the text for later.
711 # If we're executing, store the text for later.
704 if self._executing:
712 if self._executing:
705 self._input_buffer_pending = string
713 self._input_buffer_pending = string
706 return
714 return
707
715
708 # Remove old text.
716 # Remove old text.
709 cursor = self._get_end_cursor()
717 cursor = self._get_end_cursor()
710 cursor.beginEditBlock()
718 cursor.beginEditBlock()
711 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
719 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
712 cursor.removeSelectedText()
720 cursor.removeSelectedText()
713
721
714 # Insert new text with continuation prompts.
722 # Insert new text with continuation prompts.
715 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
723 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
716 cursor.endEditBlock()
724 cursor.endEditBlock()
717 self._control.moveCursor(QtGui.QTextCursor.End)
725 self._control.moveCursor(QtGui.QTextCursor.End)
718
726
719 input_buffer = property(_get_input_buffer, _set_input_buffer)
727 input_buffer = property(_get_input_buffer, _set_input_buffer)
720
728
721 def _get_font(self):
729 def _get_font(self):
722 """ The base font being used by the ConsoleWidget.
730 """ The base font being used by the ConsoleWidget.
723 """
731 """
724 return self._control.document().defaultFont()
732 return self._control.document().defaultFont()
725
733
726 def _set_font(self, font):
734 def _set_font(self, font):
727 """ Sets the base font for the ConsoleWidget to the specified QFont.
735 """ Sets the base font for the ConsoleWidget to the specified QFont.
728 """
736 """
729 font_metrics = QtGui.QFontMetrics(font)
737 font_metrics = QtGui.QFontMetrics(font)
730 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
738 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
731
739
732 self._completion_widget.setFont(font)
740 self._completion_widget.setFont(font)
733 self._control.document().setDefaultFont(font)
741 self._control.document().setDefaultFont(font)
734 if self._page_control:
742 if self._page_control:
735 self._page_control.document().setDefaultFont(font)
743 self._page_control.document().setDefaultFont(font)
736
744
737 self.font_changed.emit(font)
745 self.font_changed.emit(font)
738
746
739 font = property(_get_font, _set_font)
747 font = property(_get_font, _set_font)
740
748
741 def open_anchor(self, anchor):
749 def open_anchor(self, anchor):
742 """ Open selected anchor in the default webbrowser
750 """ Open selected anchor in the default webbrowser
743 """
751 """
744 webbrowser.open( anchor )
752 webbrowser.open( anchor )
745
753
746 def paste(self, mode=QtGui.QClipboard.Clipboard):
754 def paste(self, mode=QtGui.QClipboard.Clipboard):
747 """ Paste the contents of the clipboard into the input region.
755 """ Paste the contents of the clipboard into the input region.
748
756
749 Parameters
757 Parameters
750 ----------
758 ----------
751 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
759 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
752
760
753 Controls which part of the system clipboard is used. This can be
761 Controls which part of the system clipboard is used. This can be
754 used to access the selection clipboard in X11 and the Find buffer
762 used to access the selection clipboard in X11 and the Find buffer
755 in Mac OS. By default, the regular clipboard is used.
763 in Mac OS. By default, the regular clipboard is used.
756 """
764 """
757 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
765 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
758 # Make sure the paste is safe.
766 # Make sure the paste is safe.
759 self._keep_cursor_in_buffer()
767 self._keep_cursor_in_buffer()
760 cursor = self._control.textCursor()
768 cursor = self._control.textCursor()
761
769
762 # Remove any trailing newline, which confuses the GUI and forces the
770 # Remove any trailing newline, which confuses the GUI and forces the
763 # user to backspace.
771 # user to backspace.
764 text = QtGui.QApplication.clipboard().text(mode).rstrip()
772 text = QtGui.QApplication.clipboard().text(mode).rstrip()
765 self._insert_plain_text_into_buffer(cursor, dedent(text))
773 self._insert_plain_text_into_buffer(cursor, dedent(text))
766
774
767 def print_(self, printer = None):
775 def print_(self, printer = None):
768 """ Print the contents of the ConsoleWidget to the specified QPrinter.
776 """ Print the contents of the ConsoleWidget to the specified QPrinter.
769 """
777 """
770 if (not printer):
778 if (not printer):
771 printer = QtGui.QPrinter()
779 printer = QtGui.QPrinter()
772 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
780 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
773 return
781 return
774 self._control.print_(printer)
782 self._control.print_(printer)
775
783
776 def prompt_to_top(self):
784 def prompt_to_top(self):
777 """ Moves the prompt to the top of the viewport.
785 """ Moves the prompt to the top of the viewport.
778 """
786 """
779 if not self._executing:
787 if not self._executing:
780 prompt_cursor = self._get_prompt_cursor()
788 prompt_cursor = self._get_prompt_cursor()
781 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
789 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
782 self._set_cursor(prompt_cursor)
790 self._set_cursor(prompt_cursor)
783 self._set_top_cursor(prompt_cursor)
791 self._set_top_cursor(prompt_cursor)
784
792
785 def redo(self):
793 def redo(self):
786 """ Redo the last operation. If there is no operation to redo, nothing
794 """ Redo the last operation. If there is no operation to redo, nothing
787 happens.
795 happens.
788 """
796 """
789 self._control.redo()
797 self._control.redo()
790
798
791 def reset_font(self):
799 def reset_font(self):
792 """ Sets the font to the default fixed-width font for this platform.
800 """ Sets the font to the default fixed-width font for this platform.
793 """
801 """
794 if sys.platform == 'win32':
802 if sys.platform == 'win32':
795 # Consolas ships with Vista/Win7, fallback to Courier if needed
803 # Consolas ships with Vista/Win7, fallback to Courier if needed
796 fallback = 'Courier'
804 fallback = 'Courier'
797 elif sys.platform == 'darwin':
805 elif sys.platform == 'darwin':
798 # OSX always has Monaco
806 # OSX always has Monaco
799 fallback = 'Monaco'
807 fallback = 'Monaco'
800 else:
808 else:
801 # Monospace should always exist
809 # Monospace should always exist
802 fallback = 'Monospace'
810 fallback = 'Monospace'
803 font = get_font(self.font_family, fallback)
811 font = get_font(self.font_family, fallback)
804 if self.font_size:
812 if self.font_size:
805 font.setPointSize(self.font_size)
813 font.setPointSize(self.font_size)
806 else:
814 else:
807 font.setPointSize(QtGui.qApp.font().pointSize())
815 font.setPointSize(QtGui.qApp.font().pointSize())
808 font.setStyleHint(QtGui.QFont.TypeWriter)
816 font.setStyleHint(QtGui.QFont.TypeWriter)
809 self._set_font(font)
817 self._set_font(font)
810
818
811 def change_font_size(self, delta):
819 def change_font_size(self, delta):
812 """Change the font size by the specified amount (in points).
820 """Change the font size by the specified amount (in points).
813 """
821 """
814 font = self.font
822 font = self.font
815 size = max(font.pointSize() + delta, 1) # minimum 1 point
823 size = max(font.pointSize() + delta, 1) # minimum 1 point
816 font.setPointSize(size)
824 font.setPointSize(size)
817 self._set_font(font)
825 self._set_font(font)
818
826
819 def _increase_font_size(self):
827 def _increase_font_size(self):
820 self.change_font_size(1)
828 self.change_font_size(1)
821
829
822 def _decrease_font_size(self):
830 def _decrease_font_size(self):
823 self.change_font_size(-1)
831 self.change_font_size(-1)
824
832
825 def select_all(self):
833 def select_all(self):
826 """ Selects all the text in the buffer.
834 """ Selects all the text in the buffer.
827 """
835 """
828 self._control.selectAll()
836 self._control.selectAll()
829
837
830 def _get_tab_width(self):
838 def _get_tab_width(self):
831 """ The width (in terms of space characters) for tab characters.
839 """ The width (in terms of space characters) for tab characters.
832 """
840 """
833 return self._tab_width
841 return self._tab_width
834
842
835 def _set_tab_width(self, tab_width):
843 def _set_tab_width(self, tab_width):
836 """ Sets the width (in terms of space characters) for tab characters.
844 """ Sets the width (in terms of space characters) for tab characters.
837 """
845 """
838 font_metrics = QtGui.QFontMetrics(self.font)
846 font_metrics = QtGui.QFontMetrics(self.font)
839 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
847 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
840
848
841 self._tab_width = tab_width
849 self._tab_width = tab_width
842
850
843 tab_width = property(_get_tab_width, _set_tab_width)
851 tab_width = property(_get_tab_width, _set_tab_width)
844
852
845 def undo(self):
853 def undo(self):
846 """ Undo the last operation. If there is no operation to undo, nothing
854 """ Undo the last operation. If there is no operation to undo, nothing
847 happens.
855 happens.
848 """
856 """
849 self._control.undo()
857 self._control.undo()
850
858
851 #---------------------------------------------------------------------------
859 #---------------------------------------------------------------------------
852 # 'ConsoleWidget' abstract interface
860 # 'ConsoleWidget' abstract interface
853 #---------------------------------------------------------------------------
861 #---------------------------------------------------------------------------
854
862
855 def _is_complete(self, source, interactive):
863 def _is_complete(self, source, interactive):
856 """ Returns whether 'source' can be executed. When triggered by an
864 """ Returns whether 'source' can be executed. When triggered by an
857 Enter/Return key press, 'interactive' is True; otherwise, it is
865 Enter/Return key press, 'interactive' is True; otherwise, it is
858 False.
866 False.
859 """
867 """
860 raise NotImplementedError
868 raise NotImplementedError
861
869
862 def _execute(self, source, hidden):
870 def _execute(self, source, hidden):
863 """ Execute 'source'. If 'hidden', do not show any output.
871 """ Execute 'source'. If 'hidden', do not show any output.
864 """
872 """
865 raise NotImplementedError
873 raise NotImplementedError
866
874
867 def _prompt_started_hook(self):
875 def _prompt_started_hook(self):
868 """ Called immediately after a new prompt is displayed.
876 """ Called immediately after a new prompt is displayed.
869 """
877 """
870 pass
878 pass
871
879
872 def _prompt_finished_hook(self):
880 def _prompt_finished_hook(self):
873 """ Called immediately after a prompt is finished, i.e. when some input
881 """ Called immediately after a prompt is finished, i.e. when some input
874 will be processed and a new prompt displayed.
882 will be processed and a new prompt displayed.
875 """
883 """
876 pass
884 pass
877
885
878 def _up_pressed(self, shift_modifier):
886 def _up_pressed(self, shift_modifier):
879 """ Called when the up key is pressed. Returns whether to continue
887 """ Called when the up key is pressed. Returns whether to continue
880 processing the event.
888 processing the event.
881 """
889 """
882 return True
890 return True
883
891
884 def _down_pressed(self, shift_modifier):
892 def _down_pressed(self, shift_modifier):
885 """ Called when the down key is pressed. Returns whether to continue
893 """ Called when the down key is pressed. Returns whether to continue
886 processing the event.
894 processing the event.
887 """
895 """
888 return True
896 return True
889
897
890 def _tab_pressed(self):
898 def _tab_pressed(self):
891 """ Called when the tab key is pressed. Returns whether to continue
899 """ Called when the tab key is pressed. Returns whether to continue
892 processing the event.
900 processing the event.
893 """
901 """
894 return False
902 return False
895
903
896 #--------------------------------------------------------------------------
904 #--------------------------------------------------------------------------
897 # 'ConsoleWidget' protected interface
905 # 'ConsoleWidget' protected interface
898 #--------------------------------------------------------------------------
906 #--------------------------------------------------------------------------
899
907
900 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
908 def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
901 """ A low-level method for appending content to the end of the buffer.
909 """ A low-level method for appending content to the end of the buffer.
902
910
903 If 'before_prompt' is enabled, the content will be inserted before the
911 If 'before_prompt' is enabled, the content will be inserted before the
904 current prompt, if there is one.
912 current prompt, if there is one.
905 """
913 """
906 # Determine where to insert the content.
914 # Determine where to insert the content.
907 cursor = self._control.textCursor()
915 cursor = self._control.textCursor()
908 if before_prompt and (self._reading or not self._executing):
916 if before_prompt and (self._reading or not self._executing):
909 self._flush_pending_stream()
917 self._flush_pending_stream()
910 cursor.setPosition(self._append_before_prompt_pos)
918 cursor.setPosition(self._append_before_prompt_pos)
911 else:
919 else:
912 if insert != self._insert_plain_text:
920 if insert != self._insert_plain_text:
913 self._flush_pending_stream()
921 self._flush_pending_stream()
914 cursor.movePosition(QtGui.QTextCursor.End)
922 cursor.movePosition(QtGui.QTextCursor.End)
915 start_pos = cursor.position()
923 start_pos = cursor.position()
916
924
917 # Perform the insertion.
925 # Perform the insertion.
918 result = insert(cursor, input, *args, **kwargs)
926 result = insert(cursor, input, *args, **kwargs)
919
927
920 # Adjust the prompt position if we have inserted before it. This is safe
928 # Adjust the prompt position if we have inserted before it. This is safe
921 # because buffer truncation is disabled when not executing.
929 # because buffer truncation is disabled when not executing.
922 if before_prompt and (self._reading or not self._executing):
930 if before_prompt and (self._reading or not self._executing):
923 diff = cursor.position() - start_pos
931 diff = cursor.position() - start_pos
924 self._append_before_prompt_pos += diff
932 self._append_before_prompt_pos += diff
925 self._prompt_pos += diff
933 self._prompt_pos += diff
926
934
927 return result
935 return result
928
936
929 def _append_block(self, block_format=None, before_prompt=False):
937 def _append_block(self, block_format=None, before_prompt=False):
930 """ Appends an new QTextBlock to the end of the console buffer.
938 """ Appends an new QTextBlock to the end of the console buffer.
931 """
939 """
932 self._append_custom(self._insert_block, block_format, before_prompt)
940 self._append_custom(self._insert_block, block_format, before_prompt)
933
941
934 def _append_html(self, html, before_prompt=False):
942 def _append_html(self, html, before_prompt=False):
935 """ Appends HTML at the end of the console buffer.
943 """ Appends HTML at the end of the console buffer.
936 """
944 """
937 self._append_custom(self._insert_html, html, before_prompt)
945 self._append_custom(self._insert_html, html, before_prompt)
938
946
939 def _append_html_fetching_plain_text(self, html, before_prompt=False):
947 def _append_html_fetching_plain_text(self, html, before_prompt=False):
940 """ Appends HTML, then returns the plain text version of it.
948 """ Appends HTML, then returns the plain text version of it.
941 """
949 """
942 return self._append_custom(self._insert_html_fetching_plain_text,
950 return self._append_custom(self._insert_html_fetching_plain_text,
943 html, before_prompt)
951 html, before_prompt)
944
952
945 def _append_plain_text(self, text, before_prompt=False):
953 def _append_plain_text(self, text, before_prompt=False):
946 """ Appends plain text, processing ANSI codes if enabled.
954 """ Appends plain text, processing ANSI codes if enabled.
947 """
955 """
948 self._append_custom(self._insert_plain_text, text, before_prompt)
956 self._append_custom(self._insert_plain_text, text, before_prompt)
949
957
950 def _cancel_completion(self):
958 def _cancel_completion(self):
951 """ If text completion is progress, cancel it.
959 """ If text completion is progress, cancel it.
952 """
960 """
953 self._completion_widget.cancel_completion()
961 self._completion_widget.cancel_completion()
954
962
955 def _clear_temporary_buffer(self):
963 def _clear_temporary_buffer(self):
956 """ Clears the "temporary text" buffer, i.e. all the text following
964 """ Clears the "temporary text" buffer, i.e. all the text following
957 the prompt region.
965 the prompt region.
958 """
966 """
959 # Select and remove all text below the input buffer.
967 # Select and remove all text below the input buffer.
960 cursor = self._get_prompt_cursor()
968 cursor = self._get_prompt_cursor()
961 prompt = self._continuation_prompt.lstrip()
969 prompt = self._continuation_prompt.lstrip()
962 if(self._temp_buffer_filled):
970 if(self._temp_buffer_filled):
963 self._temp_buffer_filled = False
971 self._temp_buffer_filled = False
964 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
972 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
965 temp_cursor = QtGui.QTextCursor(cursor)
973 temp_cursor = QtGui.QTextCursor(cursor)
966 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
974 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
967 text = temp_cursor.selection().toPlainText().lstrip()
975 text = temp_cursor.selection().toPlainText().lstrip()
968 if not text.startswith(prompt):
976 if not text.startswith(prompt):
969 break
977 break
970 else:
978 else:
971 # We've reached the end of the input buffer and no text follows.
979 # We've reached the end of the input buffer and no text follows.
972 return
980 return
973 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
981 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
974 cursor.movePosition(QtGui.QTextCursor.End,
982 cursor.movePosition(QtGui.QTextCursor.End,
975 QtGui.QTextCursor.KeepAnchor)
983 QtGui.QTextCursor.KeepAnchor)
976 cursor.removeSelectedText()
984 cursor.removeSelectedText()
977
985
978 # After doing this, we have no choice but to clear the undo/redo
986 # After doing this, we have no choice but to clear the undo/redo
979 # history. Otherwise, the text is not "temporary" at all, because it
987 # history. Otherwise, the text is not "temporary" at all, because it
980 # can be recalled with undo/redo. Unfortunately, Qt does not expose
988 # can be recalled with undo/redo. Unfortunately, Qt does not expose
981 # fine-grained control to the undo/redo system.
989 # fine-grained control to the undo/redo system.
982 if self._control.isUndoRedoEnabled():
990 if self._control.isUndoRedoEnabled():
983 self._control.setUndoRedoEnabled(False)
991 self._control.setUndoRedoEnabled(False)
984 self._control.setUndoRedoEnabled(True)
992 self._control.setUndoRedoEnabled(True)
985
993
986 def _complete_with_items(self, cursor, items):
994 def _complete_with_items(self, cursor, items):
987 """ Performs completion with 'items' at the specified cursor location.
995 """ Performs completion with 'items' at the specified cursor location.
988 """
996 """
989 self._cancel_completion()
997 self._cancel_completion()
990
998
991 if len(items) == 1:
999 if len(items) == 1:
992 cursor.setPosition(self._control.textCursor().position(),
1000 cursor.setPosition(self._control.textCursor().position(),
993 QtGui.QTextCursor.KeepAnchor)
1001 QtGui.QTextCursor.KeepAnchor)
994 cursor.insertText(items[0])
1002 cursor.insertText(items[0])
995
1003
996 elif len(items) > 1:
1004 elif len(items) > 1:
997 current_pos = self._control.textCursor().position()
1005 current_pos = self._control.textCursor().position()
998 prefix = commonprefix(items)
1006 prefix = commonprefix(items)
999 if prefix:
1007 if prefix:
1000 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1008 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
1001 cursor.insertText(prefix)
1009 cursor.insertText(prefix)
1002 current_pos = cursor.position()
1010 current_pos = cursor.position()
1003
1011
1004 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1012 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
1005 self._completion_widget.show_items(cursor, items)
1013 self._completion_widget.show_items(cursor, items)
1006
1014
1007
1015
1008 def _fill_temporary_buffer(self, cursor, text, html=False):
1016 def _fill_temporary_buffer(self, cursor, text, html=False):
1009 """fill the area below the active editting zone with text"""
1017 """fill the area below the active editting zone with text"""
1010
1018
1011 current_pos = self._control.textCursor().position()
1019 current_pos = self._control.textCursor().position()
1012
1020
1013 cursor.beginEditBlock()
1021 cursor.beginEditBlock()
1014 self._append_plain_text('\n')
1022 self._append_plain_text('\n')
1015 self._page(text, html=html)
1023 self._page(text, html=html)
1016 cursor.endEditBlock()
1024 cursor.endEditBlock()
1017
1025
1018 cursor.setPosition(current_pos)
1026 cursor.setPosition(current_pos)
1019 self._control.moveCursor(QtGui.QTextCursor.End)
1027 self._control.moveCursor(QtGui.QTextCursor.End)
1020 self._control.setTextCursor(cursor)
1028 self._control.setTextCursor(cursor)
1021
1029
1022 self._temp_buffer_filled = True
1030 self._temp_buffer_filled = True
1023
1031
1024
1032
1025 def _context_menu_make(self, pos):
1033 def _context_menu_make(self, pos):
1026 """ Creates a context menu for the given QPoint (in widget coordinates).
1034 """ Creates a context menu for the given QPoint (in widget coordinates).
1027 """
1035 """
1028 menu = QtGui.QMenu(self)
1036 menu = QtGui.QMenu(self)
1029
1037
1030 self.cut_action = menu.addAction('Cut', self.cut)
1038 self.cut_action = menu.addAction('Cut', self.cut)
1031 self.cut_action.setEnabled(self.can_cut())
1039 self.cut_action.setEnabled(self.can_cut())
1032 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1040 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
1033
1041
1034 self.copy_action = menu.addAction('Copy', self.copy)
1042 self.copy_action = menu.addAction('Copy', self.copy)
1035 self.copy_action.setEnabled(self.can_copy())
1043 self.copy_action.setEnabled(self.can_copy())
1036 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1044 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
1037
1045
1038 self.paste_action = menu.addAction('Paste', self.paste)
1046 self.paste_action = menu.addAction('Paste', self.paste)
1039 self.paste_action.setEnabled(self.can_paste())
1047 self.paste_action.setEnabled(self.can_paste())
1040 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1048 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
1041
1049
1042 anchor = self._control.anchorAt(pos)
1050 anchor = self._control.anchorAt(pos)
1043 if anchor:
1051 if anchor:
1044 menu.addSeparator()
1052 menu.addSeparator()
1045 self.copy_link_action = menu.addAction(
1053 self.copy_link_action = menu.addAction(
1046 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1054 'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1047 self.open_link_action = menu.addAction(
1055 self.open_link_action = menu.addAction(
1048 'Open Link', lambda: self.open_anchor(anchor=anchor))
1056 'Open Link', lambda: self.open_anchor(anchor=anchor))
1049
1057
1050 menu.addSeparator()
1058 menu.addSeparator()
1051 menu.addAction(self.select_all_action)
1059 menu.addAction(self.select_all_action)
1052
1060
1053 menu.addSeparator()
1061 menu.addSeparator()
1054 menu.addAction(self.export_action)
1062 menu.addAction(self.export_action)
1055 menu.addAction(self.print_action)
1063 menu.addAction(self.print_action)
1056
1064
1057 return menu
1065 return menu
1058
1066
1059 def _control_key_down(self, modifiers, include_command=False):
1067 def _control_key_down(self, modifiers, include_command=False):
1060 """ Given a KeyboardModifiers flags object, return whether the Control
1068 """ Given a KeyboardModifiers flags object, return whether the Control
1061 key is down.
1069 key is down.
1062
1070
1063 Parameters
1071 Parameters
1064 ----------
1072 ----------
1065 include_command : bool, optional (default True)
1073 include_command : bool, optional (default True)
1066 Whether to treat the Command key as a (mutually exclusive) synonym
1074 Whether to treat the Command key as a (mutually exclusive) synonym
1067 for Control when in Mac OS.
1075 for Control when in Mac OS.
1068 """
1076 """
1069 # Note that on Mac OS, ControlModifier corresponds to the Command key
1077 # Note that on Mac OS, ControlModifier corresponds to the Command key
1070 # while MetaModifier corresponds to the Control key.
1078 # while MetaModifier corresponds to the Control key.
1071 if sys.platform == 'darwin':
1079 if sys.platform == 'darwin':
1072 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1080 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1073 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1081 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1074 else:
1082 else:
1075 return bool(modifiers & QtCore.Qt.ControlModifier)
1083 return bool(modifiers & QtCore.Qt.ControlModifier)
1076
1084
1077 def _create_control(self):
1085 def _create_control(self):
1078 """ Creates and connects the underlying text widget.
1086 """ Creates and connects the underlying text widget.
1079 """
1087 """
1080 # Create the underlying control.
1088 # Create the underlying control.
1081 if self.custom_control:
1089 if self.custom_control:
1082 control = self.custom_control()
1090 control = self.custom_control()
1083 elif self.kind == 'plain':
1091 elif self.kind == 'plain':
1084 control = QtGui.QPlainTextEdit()
1092 control = QtGui.QPlainTextEdit()
1085 elif self.kind == 'rich':
1093 elif self.kind == 'rich':
1086 control = QtGui.QTextEdit()
1094 control = QtGui.QTextEdit()
1087 control.setAcceptRichText(False)
1095 control.setAcceptRichText(False)
1088 control.setMouseTracking(True)
1096 control.setMouseTracking(True)
1089
1097
1090 # Prevent the widget from handling drops, as we already provide
1098 # Prevent the widget from handling drops, as we already provide
1091 # the logic in this class.
1099 # the logic in this class.
1092 control.setAcceptDrops(False)
1100 control.setAcceptDrops(False)
1093
1101
1094 # Install event filters. The filter on the viewport is needed for
1102 # Install event filters. The filter on the viewport is needed for
1095 # mouse events.
1103 # mouse events.
1096 control.installEventFilter(self)
1104 control.installEventFilter(self)
1097 control.viewport().installEventFilter(self)
1105 control.viewport().installEventFilter(self)
1098
1106
1099 # Connect signals.
1107 # Connect signals.
1100 control.customContextMenuRequested.connect(
1108 control.customContextMenuRequested.connect(
1101 self._custom_context_menu_requested)
1109 self._custom_context_menu_requested)
1102 control.copyAvailable.connect(self.copy_available)
1110 control.copyAvailable.connect(self.copy_available)
1103 control.redoAvailable.connect(self.redo_available)
1111 control.redoAvailable.connect(self.redo_available)
1104 control.undoAvailable.connect(self.undo_available)
1112 control.undoAvailable.connect(self.undo_available)
1105
1113
1106 # Hijack the document size change signal to prevent Qt from adjusting
1114 # Hijack the document size change signal to prevent Qt from adjusting
1107 # the viewport's scrollbar. We are relying on an implementation detail
1115 # the viewport's scrollbar. We are relying on an implementation detail
1108 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1116 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1109 # this functionality we cannot create a nice terminal interface.
1117 # this functionality we cannot create a nice terminal interface.
1110 layout = control.document().documentLayout()
1118 layout = control.document().documentLayout()
1111 layout.documentSizeChanged.disconnect()
1119 layout.documentSizeChanged.disconnect()
1112 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1120 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1113
1121
1114 # Configure the control.
1122 # Configure the control.
1115 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1123 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1116 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1124 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1117 control.setReadOnly(True)
1125 control.setReadOnly(True)
1118 control.setUndoRedoEnabled(False)
1126 control.setUndoRedoEnabled(False)
1119 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1127 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1120 return control
1128 return control
1121
1129
1122 def _create_page_control(self):
1130 def _create_page_control(self):
1123 """ Creates and connects the underlying paging widget.
1131 """ Creates and connects the underlying paging widget.
1124 """
1132 """
1125 if self.custom_page_control:
1133 if self.custom_page_control:
1126 control = self.custom_page_control()
1134 control = self.custom_page_control()
1127 elif self.kind == 'plain':
1135 elif self.kind == 'plain':
1128 control = QtGui.QPlainTextEdit()
1136 control = QtGui.QPlainTextEdit()
1129 elif self.kind == 'rich':
1137 elif self.kind == 'rich':
1130 control = QtGui.QTextEdit()
1138 control = QtGui.QTextEdit()
1131 control.installEventFilter(self)
1139 control.installEventFilter(self)
1132 viewport = control.viewport()
1140 viewport = control.viewport()
1133 viewport.installEventFilter(self)
1141 viewport.installEventFilter(self)
1134 control.setReadOnly(True)
1142 control.setReadOnly(True)
1135 control.setUndoRedoEnabled(False)
1143 control.setUndoRedoEnabled(False)
1136 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1144 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1137 return control
1145 return control
1138
1146
1139 def _event_filter_console_keypress(self, event):
1147 def _event_filter_console_keypress(self, event):
1140 """ Filter key events for the underlying text widget to create a
1148 """ Filter key events for the underlying text widget to create a
1141 console-like interface.
1149 console-like interface.
1142 """
1150 """
1143 intercepted = False
1151 intercepted = False
1144 cursor = self._control.textCursor()
1152 cursor = self._control.textCursor()
1145 position = cursor.position()
1153 position = cursor.position()
1146 key = event.key()
1154 key = event.key()
1147 ctrl_down = self._control_key_down(event.modifiers())
1155 ctrl_down = self._control_key_down(event.modifiers())
1148 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1156 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1149 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1157 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1150
1158
1151 #------ Special sequences ----------------------------------------------
1159 #------ Special sequences ----------------------------------------------
1152
1160
1153 if event.matches(QtGui.QKeySequence.Copy):
1161 if event.matches(QtGui.QKeySequence.Copy):
1154 self.copy()
1162 self.copy()
1155 intercepted = True
1163 intercepted = True
1156
1164
1157 elif event.matches(QtGui.QKeySequence.Cut):
1165 elif event.matches(QtGui.QKeySequence.Cut):
1158 self.cut()
1166 self.cut()
1159 intercepted = True
1167 intercepted = True
1160
1168
1161 elif event.matches(QtGui.QKeySequence.Paste):
1169 elif event.matches(QtGui.QKeySequence.Paste):
1162 self.paste()
1170 self.paste()
1163 intercepted = True
1171 intercepted = True
1164
1172
1165 #------ Special modifier logic -----------------------------------------
1173 #------ Special modifier logic -----------------------------------------
1166
1174
1167 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1175 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1168 intercepted = True
1176 intercepted = True
1169
1177
1170 # Special handling when tab completing in text mode.
1178 # Special handling when tab completing in text mode.
1171 self._cancel_completion()
1179 self._cancel_completion()
1172
1180
1173 if self._in_buffer(position):
1181 if self._in_buffer(position):
1174 # Special handling when a reading a line of raw input.
1182 # Special handling when a reading a line of raw input.
1175 if self._reading:
1183 if self._reading:
1176 self._append_plain_text('\n')
1184 self._append_plain_text('\n')
1177 self._reading = False
1185 self._reading = False
1178 if self._reading_callback:
1186 if self._reading_callback:
1179 self._reading_callback()
1187 self._reading_callback()
1180
1188
1181 # If the input buffer is a single line or there is only
1189 # If the input buffer is a single line or there is only
1182 # whitespace after the cursor, execute. Otherwise, split the
1190 # whitespace after the cursor, execute. Otherwise, split the
1183 # line with a continuation prompt.
1191 # line with a continuation prompt.
1184 elif not self._executing:
1192 elif not self._executing:
1185 cursor.movePosition(QtGui.QTextCursor.End,
1193 cursor.movePosition(QtGui.QTextCursor.End,
1186 QtGui.QTextCursor.KeepAnchor)
1194 QtGui.QTextCursor.KeepAnchor)
1187 at_end = len(cursor.selectedText().strip()) == 0
1195 at_end = len(cursor.selectedText().strip()) == 0
1188 single_line = (self._get_end_cursor().blockNumber() ==
1196 single_line = (self._get_end_cursor().blockNumber() ==
1189 self._get_prompt_cursor().blockNumber())
1197 self._get_prompt_cursor().blockNumber())
1190 if (at_end or shift_down or single_line) and not ctrl_down:
1198 if (at_end or shift_down or single_line) and not ctrl_down:
1191 self.execute(interactive = not shift_down)
1199 self.execute(interactive = not shift_down)
1192 else:
1200 else:
1193 # Do this inside an edit block for clean undo/redo.
1201 # Do this inside an edit block for clean undo/redo.
1194 cursor.beginEditBlock()
1202 cursor.beginEditBlock()
1195 cursor.setPosition(position)
1203 cursor.setPosition(position)
1196 cursor.insertText('\n')
1204 cursor.insertText('\n')
1197 self._insert_continuation_prompt(cursor)
1205 self._insert_continuation_prompt(cursor)
1198 cursor.endEditBlock()
1206 cursor.endEditBlock()
1199
1207
1200 # Ensure that the whole input buffer is visible.
1208 # Ensure that the whole input buffer is visible.
1201 # FIXME: This will not be usable if the input buffer is
1209 # FIXME: This will not be usable if the input buffer is
1202 # taller than the console widget.
1210 # taller than the console widget.
1203 self._control.moveCursor(QtGui.QTextCursor.End)
1211 self._control.moveCursor(QtGui.QTextCursor.End)
1204 self._control.setTextCursor(cursor)
1212 self._control.setTextCursor(cursor)
1205
1213
1206 #------ Control/Cmd modifier -------------------------------------------
1214 #------ Control/Cmd modifier -------------------------------------------
1207
1215
1208 elif ctrl_down:
1216 elif ctrl_down:
1209 if key == QtCore.Qt.Key_G:
1217 if key == QtCore.Qt.Key_G:
1210 self._keyboard_quit()
1218 self._keyboard_quit()
1211 intercepted = True
1219 intercepted = True
1212
1220
1213 elif key == QtCore.Qt.Key_K:
1221 elif key == QtCore.Qt.Key_K:
1214 if self._in_buffer(position):
1222 if self._in_buffer(position):
1215 cursor.clearSelection()
1223 cursor.clearSelection()
1216 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1224 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1217 QtGui.QTextCursor.KeepAnchor)
1225 QtGui.QTextCursor.KeepAnchor)
1218 if not cursor.hasSelection():
1226 if not cursor.hasSelection():
1219 # Line deletion (remove continuation prompt)
1227 # Line deletion (remove continuation prompt)
1220 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1228 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1221 QtGui.QTextCursor.KeepAnchor)
1229 QtGui.QTextCursor.KeepAnchor)
1222 cursor.movePosition(QtGui.QTextCursor.Right,
1230 cursor.movePosition(QtGui.QTextCursor.Right,
1223 QtGui.QTextCursor.KeepAnchor,
1231 QtGui.QTextCursor.KeepAnchor,
1224 len(self._continuation_prompt))
1232 len(self._continuation_prompt))
1225 self._kill_ring.kill_cursor(cursor)
1233 self._kill_ring.kill_cursor(cursor)
1226 self._set_cursor(cursor)
1234 self._set_cursor(cursor)
1227 intercepted = True
1235 intercepted = True
1228
1236
1229 elif key == QtCore.Qt.Key_L:
1237 elif key == QtCore.Qt.Key_L:
1230 self.prompt_to_top()
1238 self.prompt_to_top()
1231 intercepted = True
1239 intercepted = True
1232
1240
1233 elif key == QtCore.Qt.Key_O:
1241 elif key == QtCore.Qt.Key_O:
1234 if self._page_control and self._page_control.isVisible():
1242 if self._page_control and self._page_control.isVisible():
1235 self._page_control.setFocus()
1243 self._page_control.setFocus()
1236 intercepted = True
1244 intercepted = True
1237
1245
1238 elif key == QtCore.Qt.Key_U:
1246 elif key == QtCore.Qt.Key_U:
1239 if self._in_buffer(position):
1247 if self._in_buffer(position):
1240 cursor.clearSelection()
1248 cursor.clearSelection()
1241 start_line = cursor.blockNumber()
1249 start_line = cursor.blockNumber()
1242 if start_line == self._get_prompt_cursor().blockNumber():
1250 if start_line == self._get_prompt_cursor().blockNumber():
1243 offset = len(self._prompt)
1251 offset = len(self._prompt)
1244 else:
1252 else:
1245 offset = len(self._continuation_prompt)
1253 offset = len(self._continuation_prompt)
1246 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1247 QtGui.QTextCursor.KeepAnchor)
1255 QtGui.QTextCursor.KeepAnchor)
1248 cursor.movePosition(QtGui.QTextCursor.Right,
1256 cursor.movePosition(QtGui.QTextCursor.Right,
1249 QtGui.QTextCursor.KeepAnchor, offset)
1257 QtGui.QTextCursor.KeepAnchor, offset)
1250 self._kill_ring.kill_cursor(cursor)
1258 self._kill_ring.kill_cursor(cursor)
1251 self._set_cursor(cursor)
1259 self._set_cursor(cursor)
1252 intercepted = True
1260 intercepted = True
1253
1261
1254 elif key == QtCore.Qt.Key_Y:
1262 elif key == QtCore.Qt.Key_Y:
1255 self._keep_cursor_in_buffer()
1263 self._keep_cursor_in_buffer()
1256 self._kill_ring.yank()
1264 self._kill_ring.yank()
1257 intercepted = True
1265 intercepted = True
1258
1266
1259 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1267 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1260 if key == QtCore.Qt.Key_Backspace:
1268 if key == QtCore.Qt.Key_Backspace:
1261 cursor = self._get_word_start_cursor(position)
1269 cursor = self._get_word_start_cursor(position)
1262 else: # key == QtCore.Qt.Key_Delete
1270 else: # key == QtCore.Qt.Key_Delete
1263 cursor = self._get_word_end_cursor(position)
1271 cursor = self._get_word_end_cursor(position)
1264 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1272 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1265 self._kill_ring.kill_cursor(cursor)
1273 self._kill_ring.kill_cursor(cursor)
1266 intercepted = True
1274 intercepted = True
1267
1275
1268 elif key == QtCore.Qt.Key_D:
1276 elif key == QtCore.Qt.Key_D:
1269 if len(self.input_buffer) == 0:
1277 if len(self.input_buffer) == 0:
1270 self.exit_requested.emit(self)
1278 self.exit_requested.emit(self)
1271 else:
1279 else:
1272 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1280 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1273 QtCore.Qt.Key_Delete,
1281 QtCore.Qt.Key_Delete,
1274 QtCore.Qt.NoModifier)
1282 QtCore.Qt.NoModifier)
1275 QtGui.qApp.sendEvent(self._control, new_event)
1283 QtGui.qApp.sendEvent(self._control, new_event)
1276 intercepted = True
1284 intercepted = True
1277
1285
1278 #------ Alt modifier ---------------------------------------------------
1286 #------ Alt modifier ---------------------------------------------------
1279
1287
1280 elif alt_down:
1288 elif alt_down:
1281 if key == QtCore.Qt.Key_B:
1289 if key == QtCore.Qt.Key_B:
1282 self._set_cursor(self._get_word_start_cursor(position))
1290 self._set_cursor(self._get_word_start_cursor(position))
1283 intercepted = True
1291 intercepted = True
1284
1292
1285 elif key == QtCore.Qt.Key_F:
1293 elif key == QtCore.Qt.Key_F:
1286 self._set_cursor(self._get_word_end_cursor(position))
1294 self._set_cursor(self._get_word_end_cursor(position))
1287 intercepted = True
1295 intercepted = True
1288
1296
1289 elif key == QtCore.Qt.Key_Y:
1297 elif key == QtCore.Qt.Key_Y:
1290 self._kill_ring.rotate()
1298 self._kill_ring.rotate()
1291 intercepted = True
1299 intercepted = True
1292
1300
1293 elif key == QtCore.Qt.Key_Backspace:
1301 elif key == QtCore.Qt.Key_Backspace:
1294 cursor = self._get_word_start_cursor(position)
1302 cursor = self._get_word_start_cursor(position)
1295 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1303 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1296 self._kill_ring.kill_cursor(cursor)
1304 self._kill_ring.kill_cursor(cursor)
1297 intercepted = True
1305 intercepted = True
1298
1306
1299 elif key == QtCore.Qt.Key_D:
1307 elif key == QtCore.Qt.Key_D:
1300 cursor = self._get_word_end_cursor(position)
1308 cursor = self._get_word_end_cursor(position)
1301 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1309 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1302 self._kill_ring.kill_cursor(cursor)
1310 self._kill_ring.kill_cursor(cursor)
1303 intercepted = True
1311 intercepted = True
1304
1312
1305 elif key == QtCore.Qt.Key_Delete:
1313 elif key == QtCore.Qt.Key_Delete:
1306 intercepted = True
1314 intercepted = True
1307
1315
1308 elif key == QtCore.Qt.Key_Greater:
1316 elif key == QtCore.Qt.Key_Greater:
1309 self._control.moveCursor(QtGui.QTextCursor.End)
1317 self._control.moveCursor(QtGui.QTextCursor.End)
1310 intercepted = True
1318 intercepted = True
1311
1319
1312 elif key == QtCore.Qt.Key_Less:
1320 elif key == QtCore.Qt.Key_Less:
1313 self._control.setTextCursor(self._get_prompt_cursor())
1321 self._control.setTextCursor(self._get_prompt_cursor())
1314 intercepted = True
1322 intercepted = True
1315
1323
1316 #------ No modifiers ---------------------------------------------------
1324 #------ No modifiers ---------------------------------------------------
1317
1325
1318 else:
1326 else:
1319 if shift_down:
1327 if shift_down:
1320 anchormode = QtGui.QTextCursor.KeepAnchor
1328 anchormode = QtGui.QTextCursor.KeepAnchor
1321 else:
1329 else:
1322 anchormode = QtGui.QTextCursor.MoveAnchor
1330 anchormode = QtGui.QTextCursor.MoveAnchor
1323
1331
1324 if key == QtCore.Qt.Key_Escape:
1332 if key == QtCore.Qt.Key_Escape:
1325 self._keyboard_quit()
1333 self._keyboard_quit()
1326 intercepted = True
1334 intercepted = True
1327
1335
1328 elif key == QtCore.Qt.Key_Up:
1336 elif key == QtCore.Qt.Key_Up:
1329 if self._reading or not self._up_pressed(shift_down):
1337 if self._reading or not self._up_pressed(shift_down):
1330 intercepted = True
1338 intercepted = True
1331 else:
1339 else:
1332 prompt_line = self._get_prompt_cursor().blockNumber()
1340 prompt_line = self._get_prompt_cursor().blockNumber()
1333 intercepted = cursor.blockNumber() <= prompt_line
1341 intercepted = cursor.blockNumber() <= prompt_line
1334
1342
1335 elif key == QtCore.Qt.Key_Down:
1343 elif key == QtCore.Qt.Key_Down:
1336 if self._reading or not self._down_pressed(shift_down):
1344 if self._reading or not self._down_pressed(shift_down):
1337 intercepted = True
1345 intercepted = True
1338 else:
1346 else:
1339 end_line = self._get_end_cursor().blockNumber()
1347 end_line = self._get_end_cursor().blockNumber()
1340 intercepted = cursor.blockNumber() == end_line
1348 intercepted = cursor.blockNumber() == end_line
1341
1349
1342 elif key == QtCore.Qt.Key_Tab:
1350 elif key == QtCore.Qt.Key_Tab:
1343 if not self._reading:
1351 if not self._reading:
1344 if self._tab_pressed():
1352 if self._tab_pressed():
1345 # real tab-key, insert four spaces
1353 # real tab-key, insert four spaces
1346 cursor.insertText(' '*4)
1354 cursor.insertText(' '*4)
1347 intercepted = True
1355 intercepted = True
1348
1356
1349 elif key == QtCore.Qt.Key_Left:
1357 elif key == QtCore.Qt.Key_Left:
1350
1358
1351 # Move to the previous line
1359 # Move to the previous line
1352 line, col = cursor.blockNumber(), cursor.columnNumber()
1360 line, col = cursor.blockNumber(), cursor.columnNumber()
1353 if line > self._get_prompt_cursor().blockNumber() and \
1361 if line > self._get_prompt_cursor().blockNumber() and \
1354 col == len(self._continuation_prompt):
1362 col == len(self._continuation_prompt):
1355 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1363 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1356 mode=anchormode)
1364 mode=anchormode)
1357 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1365 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1358 mode=anchormode)
1366 mode=anchormode)
1359 intercepted = True
1367 intercepted = True
1360
1368
1361 # Regular left movement
1369 # Regular left movement
1362 else:
1370 else:
1363 intercepted = not self._in_buffer(position - 1)
1371 intercepted = not self._in_buffer(position - 1)
1364
1372
1365 elif key == QtCore.Qt.Key_Right:
1373 elif key == QtCore.Qt.Key_Right:
1366 original_block_number = cursor.blockNumber()
1374 original_block_number = cursor.blockNumber()
1367 self._control.moveCursor(QtGui.QTextCursor.Right,
1375 self._control.moveCursor(QtGui.QTextCursor.Right,
1368 mode=anchormode)
1376 mode=anchormode)
1369 if cursor.blockNumber() != original_block_number:
1377 if cursor.blockNumber() != original_block_number:
1370 self._control.moveCursor(QtGui.QTextCursor.Right,
1378 self._control.moveCursor(QtGui.QTextCursor.Right,
1371 n=len(self._continuation_prompt),
1379 n=len(self._continuation_prompt),
1372 mode=anchormode)
1380 mode=anchormode)
1373 intercepted = True
1381 intercepted = True
1374
1382
1375 elif key == QtCore.Qt.Key_Home:
1383 elif key == QtCore.Qt.Key_Home:
1376 start_line = cursor.blockNumber()
1384 start_line = cursor.blockNumber()
1377 if start_line == self._get_prompt_cursor().blockNumber():
1385 if start_line == self._get_prompt_cursor().blockNumber():
1378 start_pos = self._prompt_pos
1386 start_pos = self._prompt_pos
1379 else:
1387 else:
1380 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1388 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1381 QtGui.QTextCursor.KeepAnchor)
1389 QtGui.QTextCursor.KeepAnchor)
1382 start_pos = cursor.position()
1390 start_pos = cursor.position()
1383 start_pos += len(self._continuation_prompt)
1391 start_pos += len(self._continuation_prompt)
1384 cursor.setPosition(position)
1392 cursor.setPosition(position)
1385 if shift_down and self._in_buffer(position):
1393 if shift_down and self._in_buffer(position):
1386 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1394 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1387 else:
1395 else:
1388 cursor.setPosition(start_pos)
1396 cursor.setPosition(start_pos)
1389 self._set_cursor(cursor)
1397 self._set_cursor(cursor)
1390 intercepted = True
1398 intercepted = True
1391
1399
1392 elif key == QtCore.Qt.Key_Backspace:
1400 elif key == QtCore.Qt.Key_Backspace:
1393
1401
1394 # Line deletion (remove continuation prompt)
1402 # Line deletion (remove continuation prompt)
1395 line, col = cursor.blockNumber(), cursor.columnNumber()
1403 line, col = cursor.blockNumber(), cursor.columnNumber()
1396 if not self._reading and \
1404 if not self._reading and \
1397 col == len(self._continuation_prompt) and \
1405 col == len(self._continuation_prompt) and \
1398 line > self._get_prompt_cursor().blockNumber():
1406 line > self._get_prompt_cursor().blockNumber():
1399 cursor.beginEditBlock()
1407 cursor.beginEditBlock()
1400 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1408 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1401 QtGui.QTextCursor.KeepAnchor)
1409 QtGui.QTextCursor.KeepAnchor)
1402 cursor.removeSelectedText()
1410 cursor.removeSelectedText()
1403 cursor.deletePreviousChar()
1411 cursor.deletePreviousChar()
1404 cursor.endEditBlock()
1412 cursor.endEditBlock()
1405 intercepted = True
1413 intercepted = True
1406
1414
1407 # Regular backwards deletion
1415 # Regular backwards deletion
1408 else:
1416 else:
1409 anchor = cursor.anchor()
1417 anchor = cursor.anchor()
1410 if anchor == position:
1418 if anchor == position:
1411 intercepted = not self._in_buffer(position - 1)
1419 intercepted = not self._in_buffer(position - 1)
1412 else:
1420 else:
1413 intercepted = not self._in_buffer(min(anchor, position))
1421 intercepted = not self._in_buffer(min(anchor, position))
1414
1422
1415 elif key == QtCore.Qt.Key_Delete:
1423 elif key == QtCore.Qt.Key_Delete:
1416
1424
1417 # Line deletion (remove continuation prompt)
1425 # Line deletion (remove continuation prompt)
1418 if not self._reading and self._in_buffer(position) and \
1426 if not self._reading and self._in_buffer(position) and \
1419 cursor.atBlockEnd() and not cursor.hasSelection():
1427 cursor.atBlockEnd() and not cursor.hasSelection():
1420 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1428 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1421 QtGui.QTextCursor.KeepAnchor)
1429 QtGui.QTextCursor.KeepAnchor)
1422 cursor.movePosition(QtGui.QTextCursor.Right,
1430 cursor.movePosition(QtGui.QTextCursor.Right,
1423 QtGui.QTextCursor.KeepAnchor,
1431 QtGui.QTextCursor.KeepAnchor,
1424 len(self._continuation_prompt))
1432 len(self._continuation_prompt))
1425 cursor.removeSelectedText()
1433 cursor.removeSelectedText()
1426 intercepted = True
1434 intercepted = True
1427
1435
1428 # Regular forwards deletion:
1436 # Regular forwards deletion:
1429 else:
1437 else:
1430 anchor = cursor.anchor()
1438 anchor = cursor.anchor()
1431 intercepted = (not self._in_buffer(anchor) or
1439 intercepted = (not self._in_buffer(anchor) or
1432 not self._in_buffer(position))
1440 not self._in_buffer(position))
1433
1441
1434 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1442 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1435 # using the keyboard in any part of the buffer. Also, permit scrolling
1443 # using the keyboard in any part of the buffer. Also, permit scrolling
1436 # with Page Up/Down keys. Finally, if we're executing, don't move the
1444 # with Page Up/Down keys. Finally, if we're executing, don't move the
1437 # cursor (if even this made sense, we can't guarantee that the prompt
1445 # cursor (if even this made sense, we can't guarantee that the prompt
1438 # position is still valid due to text truncation).
1446 # position is still valid due to text truncation).
1439 if not (self._control_key_down(event.modifiers(), include_command=True)
1447 if not (self._control_key_down(event.modifiers(), include_command=True)
1440 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1448 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1441 or (self._executing and not self._reading)):
1449 or (self._executing and not self._reading)):
1442 self._keep_cursor_in_buffer()
1450 self._keep_cursor_in_buffer()
1443
1451
1444 return intercepted
1452 return intercepted
1445
1453
1446 def _event_filter_page_keypress(self, event):
1454 def _event_filter_page_keypress(self, event):
1447 """ Filter key events for the paging widget to create console-like
1455 """ Filter key events for the paging widget to create console-like
1448 interface.
1456 interface.
1449 """
1457 """
1450 key = event.key()
1458 key = event.key()
1451 ctrl_down = self._control_key_down(event.modifiers())
1459 ctrl_down = self._control_key_down(event.modifiers())
1452 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1460 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1453
1461
1454 if ctrl_down:
1462 if ctrl_down:
1455 if key == QtCore.Qt.Key_O:
1463 if key == QtCore.Qt.Key_O:
1456 self._control.setFocus()
1464 self._control.setFocus()
1457 intercept = True
1465 intercept = True
1458
1466
1459 elif alt_down:
1467 elif alt_down:
1460 if key == QtCore.Qt.Key_Greater:
1468 if key == QtCore.Qt.Key_Greater:
1461 self._page_control.moveCursor(QtGui.QTextCursor.End)
1469 self._page_control.moveCursor(QtGui.QTextCursor.End)
1462 intercepted = True
1470 intercepted = True
1463
1471
1464 elif key == QtCore.Qt.Key_Less:
1472 elif key == QtCore.Qt.Key_Less:
1465 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1473 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1466 intercepted = True
1474 intercepted = True
1467
1475
1468 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1476 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1469 if self._splitter:
1477 if self._splitter:
1470 self._page_control.hide()
1478 self._page_control.hide()
1471 self._control.setFocus()
1479 self._control.setFocus()
1472 else:
1480 else:
1473 self.layout().setCurrentWidget(self._control)
1481 self.layout().setCurrentWidget(self._control)
1474 # re-enable buffer truncation after paging
1482 # re-enable buffer truncation after paging
1475 self._control.document().setMaximumBlockCount(self.buffer_size)
1483 self._control.document().setMaximumBlockCount(self.buffer_size)
1476 return True
1484 return True
1477
1485
1478 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1486 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1479 QtCore.Qt.Key_Tab):
1487 QtCore.Qt.Key_Tab):
1480 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1488 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1481 QtCore.Qt.Key_PageDown,
1489 QtCore.Qt.Key_PageDown,
1482 QtCore.Qt.NoModifier)
1490 QtCore.Qt.NoModifier)
1483 QtGui.qApp.sendEvent(self._page_control, new_event)
1491 QtGui.qApp.sendEvent(self._page_control, new_event)
1484 return True
1492 return True
1485
1493
1486 elif key == QtCore.Qt.Key_Backspace:
1494 elif key == QtCore.Qt.Key_Backspace:
1487 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1495 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1488 QtCore.Qt.Key_PageUp,
1496 QtCore.Qt.Key_PageUp,
1489 QtCore.Qt.NoModifier)
1497 QtCore.Qt.NoModifier)
1490 QtGui.qApp.sendEvent(self._page_control, new_event)
1498 QtGui.qApp.sendEvent(self._page_control, new_event)
1491 return True
1499 return True
1492
1500
1493 # vi/less -like key bindings
1501 # vi/less -like key bindings
1494 elif key == QtCore.Qt.Key_J:
1502 elif key == QtCore.Qt.Key_J:
1495 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1503 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1496 QtCore.Qt.Key_Down,
1504 QtCore.Qt.Key_Down,
1497 QtCore.Qt.NoModifier)
1505 QtCore.Qt.NoModifier)
1498 QtGui.qApp.sendEvent(self._page_control, new_event)
1506 QtGui.qApp.sendEvent(self._page_control, new_event)
1499 return True
1507 return True
1500
1508
1501 # vi/less -like key bindings
1509 # vi/less -like key bindings
1502 elif key == QtCore.Qt.Key_K:
1510 elif key == QtCore.Qt.Key_K:
1503 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1511 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1504 QtCore.Qt.Key_Up,
1512 QtCore.Qt.Key_Up,
1505 QtCore.Qt.NoModifier)
1513 QtCore.Qt.NoModifier)
1506 QtGui.qApp.sendEvent(self._page_control, new_event)
1514 QtGui.qApp.sendEvent(self._page_control, new_event)
1507 return True
1515 return True
1508
1516
1509 return False
1517 return False
1510
1518
1511 def _on_flush_pending_stream_timer(self):
1519 def _on_flush_pending_stream_timer(self):
1512 """ Flush the pending stream output and change the
1520 """ Flush the pending stream output and change the
1513 prompt position appropriately.
1521 prompt position appropriately.
1514 """
1522 """
1515 cursor = self._control.textCursor()
1523 cursor = self._control.textCursor()
1516 cursor.movePosition(QtGui.QTextCursor.End)
1524 cursor.movePosition(QtGui.QTextCursor.End)
1517 pos = cursor.position()
1525 pos = cursor.position()
1518 self._flush_pending_stream()
1526 self._flush_pending_stream()
1519 cursor.movePosition(QtGui.QTextCursor.End)
1527 cursor.movePosition(QtGui.QTextCursor.End)
1520 diff = cursor.position() - pos
1528 diff = cursor.position() - pos
1521 if diff > 0:
1529 if diff > 0:
1522 self._prompt_pos += diff
1530 self._prompt_pos += diff
1523 self._append_before_prompt_pos += diff
1531 self._append_before_prompt_pos += diff
1524
1532
1525 def _flush_pending_stream(self):
1533 def _flush_pending_stream(self):
1526 """ Flush out pending text into the widget. """
1534 """ Flush out pending text into the widget. """
1527 text = self._pending_insert_text
1535 text = self._pending_insert_text
1528 self._pending_insert_text = []
1536 self._pending_insert_text = []
1529 buffer_size = self._control.document().maximumBlockCount()
1537 buffer_size = self._control.document().maximumBlockCount()
1530 if buffer_size > 0:
1538 if buffer_size > 0:
1531 text = self._get_last_lines_from_list(text, buffer_size)
1539 text = self._get_last_lines_from_list(text, buffer_size)
1532 text = ''.join(text)
1540 text = ''.join(text)
1533 t = time.time()
1541 t = time.time()
1534 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1542 self._insert_plain_text(self._get_end_cursor(), text, flush=True)
1535 # Set the flush interval to equal the maximum time to update text.
1543 # Set the flush interval to equal the maximum time to update text.
1536 self._pending_text_flush_interval.setInterval(max(100,
1544 self._pending_text_flush_interval.setInterval(max(100,
1537 (time.time()-t)*1000))
1545 (time.time()-t)*1000))
1538
1546
1539 def _format_as_columns(self, items, separator=' '):
1547 def _format_as_columns(self, items, separator=' '):
1540 """ Transform a list of strings into a single string with columns.
1548 """ Transform a list of strings into a single string with columns.
1541
1549
1542 Parameters
1550 Parameters
1543 ----------
1551 ----------
1544 items : sequence of strings
1552 items : sequence of strings
1545 The strings to process.
1553 The strings to process.
1546
1554
1547 separator : str, optional [default is two spaces]
1555 separator : str, optional [default is two spaces]
1548 The string that separates columns.
1556 The string that separates columns.
1549
1557
1550 Returns
1558 Returns
1551 -------
1559 -------
1552 The formatted string.
1560 The formatted string.
1553 """
1561 """
1554 # Calculate the number of characters available.
1562 # Calculate the number of characters available.
1555 width = self._control.viewport().width()
1563 width = self._control.viewport().width()
1556 char_width = QtGui.QFontMetrics(self.font).width(' ')
1564 char_width = QtGui.QFontMetrics(self.font).width(' ')
1557 displaywidth = max(10, (width / char_width) - 1)
1565 displaywidth = max(10, (width / char_width) - 1)
1558
1566
1559 return columnize(items, separator, displaywidth)
1567 return columnize(items, separator, displaywidth)
1560
1568
1561 def _get_block_plain_text(self, block):
1569 def _get_block_plain_text(self, block):
1562 """ Given a QTextBlock, return its unformatted text.
1570 """ Given a QTextBlock, return its unformatted text.
1563 """
1571 """
1564 cursor = QtGui.QTextCursor(block)
1572 cursor = QtGui.QTextCursor(block)
1565 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1573 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1566 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1574 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1567 QtGui.QTextCursor.KeepAnchor)
1575 QtGui.QTextCursor.KeepAnchor)
1568 return cursor.selection().toPlainText()
1576 return cursor.selection().toPlainText()
1569
1577
1570 def _get_cursor(self):
1578 def _get_cursor(self):
1571 """ Convenience method that returns a cursor for the current position.
1579 """ Convenience method that returns a cursor for the current position.
1572 """
1580 """
1573 return self._control.textCursor()
1581 return self._control.textCursor()
1574
1582
1575 def _get_end_cursor(self):
1583 def _get_end_cursor(self):
1576 """ Convenience method that returns a cursor for the last character.
1584 """ Convenience method that returns a cursor for the last character.
1577 """
1585 """
1578 cursor = self._control.textCursor()
1586 cursor = self._control.textCursor()
1579 cursor.movePosition(QtGui.QTextCursor.End)
1587 cursor.movePosition(QtGui.QTextCursor.End)
1580 return cursor
1588 return cursor
1581
1589
1582 def _get_input_buffer_cursor_column(self):
1590 def _get_input_buffer_cursor_column(self):
1583 """ Returns the column of the cursor in the input buffer, excluding the
1591 """ Returns the column of the cursor in the input buffer, excluding the
1584 contribution by the prompt, or -1 if there is no such column.
1592 contribution by the prompt, or -1 if there is no such column.
1585 """
1593 """
1586 prompt = self._get_input_buffer_cursor_prompt()
1594 prompt = self._get_input_buffer_cursor_prompt()
1587 if prompt is None:
1595 if prompt is None:
1588 return -1
1596 return -1
1589 else:
1597 else:
1590 cursor = self._control.textCursor()
1598 cursor = self._control.textCursor()
1591 return cursor.columnNumber() - len(prompt)
1599 return cursor.columnNumber() - len(prompt)
1592
1600
1593 def _get_input_buffer_cursor_line(self):
1601 def _get_input_buffer_cursor_line(self):
1594 """ Returns the text of the line of the input buffer that contains the
1602 """ Returns the text of the line of the input buffer that contains the
1595 cursor, or None if there is no such line.
1603 cursor, or None if there is no such line.
1596 """
1604 """
1597 prompt = self._get_input_buffer_cursor_prompt()
1605 prompt = self._get_input_buffer_cursor_prompt()
1598 if prompt is None:
1606 if prompt is None:
1599 return None
1607 return None
1600 else:
1608 else:
1601 cursor = self._control.textCursor()
1609 cursor = self._control.textCursor()
1602 text = self._get_block_plain_text(cursor.block())
1610 text = self._get_block_plain_text(cursor.block())
1603 return text[len(prompt):]
1611 return text[len(prompt):]
1604
1612
1605 def _get_input_buffer_cursor_pos(self):
1613 def _get_input_buffer_cursor_pos(self):
1606 """Return the cursor position within the input buffer."""
1614 """Return the cursor position within the input buffer."""
1607 cursor = self._control.textCursor()
1615 cursor = self._control.textCursor()
1608 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1616 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
1609 input_buffer = cursor.selection().toPlainText()
1617 input_buffer = cursor.selection().toPlainText()
1610
1618
1611 # Don't count continuation prompts
1619 # Don't count continuation prompts
1612 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1620 return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
1613
1621
1614 def _get_input_buffer_cursor_prompt(self):
1622 def _get_input_buffer_cursor_prompt(self):
1615 """ Returns the (plain text) prompt for line of the input buffer that
1623 """ Returns the (plain text) prompt for line of the input buffer that
1616 contains the cursor, or None if there is no such line.
1624 contains the cursor, or None if there is no such line.
1617 """
1625 """
1618 if self._executing:
1626 if self._executing:
1619 return None
1627 return None
1620 cursor = self._control.textCursor()
1628 cursor = self._control.textCursor()
1621 if cursor.position() >= self._prompt_pos:
1629 if cursor.position() >= self._prompt_pos:
1622 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1630 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1623 return self._prompt
1631 return self._prompt
1624 else:
1632 else:
1625 return self._continuation_prompt
1633 return self._continuation_prompt
1626 else:
1634 else:
1627 return None
1635 return None
1628
1636
1629 def _get_last_lines(self, text, num_lines, return_count=False):
1637 def _get_last_lines(self, text, num_lines, return_count=False):
1630 """ Return last specified number of lines of text (like `tail -n`).
1638 """ Return last specified number of lines of text (like `tail -n`).
1631 If return_count is True, returns a tuple of clipped text and the
1639 If return_count is True, returns a tuple of clipped text and the
1632 number of lines in the clipped text.
1640 number of lines in the clipped text.
1633 """
1641 """
1634 pos = len(text)
1642 pos = len(text)
1635 if pos < num_lines:
1643 if pos < num_lines:
1636 if return_count:
1644 if return_count:
1637 return text, text.count('\n') if return_count else text
1645 return text, text.count('\n') if return_count else text
1638 else:
1646 else:
1639 return text
1647 return text
1640 i = 0
1648 i = 0
1641 while i < num_lines:
1649 while i < num_lines:
1642 pos = text.rfind('\n', None, pos)
1650 pos = text.rfind('\n', None, pos)
1643 if pos == -1:
1651 if pos == -1:
1644 pos = None
1652 pos = None
1645 break
1653 break
1646 i += 1
1654 i += 1
1647 if return_count:
1655 if return_count:
1648 return text[pos:], i
1656 return text[pos:], i
1649 else:
1657 else:
1650 return text[pos:]
1658 return text[pos:]
1651
1659
1652 def _get_last_lines_from_list(self, text_list, num_lines):
1660 def _get_last_lines_from_list(self, text_list, num_lines):
1653 """ Return the list of text clipped to last specified lines.
1661 """ Return the list of text clipped to last specified lines.
1654 """
1662 """
1655 ret = []
1663 ret = []
1656 lines_pending = num_lines
1664 lines_pending = num_lines
1657 for text in reversed(text_list):
1665 for text in reversed(text_list):
1658 text, lines_added = self._get_last_lines(text, lines_pending,
1666 text, lines_added = self._get_last_lines(text, lines_pending,
1659 return_count=True)
1667 return_count=True)
1660 ret.append(text)
1668 ret.append(text)
1661 lines_pending -= lines_added
1669 lines_pending -= lines_added
1662 if lines_pending <= 0:
1670 if lines_pending <= 0:
1663 break
1671 break
1664 return ret[::-1]
1672 return ret[::-1]
1665
1673
1666 def _get_prompt_cursor(self):
1674 def _get_prompt_cursor(self):
1667 """ Convenience method that returns a cursor for the prompt position.
1675 """ Convenience method that returns a cursor for the prompt position.
1668 """
1676 """
1669 cursor = self._control.textCursor()
1677 cursor = self._control.textCursor()
1670 cursor.setPosition(self._prompt_pos)
1678 cursor.setPosition(self._prompt_pos)
1671 return cursor
1679 return cursor
1672
1680
1673 def _get_selection_cursor(self, start, end):
1681 def _get_selection_cursor(self, start, end):
1674 """ Convenience method that returns a cursor with text selected between
1682 """ Convenience method that returns a cursor with text selected between
1675 the positions 'start' and 'end'.
1683 the positions 'start' and 'end'.
1676 """
1684 """
1677 cursor = self._control.textCursor()
1685 cursor = self._control.textCursor()
1678 cursor.setPosition(start)
1686 cursor.setPosition(start)
1679 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1687 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1680 return cursor
1688 return cursor
1681
1689
1682 def _get_word_start_cursor(self, position):
1690 def _get_word_start_cursor(self, position):
1683 """ Find the start of the word to the left the given position. If a
1691 """ Find the start of the word to the left the given position. If a
1684 sequence of non-word characters precedes the first word, skip over
1692 sequence of non-word characters precedes the first word, skip over
1685 them. (This emulates the behavior of bash, emacs, etc.)
1693 them. (This emulates the behavior of bash, emacs, etc.)
1686 """
1694 """
1687 document = self._control.document()
1695 document = self._control.document()
1688 position -= 1
1696 position -= 1
1689 while position >= self._prompt_pos and \
1697 while position >= self._prompt_pos and \
1690 not is_letter_or_number(document.characterAt(position)):
1698 not is_letter_or_number(document.characterAt(position)):
1691 position -= 1
1699 position -= 1
1692 while position >= self._prompt_pos and \
1700 while position >= self._prompt_pos and \
1693 is_letter_or_number(document.characterAt(position)):
1701 is_letter_or_number(document.characterAt(position)):
1694 position -= 1
1702 position -= 1
1695 cursor = self._control.textCursor()
1703 cursor = self._control.textCursor()
1696 cursor.setPosition(position + 1)
1704 cursor.setPosition(position + 1)
1697 return cursor
1705 return cursor
1698
1706
1699 def _get_word_end_cursor(self, position):
1707 def _get_word_end_cursor(self, position):
1700 """ Find the end of the word to the right the given position. If a
1708 """ Find the end of the word to the right the given position. If a
1701 sequence of non-word characters precedes the first word, skip over
1709 sequence of non-word characters precedes the first word, skip over
1702 them. (This emulates the behavior of bash, emacs, etc.)
1710 them. (This emulates the behavior of bash, emacs, etc.)
1703 """
1711 """
1704 document = self._control.document()
1712 document = self._control.document()
1705 end = self._get_end_cursor().position()
1713 end = self._get_end_cursor().position()
1706 while position < end and \
1714 while position < end and \
1707 not is_letter_or_number(document.characterAt(position)):
1715 not is_letter_or_number(document.characterAt(position)):
1708 position += 1
1716 position += 1
1709 while position < end and \
1717 while position < end and \
1710 is_letter_or_number(document.characterAt(position)):
1718 is_letter_or_number(document.characterAt(position)):
1711 position += 1
1719 position += 1
1712 cursor = self._control.textCursor()
1720 cursor = self._control.textCursor()
1713 cursor.setPosition(position)
1721 cursor.setPosition(position)
1714 return cursor
1722 return cursor
1715
1723
1716 def _insert_continuation_prompt(self, cursor):
1724 def _insert_continuation_prompt(self, cursor):
1717 """ Inserts new continuation prompt using the specified cursor.
1725 """ Inserts new continuation prompt using the specified cursor.
1718 """
1726 """
1719 if self._continuation_prompt_html is None:
1727 if self._continuation_prompt_html is None:
1720 self._insert_plain_text(cursor, self._continuation_prompt)
1728 self._insert_plain_text(cursor, self._continuation_prompt)
1721 else:
1729 else:
1722 self._continuation_prompt = self._insert_html_fetching_plain_text(
1730 self._continuation_prompt = self._insert_html_fetching_plain_text(
1723 cursor, self._continuation_prompt_html)
1731 cursor, self._continuation_prompt_html)
1724
1732
1725 def _insert_block(self, cursor, block_format=None):
1733 def _insert_block(self, cursor, block_format=None):
1726 """ Inserts an empty QTextBlock using the specified cursor.
1734 """ Inserts an empty QTextBlock using the specified cursor.
1727 """
1735 """
1728 if block_format is None:
1736 if block_format is None:
1729 block_format = QtGui.QTextBlockFormat()
1737 block_format = QtGui.QTextBlockFormat()
1730 cursor.insertBlock(block_format)
1738 cursor.insertBlock(block_format)
1731
1739
1732 def _insert_html(self, cursor, html):
1740 def _insert_html(self, cursor, html):
1733 """ Inserts HTML using the specified cursor in such a way that future
1741 """ Inserts HTML using the specified cursor in such a way that future
1734 formatting is unaffected.
1742 formatting is unaffected.
1735 """
1743 """
1736 cursor.beginEditBlock()
1744 cursor.beginEditBlock()
1737 cursor.insertHtml(html)
1745 cursor.insertHtml(html)
1738
1746
1739 # After inserting HTML, the text document "remembers" it's in "html
1747 # After inserting HTML, the text document "remembers" it's in "html
1740 # mode", which means that subsequent calls adding plain text will result
1748 # mode", which means that subsequent calls adding plain text will result
1741 # in unwanted formatting, lost tab characters, etc. The following code
1749 # in unwanted formatting, lost tab characters, etc. The following code
1742 # hacks around this behavior, which I consider to be a bug in Qt, by
1750 # hacks around this behavior, which I consider to be a bug in Qt, by
1743 # (crudely) resetting the document's style state.
1751 # (crudely) resetting the document's style state.
1744 cursor.movePosition(QtGui.QTextCursor.Left,
1752 cursor.movePosition(QtGui.QTextCursor.Left,
1745 QtGui.QTextCursor.KeepAnchor)
1753 QtGui.QTextCursor.KeepAnchor)
1746 if cursor.selection().toPlainText() == ' ':
1754 if cursor.selection().toPlainText() == ' ':
1747 cursor.removeSelectedText()
1755 cursor.removeSelectedText()
1748 else:
1756 else:
1749 cursor.movePosition(QtGui.QTextCursor.Right)
1757 cursor.movePosition(QtGui.QTextCursor.Right)
1750 cursor.insertText(' ', QtGui.QTextCharFormat())
1758 cursor.insertText(' ', QtGui.QTextCharFormat())
1751 cursor.endEditBlock()
1759 cursor.endEditBlock()
1752
1760
1753 def _insert_html_fetching_plain_text(self, cursor, html):
1761 def _insert_html_fetching_plain_text(self, cursor, html):
1754 """ Inserts HTML using the specified cursor, then returns its plain text
1762 """ Inserts HTML using the specified cursor, then returns its plain text
1755 version.
1763 version.
1756 """
1764 """
1757 cursor.beginEditBlock()
1765 cursor.beginEditBlock()
1758 cursor.removeSelectedText()
1766 cursor.removeSelectedText()
1759
1767
1760 start = cursor.position()
1768 start = cursor.position()
1761 self._insert_html(cursor, html)
1769 self._insert_html(cursor, html)
1762 end = cursor.position()
1770 end = cursor.position()
1763 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1771 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1764 text = cursor.selection().toPlainText()
1772 text = cursor.selection().toPlainText()
1765
1773
1766 cursor.setPosition(end)
1774 cursor.setPosition(end)
1767 cursor.endEditBlock()
1775 cursor.endEditBlock()
1768 return text
1776 return text
1769
1777
1770 def _insert_plain_text(self, cursor, text, flush=False):
1778 def _insert_plain_text(self, cursor, text, flush=False):
1771 """ Inserts plain text using the specified cursor, processing ANSI codes
1779 """ Inserts plain text using the specified cursor, processing ANSI codes
1772 if enabled.
1780 if enabled.
1773 """
1781 """
1774 # maximumBlockCount() can be different from self.buffer_size in
1782 # maximumBlockCount() can be different from self.buffer_size in
1775 # case input prompt is active.
1783 # case input prompt is active.
1776 buffer_size = self._control.document().maximumBlockCount()
1784 buffer_size = self._control.document().maximumBlockCount()
1777
1785
1778 if (self._executing and not flush and
1786 if (self._executing and not flush and
1779 self._pending_text_flush_interval.isActive() and
1787 self._pending_text_flush_interval.isActive() and
1780 cursor.position() == self._get_end_cursor().position()):
1788 cursor.position() == self._get_end_cursor().position()):
1781 # Queue the text to insert in case it is being inserted at end
1789 # Queue the text to insert in case it is being inserted at end
1782 self._pending_insert_text.append(text)
1790 self._pending_insert_text.append(text)
1783 if buffer_size > 0:
1791 if buffer_size > 0:
1784 self._pending_insert_text = self._get_last_lines_from_list(
1792 self._pending_insert_text = self._get_last_lines_from_list(
1785 self._pending_insert_text, buffer_size)
1793 self._pending_insert_text, buffer_size)
1786 return
1794 return
1787
1795
1788 if self._executing and not self._pending_text_flush_interval.isActive():
1796 if self._executing and not self._pending_text_flush_interval.isActive():
1789 self._pending_text_flush_interval.start()
1797 self._pending_text_flush_interval.start()
1790
1798
1791 # Clip the text to last `buffer_size` lines.
1799 # Clip the text to last `buffer_size` lines.
1792 if buffer_size > 0:
1800 if buffer_size > 0:
1793 text = self._get_last_lines(text, buffer_size)
1801 text = self._get_last_lines(text, buffer_size)
1794
1802
1795 cursor.beginEditBlock()
1803 cursor.beginEditBlock()
1796 if self.ansi_codes:
1804 if self.ansi_codes:
1797 for substring in self._ansi_processor.split_string(text):
1805 for substring in self._ansi_processor.split_string(text):
1798 for act in self._ansi_processor.actions:
1806 for act in self._ansi_processor.actions:
1799
1807
1800 # Unlike real terminal emulators, we don't distinguish
1808 # Unlike real terminal emulators, we don't distinguish
1801 # between the screen and the scrollback buffer. A screen
1809 # between the screen and the scrollback buffer. A screen
1802 # erase request clears everything.
1810 # erase request clears everything.
1803 if act.action == 'erase' and act.area == 'screen':
1811 if act.action == 'erase' and act.area == 'screen':
1804 cursor.select(QtGui.QTextCursor.Document)
1812 cursor.select(QtGui.QTextCursor.Document)
1805 cursor.removeSelectedText()
1813 cursor.removeSelectedText()
1806
1814
1807 # Simulate a form feed by scrolling just past the last line.
1815 # Simulate a form feed by scrolling just past the last line.
1808 elif act.action == 'scroll' and act.unit == 'page':
1816 elif act.action == 'scroll' and act.unit == 'page':
1809 cursor.insertText('\n')
1817 cursor.insertText('\n')
1810 cursor.endEditBlock()
1818 cursor.endEditBlock()
1811 self._set_top_cursor(cursor)
1819 self._set_top_cursor(cursor)
1812 cursor.joinPreviousEditBlock()
1820 cursor.joinPreviousEditBlock()
1813 cursor.deletePreviousChar()
1821 cursor.deletePreviousChar()
1814
1822
1815 elif act.action == 'carriage-return':
1823 elif act.action == 'carriage-return':
1816 cursor.movePosition(
1824 cursor.movePosition(
1817 cursor.StartOfLine, cursor.KeepAnchor)
1825 cursor.StartOfLine, cursor.KeepAnchor)
1818
1826
1819 elif act.action == 'beep':
1827 elif act.action == 'beep':
1820 QtGui.qApp.beep()
1828 QtGui.qApp.beep()
1821
1829
1822 elif act.action == 'backspace':
1830 elif act.action == 'backspace':
1823 if not cursor.atBlockStart():
1831 if not cursor.atBlockStart():
1824 cursor.movePosition(
1832 cursor.movePosition(
1825 cursor.PreviousCharacter, cursor.KeepAnchor)
1833 cursor.PreviousCharacter, cursor.KeepAnchor)
1826
1834
1827 elif act.action == 'newline':
1835 elif act.action == 'newline':
1828 cursor.movePosition(cursor.EndOfLine)
1836 cursor.movePosition(cursor.EndOfLine)
1829
1837
1830 format = self._ansi_processor.get_format()
1838 format = self._ansi_processor.get_format()
1831
1839
1832 selection = cursor.selectedText()
1840 selection = cursor.selectedText()
1833 if len(selection) == 0:
1841 if len(selection) == 0:
1834 cursor.insertText(substring, format)
1842 cursor.insertText(substring, format)
1835 elif substring is not None:
1843 elif substring is not None:
1836 # BS and CR are treated as a change in print
1844 # BS and CR are treated as a change in print
1837 # position, rather than a backwards character
1845 # position, rather than a backwards character
1838 # deletion for output equivalence with (I)Python
1846 # deletion for output equivalence with (I)Python
1839 # terminal.
1847 # terminal.
1840 if len(substring) >= len(selection):
1848 if len(substring) >= len(selection):
1841 cursor.insertText(substring, format)
1849 cursor.insertText(substring, format)
1842 else:
1850 else:
1843 old_text = selection[len(substring):]
1851 old_text = selection[len(substring):]
1844 cursor.insertText(substring + old_text, format)
1852 cursor.insertText(substring + old_text, format)
1845 cursor.movePosition(cursor.PreviousCharacter,
1853 cursor.movePosition(cursor.PreviousCharacter,
1846 cursor.KeepAnchor, len(old_text))
1854 cursor.KeepAnchor, len(old_text))
1847 else:
1855 else:
1848 cursor.insertText(text)
1856 cursor.insertText(text)
1849 cursor.endEditBlock()
1857 cursor.endEditBlock()
1850
1858
1851 def _insert_plain_text_into_buffer(self, cursor, text):
1859 def _insert_plain_text_into_buffer(self, cursor, text):
1852 """ Inserts text into the input buffer using the specified cursor (which
1860 """ Inserts text into the input buffer using the specified cursor (which
1853 must be in the input buffer), ensuring that continuation prompts are
1861 must be in the input buffer), ensuring that continuation prompts are
1854 inserted as necessary.
1862 inserted as necessary.
1855 """
1863 """
1856 lines = text.splitlines(True)
1864 lines = text.splitlines(True)
1857 if lines:
1865 if lines:
1858 cursor.beginEditBlock()
1866 cursor.beginEditBlock()
1859 cursor.insertText(lines[0])
1867 cursor.insertText(lines[0])
1860 for line in lines[1:]:
1868 for line in lines[1:]:
1861 if self._continuation_prompt_html is None:
1869 if self._continuation_prompt_html is None:
1862 cursor.insertText(self._continuation_prompt)
1870 cursor.insertText(self._continuation_prompt)
1863 else:
1871 else:
1864 self._continuation_prompt = \
1872 self._continuation_prompt = \
1865 self._insert_html_fetching_plain_text(
1873 self._insert_html_fetching_plain_text(
1866 cursor, self._continuation_prompt_html)
1874 cursor, self._continuation_prompt_html)
1867 cursor.insertText(line)
1875 cursor.insertText(line)
1868 cursor.endEditBlock()
1876 cursor.endEditBlock()
1869
1877
1870 def _in_buffer(self, position=None):
1878 def _in_buffer(self, position=None):
1871 """ Returns whether the current cursor (or, if specified, a position) is
1879 """ Returns whether the current cursor (or, if specified, a position) is
1872 inside the editing region.
1880 inside the editing region.
1873 """
1881 """
1874 cursor = self._control.textCursor()
1882 cursor = self._control.textCursor()
1875 if position is None:
1883 if position is None:
1876 position = cursor.position()
1884 position = cursor.position()
1877 else:
1885 else:
1878 cursor.setPosition(position)
1886 cursor.setPosition(position)
1879 line = cursor.blockNumber()
1887 line = cursor.blockNumber()
1880 prompt_line = self._get_prompt_cursor().blockNumber()
1888 prompt_line = self._get_prompt_cursor().blockNumber()
1881 if line == prompt_line:
1889 if line == prompt_line:
1882 return position >= self._prompt_pos
1890 return position >= self._prompt_pos
1883 elif line > prompt_line:
1891 elif line > prompt_line:
1884 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1892 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1885 prompt_pos = cursor.position() + len(self._continuation_prompt)
1893 prompt_pos = cursor.position() + len(self._continuation_prompt)
1886 return position >= prompt_pos
1894 return position >= prompt_pos
1887 return False
1895 return False
1888
1896
1889 def _keep_cursor_in_buffer(self):
1897 def _keep_cursor_in_buffer(self):
1890 """ Ensures that the cursor is inside the editing region. Returns
1898 """ Ensures that the cursor is inside the editing region. Returns
1891 whether the cursor was moved.
1899 whether the cursor was moved.
1892 """
1900 """
1893 moved = not self._in_buffer()
1901 moved = not self._in_buffer()
1894 if moved:
1902 if moved:
1895 cursor = self._control.textCursor()
1903 cursor = self._control.textCursor()
1896 cursor.movePosition(QtGui.QTextCursor.End)
1904 cursor.movePosition(QtGui.QTextCursor.End)
1897 self._control.setTextCursor(cursor)
1905 self._control.setTextCursor(cursor)
1898 return moved
1906 return moved
1899
1907
1900 def _keyboard_quit(self):
1908 def _keyboard_quit(self):
1901 """ Cancels the current editing task ala Ctrl-G in Emacs.
1909 """ Cancels the current editing task ala Ctrl-G in Emacs.
1902 """
1910 """
1903 if self._temp_buffer_filled :
1911 if self._temp_buffer_filled :
1904 self._cancel_completion()
1912 self._cancel_completion()
1905 self._clear_temporary_buffer()
1913 self._clear_temporary_buffer()
1906 else:
1914 else:
1907 self.input_buffer = ''
1915 self.input_buffer = ''
1908
1916
1909 def _page(self, text, html=False):
1917 def _page(self, text, html=False):
1910 """ Displays text using the pager if it exceeds the height of the
1918 """ Displays text using the pager if it exceeds the height of the
1911 viewport.
1919 viewport.
1912
1920
1913 Parameters
1921 Parameters
1914 ----------
1922 ----------
1915 html : bool, optional (default False)
1923 html : bool, optional (default False)
1916 If set, the text will be interpreted as HTML instead of plain text.
1924 If set, the text will be interpreted as HTML instead of plain text.
1917 """
1925 """
1918 line_height = QtGui.QFontMetrics(self.font).height()
1926 line_height = QtGui.QFontMetrics(self.font).height()
1919 minlines = self._control.viewport().height() / line_height
1927 minlines = self._control.viewport().height() / line_height
1920 if self.paging != 'none' and \
1928 if self.paging != 'none' and \
1921 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1929 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1922 if self.paging == 'custom':
1930 if self.paging == 'custom':
1923 self.custom_page_requested.emit(text)
1931 self.custom_page_requested.emit(text)
1924 else:
1932 else:
1925 # disable buffer truncation during paging
1933 # disable buffer truncation during paging
1926 self._control.document().setMaximumBlockCount(0)
1934 self._control.document().setMaximumBlockCount(0)
1927 self._page_control.clear()
1935 self._page_control.clear()
1928 cursor = self._page_control.textCursor()
1936 cursor = self._page_control.textCursor()
1929 if html:
1937 if html:
1930 self._insert_html(cursor, text)
1938 self._insert_html(cursor, text)
1931 else:
1939 else:
1932 self._insert_plain_text(cursor, text)
1940 self._insert_plain_text(cursor, text)
1933 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1941 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1934
1942
1935 self._page_control.viewport().resize(self._control.size())
1943 self._page_control.viewport().resize(self._control.size())
1936 if self._splitter:
1944 if self._splitter:
1937 self._page_control.show()
1945 self._page_control.show()
1938 self._page_control.setFocus()
1946 self._page_control.setFocus()
1939 else:
1947 else:
1940 self.layout().setCurrentWidget(self._page_control)
1948 self.layout().setCurrentWidget(self._page_control)
1941 elif html:
1949 elif html:
1942 self._append_html(text)
1950 self._append_html(text)
1943 else:
1951 else:
1944 self._append_plain_text(text)
1952 self._append_plain_text(text)
1945
1953
1946 def _set_paging(self, paging):
1954 def _set_paging(self, paging):
1947 """
1955 """
1948 Change the pager to `paging` style.
1956 Change the pager to `paging` style.
1949
1957
1950 Parameters
1958 Parameters
1951 ----------
1959 ----------
1952 paging : string
1960 paging : string
1953 Either "hsplit", "vsplit", or "inside"
1961 Either "hsplit", "vsplit", or "inside"
1954 """
1962 """
1955 if self._splitter is None:
1963 if self._splitter is None:
1956 raise NotImplementedError("""can only switch if --paging=hsplit or
1964 raise NotImplementedError("""can only switch if --paging=hsplit or
1957 --paging=vsplit is used.""")
1965 --paging=vsplit is used.""")
1958 if paging == 'hsplit':
1966 if paging == 'hsplit':
1959 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1967 self._splitter.setOrientation(QtCore.Qt.Horizontal)
1960 elif paging == 'vsplit':
1968 elif paging == 'vsplit':
1961 self._splitter.setOrientation(QtCore.Qt.Vertical)
1969 self._splitter.setOrientation(QtCore.Qt.Vertical)
1962 elif paging == 'inside':
1970 elif paging == 'inside':
1963 raise NotImplementedError("""switching to 'inside' paging not
1971 raise NotImplementedError("""switching to 'inside' paging not
1964 supported yet.""")
1972 supported yet.""")
1965 else:
1973 else:
1966 raise ValueError("unknown paging method '%s'" % paging)
1974 raise ValueError("unknown paging method '%s'" % paging)
1967 self.paging = paging
1975 self.paging = paging
1968
1976
1969 def _prompt_finished(self):
1977 def _prompt_finished(self):
1970 """ Called immediately after a prompt is finished, i.e. when some input
1978 """ Called immediately after a prompt is finished, i.e. when some input
1971 will be processed and a new prompt displayed.
1979 will be processed and a new prompt displayed.
1972 """
1980 """
1973 self._control.setReadOnly(True)
1981 self._control.setReadOnly(True)
1974 self._prompt_finished_hook()
1982 self._prompt_finished_hook()
1975
1983
1976 def _prompt_started(self):
1984 def _prompt_started(self):
1977 """ Called immediately after a new prompt is displayed.
1985 """ Called immediately after a new prompt is displayed.
1978 """
1986 """
1979 # Temporarily disable the maximum block count to permit undo/redo and
1987 # Temporarily disable the maximum block count to permit undo/redo and
1980 # to ensure that the prompt position does not change due to truncation.
1988 # to ensure that the prompt position does not change due to truncation.
1981 self._control.document().setMaximumBlockCount(0)
1989 self._control.document().setMaximumBlockCount(0)
1982 self._control.setUndoRedoEnabled(True)
1990 self._control.setUndoRedoEnabled(True)
1983
1991
1984 # Work around bug in QPlainTextEdit: input method is not re-enabled
1992 # Work around bug in QPlainTextEdit: input method is not re-enabled
1985 # when read-only is disabled.
1993 # when read-only is disabled.
1986 self._control.setReadOnly(False)
1994 self._control.setReadOnly(False)
1987 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1995 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1988
1996
1989 if not self._reading:
1997 if not self._reading:
1990 self._executing = False
1998 self._executing = False
1991 self._prompt_started_hook()
1999 self._prompt_started_hook()
1992
2000
1993 # If the input buffer has changed while executing, load it.
2001 # If the input buffer has changed while executing, load it.
1994 if self._input_buffer_pending:
2002 if self._input_buffer_pending:
1995 self.input_buffer = self._input_buffer_pending
2003 self.input_buffer = self._input_buffer_pending
1996 self._input_buffer_pending = ''
2004 self._input_buffer_pending = ''
1997
2005
1998 self._control.moveCursor(QtGui.QTextCursor.End)
2006 self._control.moveCursor(QtGui.QTextCursor.End)
1999
2007
2000 def _readline(self, prompt='', callback=None):
2008 def _readline(self, prompt='', callback=None):
2001 """ Reads one line of input from the user.
2009 """ Reads one line of input from the user.
2002
2010
2003 Parameters
2011 Parameters
2004 ----------
2012 ----------
2005 prompt : str, optional
2013 prompt : str, optional
2006 The prompt to print before reading the line.
2014 The prompt to print before reading the line.
2007
2015
2008 callback : callable, optional
2016 callback : callable, optional
2009 A callback to execute with the read line. If not specified, input is
2017 A callback to execute with the read line. If not specified, input is
2010 read *synchronously* and this method does not return until it has
2018 read *synchronously* and this method does not return until it has
2011 been read.
2019 been read.
2012
2020
2013 Returns
2021 Returns
2014 -------
2022 -------
2015 If a callback is specified, returns nothing. Otherwise, returns the
2023 If a callback is specified, returns nothing. Otherwise, returns the
2016 input string with the trailing newline stripped.
2024 input string with the trailing newline stripped.
2017 """
2025 """
2018 if self._reading:
2026 if self._reading:
2019 raise RuntimeError('Cannot read a line. Widget is already reading.')
2027 raise RuntimeError('Cannot read a line. Widget is already reading.')
2020
2028
2021 if not callback and not self.isVisible():
2029 if not callback and not self.isVisible():
2022 # If the user cannot see the widget, this function cannot return.
2030 # If the user cannot see the widget, this function cannot return.
2023 raise RuntimeError('Cannot synchronously read a line if the widget '
2031 raise RuntimeError('Cannot synchronously read a line if the widget '
2024 'is not visible!')
2032 'is not visible!')
2025
2033
2026 self._reading = True
2034 self._reading = True
2027 self._show_prompt(prompt, newline=False)
2035 self._show_prompt(prompt, newline=False)
2028
2036
2029 if callback is None:
2037 if callback is None:
2030 self._reading_callback = None
2038 self._reading_callback = None
2031 while self._reading:
2039 while self._reading:
2032 QtCore.QCoreApplication.processEvents()
2040 QtCore.QCoreApplication.processEvents()
2033 return self._get_input_buffer(force=True).rstrip('\n')
2041 return self._get_input_buffer(force=True).rstrip('\n')
2034
2042
2035 else:
2043 else:
2036 self._reading_callback = lambda: \
2044 self._reading_callback = lambda: \
2037 callback(self._get_input_buffer(force=True).rstrip('\n'))
2045 callback(self._get_input_buffer(force=True).rstrip('\n'))
2038
2046
2039 def _set_continuation_prompt(self, prompt, html=False):
2047 def _set_continuation_prompt(self, prompt, html=False):
2040 """ Sets the continuation prompt.
2048 """ Sets the continuation prompt.
2041
2049
2042 Parameters
2050 Parameters
2043 ----------
2051 ----------
2044 prompt : str
2052 prompt : str
2045 The prompt to show when more input is needed.
2053 The prompt to show when more input is needed.
2046
2054
2047 html : bool, optional (default False)
2055 html : bool, optional (default False)
2048 If set, the prompt will be inserted as formatted HTML. Otherwise,
2056 If set, the prompt will be inserted as formatted HTML. Otherwise,
2049 the prompt will be treated as plain text, though ANSI color codes
2057 the prompt will be treated as plain text, though ANSI color codes
2050 will be handled.
2058 will be handled.
2051 """
2059 """
2052 if html:
2060 if html:
2053 self._continuation_prompt_html = prompt
2061 self._continuation_prompt_html = prompt
2054 else:
2062 else:
2055 self._continuation_prompt = prompt
2063 self._continuation_prompt = prompt
2056 self._continuation_prompt_html = None
2064 self._continuation_prompt_html = None
2057
2065
2058 def _set_cursor(self, cursor):
2066 def _set_cursor(self, cursor):
2059 """ Convenience method to set the current cursor.
2067 """ Convenience method to set the current cursor.
2060 """
2068 """
2061 self._control.setTextCursor(cursor)
2069 self._control.setTextCursor(cursor)
2062
2070
2063 def _set_top_cursor(self, cursor):
2071 def _set_top_cursor(self, cursor):
2064 """ Scrolls the viewport so that the specified cursor is at the top.
2072 """ Scrolls the viewport so that the specified cursor is at the top.
2065 """
2073 """
2066 scrollbar = self._control.verticalScrollBar()
2074 scrollbar = self._control.verticalScrollBar()
2067 scrollbar.setValue(scrollbar.maximum())
2075 scrollbar.setValue(scrollbar.maximum())
2068 original_cursor = self._control.textCursor()
2076 original_cursor = self._control.textCursor()
2069 self._control.setTextCursor(cursor)
2077 self._control.setTextCursor(cursor)
2070 self._control.ensureCursorVisible()
2078 self._control.ensureCursorVisible()
2071 self._control.setTextCursor(original_cursor)
2079 self._control.setTextCursor(original_cursor)
2072
2080
2073 def _show_prompt(self, prompt=None, html=False, newline=True):
2081 def _show_prompt(self, prompt=None, html=False, newline=True):
2074 """ Writes a new prompt at the end of the buffer.
2082 """ Writes a new prompt at the end of the buffer.
2075
2083
2076 Parameters
2084 Parameters
2077 ----------
2085 ----------
2078 prompt : str, optional
2086 prompt : str, optional
2079 The prompt to show. If not specified, the previous prompt is used.
2087 The prompt to show. If not specified, the previous prompt is used.
2080
2088
2081 html : bool, optional (default False)
2089 html : bool, optional (default False)
2082 Only relevant when a prompt is specified. If set, the prompt will
2090 Only relevant when a prompt is specified. If set, the prompt will
2083 be inserted as formatted HTML. Otherwise, the prompt will be treated
2091 be inserted as formatted HTML. Otherwise, the prompt will be treated
2084 as plain text, though ANSI color codes will be handled.
2092 as plain text, though ANSI color codes will be handled.
2085
2093
2086 newline : bool, optional (default True)
2094 newline : bool, optional (default True)
2087 If set, a new line will be written before showing the prompt if
2095 If set, a new line will be written before showing the prompt if
2088 there is not already a newline at the end of the buffer.
2096 there is not already a newline at the end of the buffer.
2089 """
2097 """
2090 # Save the current end position to support _append*(before_prompt=True).
2098 # Save the current end position to support _append*(before_prompt=True).
2091 self._flush_pending_stream()
2099 self._flush_pending_stream()
2092 cursor = self._get_end_cursor()
2100 cursor = self._get_end_cursor()
2093 self._append_before_prompt_pos = cursor.position()
2101 self._append_before_prompt_pos = cursor.position()
2094
2102
2095 # Insert a preliminary newline, if necessary.
2103 # Insert a preliminary newline, if necessary.
2096 if newline and cursor.position() > 0:
2104 if newline and cursor.position() > 0:
2097 cursor.movePosition(QtGui.QTextCursor.Left,
2105 cursor.movePosition(QtGui.QTextCursor.Left,
2098 QtGui.QTextCursor.KeepAnchor)
2106 QtGui.QTextCursor.KeepAnchor)
2099 if cursor.selection().toPlainText() != '\n':
2107 if cursor.selection().toPlainText() != '\n':
2100 self._append_block()
2108 self._append_block()
2101 self._append_before_prompt_pos += 1
2109 self._append_before_prompt_pos += 1
2102
2110
2103 # Write the prompt.
2111 # Write the prompt.
2104 self._append_plain_text(self._prompt_sep)
2112 self._append_plain_text(self._prompt_sep)
2105 if prompt is None:
2113 if prompt is None:
2106 if self._prompt_html is None:
2114 if self._prompt_html is None:
2107 self._append_plain_text(self._prompt)
2115 self._append_plain_text(self._prompt)
2108 else:
2116 else:
2109 self._append_html(self._prompt_html)
2117 self._append_html(self._prompt_html)
2110 else:
2118 else:
2111 if html:
2119 if html:
2112 self._prompt = self._append_html_fetching_plain_text(prompt)
2120 self._prompt = self._append_html_fetching_plain_text(prompt)
2113 self._prompt_html = prompt
2121 self._prompt_html = prompt
2114 else:
2122 else:
2115 self._append_plain_text(prompt)
2123 self._append_plain_text(prompt)
2116 self._prompt = prompt
2124 self._prompt = prompt
2117 self._prompt_html = None
2125 self._prompt_html = None
2118
2126
2119 self._flush_pending_stream()
2127 self._flush_pending_stream()
2120 self._prompt_pos = self._get_end_cursor().position()
2128 self._prompt_pos = self._get_end_cursor().position()
2121 self._prompt_started()
2129 self._prompt_started()
2122
2130
2123 #------ Signal handlers ----------------------------------------------------
2131 #------ Signal handlers ----------------------------------------------------
2124
2132
2125 def _adjust_scrollbars(self):
2133 def _adjust_scrollbars(self):
2126 """ Expands the vertical scrollbar beyond the range set by Qt.
2134 """ Expands the vertical scrollbar beyond the range set by Qt.
2127 """
2135 """
2128 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2136 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2129 # and qtextedit.cpp.
2137 # and qtextedit.cpp.
2130 document = self._control.document()
2138 document = self._control.document()
2131 scrollbar = self._control.verticalScrollBar()
2139 scrollbar = self._control.verticalScrollBar()
2132 viewport_height = self._control.viewport().height()
2140 viewport_height = self._control.viewport().height()
2133 if isinstance(self._control, QtGui.QPlainTextEdit):
2141 if isinstance(self._control, QtGui.QPlainTextEdit):
2134 maximum = max(0, document.lineCount() - 1)
2142 maximum = max(0, document.lineCount() - 1)
2135 step = viewport_height / self._control.fontMetrics().lineSpacing()
2143 step = viewport_height / self._control.fontMetrics().lineSpacing()
2136 else:
2144 else:
2137 # QTextEdit does not do line-based layout and blocks will not in
2145 # QTextEdit does not do line-based layout and blocks will not in
2138 # general have the same height. Therefore it does not make sense to
2146 # general have the same height. Therefore it does not make sense to
2139 # attempt to scroll in line height increments.
2147 # attempt to scroll in line height increments.
2140 maximum = document.size().height()
2148 maximum = document.size().height()
2141 step = viewport_height
2149 step = viewport_height
2142 diff = maximum - scrollbar.maximum()
2150 diff = maximum - scrollbar.maximum()
2143 scrollbar.setRange(0, maximum)
2151 scrollbar.setRange(0, maximum)
2144 scrollbar.setPageStep(step)
2152 scrollbar.setPageStep(step)
2145
2153
2146 # Compensate for undesirable scrolling that occurs automatically due to
2154 # Compensate for undesirable scrolling that occurs automatically due to
2147 # maximumBlockCount() text truncation.
2155 # maximumBlockCount() text truncation.
2148 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2156 if diff < 0 and document.blockCount() == document.maximumBlockCount():
2149 scrollbar.setValue(scrollbar.value() + diff)
2157 scrollbar.setValue(scrollbar.value() + diff)
2150
2158
2151 def _custom_context_menu_requested(self, pos):
2159 def _custom_context_menu_requested(self, pos):
2152 """ Shows a context menu at the given QPoint (in widget coordinates).
2160 """ Shows a context menu at the given QPoint (in widget coordinates).
2153 """
2161 """
2154 menu = self._context_menu_make(pos)
2162 menu = self._context_menu_make(pos)
2155 menu.exec_(self._control.mapToGlobal(pos))
2163 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,797 +1,797 b''
1 """Frontend widget for the Qt Console"""
1 """Frontend widget for the Qt Console"""
2
2
3 # Copyright (c) IPython Development Team.
3 # Copyright (c) IPython Development Team.
4 # Distributed under the terms of the Modified BSD License.
4 # Distributed under the terms of the Modified BSD License.
5
5
6 from __future__ import print_function
6 from __future__ import print_function
7
7
8 from collections import namedtuple
8 from collections import namedtuple
9 import sys
9 import sys
10 import uuid
10 import uuid
11
11
12 from IPython.external import qt
12 from IPython.external import qt
13 from IPython.external.qt import QtCore, QtGui
13 from IPython.external.qt import QtCore, QtGui
14 from IPython.utils import py3compat
14 from IPython.utils import py3compat
15 from IPython.utils.importstring import import_item
15 from IPython.utils.importstring import import_item
16
16
17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
18 from IPython.core.inputtransformer import classic_prompt
18 from IPython.core.inputtransformer import classic_prompt
19 from IPython.core.oinspect import call_tip
19 from IPython.core.oinspect import call_tip
20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
22 from .bracket_matcher import BracketMatcher
22 from .bracket_matcher import BracketMatcher
23 from .call_tip_widget import CallTipWidget
23 from .call_tip_widget import CallTipWidget
24 from .history_console_widget import HistoryConsoleWidget
24 from .history_console_widget import HistoryConsoleWidget
25 from .pygments_highlighter import PygmentsHighlighter
25 from .pygments_highlighter import PygmentsHighlighter
26
26
27
27
28 class FrontendHighlighter(PygmentsHighlighter):
28 class FrontendHighlighter(PygmentsHighlighter):
29 """ A PygmentsHighlighter that understands and ignores prompts.
29 """ A PygmentsHighlighter that understands and ignores prompts.
30 """
30 """
31
31
32 def __init__(self, frontend, lexer=None):
32 def __init__(self, frontend, lexer=None):
33 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
33 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
34 self._current_offset = 0
34 self._current_offset = 0
35 self._frontend = frontend
35 self._frontend = frontend
36 self.highlighting_on = False
36 self.highlighting_on = False
37
37
38 def highlightBlock(self, string):
38 def highlightBlock(self, string):
39 """ Highlight a block of text. Reimplemented to highlight selectively.
39 """ Highlight a block of text. Reimplemented to highlight selectively.
40 """
40 """
41 if not self.highlighting_on:
41 if not self.highlighting_on:
42 return
42 return
43
43
44 # The input to this function is a unicode string that may contain
44 # The input to this function is a unicode string that may contain
45 # paragraph break characters, non-breaking spaces, etc. Here we acquire
45 # paragraph break characters, non-breaking spaces, etc. Here we acquire
46 # the string as plain text so we can compare it.
46 # the string as plain text so we can compare it.
47 current_block = self.currentBlock()
47 current_block = self.currentBlock()
48 string = self._frontend._get_block_plain_text(current_block)
48 string = self._frontend._get_block_plain_text(current_block)
49
49
50 # Decide whether to check for the regular or continuation prompt.
50 # Decide whether to check for the regular or continuation prompt.
51 if current_block.contains(self._frontend._prompt_pos):
51 if current_block.contains(self._frontend._prompt_pos):
52 prompt = self._frontend._prompt
52 prompt = self._frontend._prompt
53 else:
53 else:
54 prompt = self._frontend._continuation_prompt
54 prompt = self._frontend._continuation_prompt
55
55
56 # Only highlight if we can identify a prompt, but make sure not to
56 # Only highlight if we can identify a prompt, but make sure not to
57 # highlight the prompt.
57 # highlight the prompt.
58 if string.startswith(prompt):
58 if string.startswith(prompt):
59 self._current_offset = len(prompt)
59 self._current_offset = len(prompt)
60 string = string[len(prompt):]
60 string = string[len(prompt):]
61 super(FrontendHighlighter, self).highlightBlock(string)
61 super(FrontendHighlighter, self).highlightBlock(string)
62
62
63 def rehighlightBlock(self, block):
63 def rehighlightBlock(self, block):
64 """ Reimplemented to temporarily enable highlighting if disabled.
64 """ Reimplemented to temporarily enable highlighting if disabled.
65 """
65 """
66 old = self.highlighting_on
66 old = self.highlighting_on
67 self.highlighting_on = True
67 self.highlighting_on = True
68 super(FrontendHighlighter, self).rehighlightBlock(block)
68 super(FrontendHighlighter, self).rehighlightBlock(block)
69 self.highlighting_on = old
69 self.highlighting_on = old
70
70
71 def setFormat(self, start, count, format):
71 def setFormat(self, start, count, format):
72 """ Reimplemented to highlight selectively.
72 """ Reimplemented to highlight selectively.
73 """
73 """
74 start += self._current_offset
74 start += self._current_offset
75 super(FrontendHighlighter, self).setFormat(start, count, format)
75 super(FrontendHighlighter, self).setFormat(start, count, format)
76
76
77
77
78 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
79 """ A Qt frontend for a generic Python kernel.
79 """ A Qt frontend for a generic Python kernel.
80 """
80 """
81
81
82 # The text to show when the kernel is (re)started.
82 # The text to show when the kernel is (re)started.
83 banner = Unicode(config=True)
83 banner = Unicode(config=True)
84 kernel_banner = Unicode()
84 kernel_banner = Unicode()
85
85
86 # An option and corresponding signal for overriding the default kernel
86 # An option and corresponding signal for overriding the default kernel
87 # interrupt behavior.
87 # interrupt behavior.
88 custom_interrupt = Bool(False)
88 custom_interrupt = Bool(False)
89 custom_interrupt_requested = QtCore.Signal()
89 custom_interrupt_requested = QtCore.Signal()
90
90
91 # An option and corresponding signals for overriding the default kernel
91 # An option and corresponding signals for overriding the default kernel
92 # restart behavior.
92 # restart behavior.
93 custom_restart = Bool(False)
93 custom_restart = Bool(False)
94 custom_restart_kernel_died = QtCore.Signal(float)
94 custom_restart_kernel_died = QtCore.Signal(float)
95 custom_restart_requested = QtCore.Signal()
95 custom_restart_requested = QtCore.Signal()
96
96
97 # Whether to automatically show calltips on open-parentheses.
97 # Whether to automatically show calltips on open-parentheses.
98 enable_calltips = Bool(True, config=True,
98 enable_calltips = Bool(True, config=True,
99 help="Whether to draw information calltips on open-parentheses.")
99 help="Whether to draw information calltips on open-parentheses.")
100
100
101 clear_on_kernel_restart = Bool(True, config=True,
101 clear_on_kernel_restart = Bool(True, config=True,
102 help="Whether to clear the console when the kernel is restarted")
102 help="Whether to clear the console when the kernel is restarted")
103
103
104 confirm_restart = Bool(True, config=True,
104 confirm_restart = Bool(True, config=True,
105 help="Whether to ask for user confirmation when restarting kernel")
105 help="Whether to ask for user confirmation when restarting kernel")
106
106
107 lexer_class = DottedObjectName(config=True,
107 lexer_class = DottedObjectName(config=True,
108 help="The pygments lexer class to use."
108 help="The pygments lexer class to use."
109 )
109 )
110 def _lexer_class_changed(self, name, old, new):
110 def _lexer_class_changed(self, name, old, new):
111 lexer_class = import_item(new)
111 lexer_class = import_item(new)
112 self.lexer = lexer_class()
112 self.lexer = lexer_class()
113
113
114 def _lexer_class_default(self):
114 def _lexer_class_default(self):
115 if py3compat.PY3:
115 if py3compat.PY3:
116 return 'pygments.lexers.Python3Lexer'
116 return 'pygments.lexers.Python3Lexer'
117 else:
117 else:
118 return 'pygments.lexers.PythonLexer'
118 return 'pygments.lexers.PythonLexer'
119
119
120 lexer = Any()
120 lexer = Any()
121 def _lexer_default(self):
121 def _lexer_default(self):
122 lexer_class = import_item(self.lexer_class)
122 lexer_class = import_item(self.lexer_class)
123 return lexer_class()
123 return lexer_class()
124
124
125 # Emitted when a user visible 'execute_request' has been submitted to the
125 # Emitted when a user visible 'execute_request' has been submitted to the
126 # kernel from the FrontendWidget. Contains the code to be executed.
126 # kernel from the FrontendWidget. Contains the code to be executed.
127 executing = QtCore.Signal(object)
127 executing = QtCore.Signal(object)
128
128
129 # Emitted when a user-visible 'execute_reply' has been received from the
129 # Emitted when a user-visible 'execute_reply' has been received from the
130 # kernel and processed by the FrontendWidget. Contains the response message.
130 # kernel and processed by the FrontendWidget. Contains the response message.
131 executed = QtCore.Signal(object)
131 executed = QtCore.Signal(object)
132
132
133 # Emitted when an exit request has been received from the kernel.
133 # Emitted when an exit request has been received from the kernel.
134 exit_requested = QtCore.Signal(object)
134 exit_requested = QtCore.Signal(object)
135
135
136 # Protected class variables.
136 # Protected class variables.
137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 logical_line_transforms=[],
138 logical_line_transforms=[],
139 python_line_transforms=[],
139 python_line_transforms=[],
140 )
140 )
141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 _input_splitter_class = InputSplitter
144 _input_splitter_class = InputSplitter
145 _local_kernel = False
145 _local_kernel = False
146 _highlighter = Instance(FrontendHighlighter)
146 _highlighter = Instance(FrontendHighlighter)
147
147
148 #---------------------------------------------------------------------------
148 #---------------------------------------------------------------------------
149 # 'object' interface
149 # 'object' interface
150 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
151
151
152 def __init__(self, *args, **kw):
152 def __init__(self, *args, **kw):
153 super(FrontendWidget, self).__init__(*args, **kw)
153 super(FrontendWidget, self).__init__(*args, **kw)
154 # FIXME: remove this when PySide min version is updated past 1.0.7
154 # FIXME: remove this when PySide min version is updated past 1.0.7
155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 if qt.QT_API == qt.QT_API_PYSIDE:
156 if qt.QT_API == qt.QT_API_PYSIDE:
157 import PySide
157 import PySide
158 if PySide.__version_info__ < (1,0,7):
158 if PySide.__version_info__ < (1,0,7):
159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 self.enable_calltips = False
160 self.enable_calltips = False
161
161
162 # FrontendWidget protected variables.
162 # FrontendWidget protected variables.
163 self._bracket_matcher = BracketMatcher(self._control)
163 self._bracket_matcher = BracketMatcher(self._control)
164 self._call_tip_widget = CallTipWidget(self._control)
164 self._call_tip_widget = CallTipWidget(self._control)
165 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
165 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
166 self._hidden = False
166 self._hidden = False
167 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
167 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
168 self._input_splitter = self._input_splitter_class()
168 self._input_splitter = self._input_splitter_class()
169 self._kernel_manager = None
169 self._kernel_manager = None
170 self._kernel_client = None
170 self._kernel_client = None
171 self._request_info = {}
171 self._request_info = {}
172 self._request_info['execute'] = {};
172 self._request_info['execute'] = {};
173 self._callback_dict = {}
173 self._callback_dict = {}
174
174
175 # Configure the ConsoleWidget.
175 # Configure the ConsoleWidget.
176 self.tab_width = 4
176 self.tab_width = 4
177 self._set_continuation_prompt('... ')
177 self._set_continuation_prompt('... ')
178
178
179 # Configure the CallTipWidget.
179 # Configure the CallTipWidget.
180 self._call_tip_widget.setFont(self.font)
180 self._call_tip_widget.setFont(self.font)
181 self.font_changed.connect(self._call_tip_widget.setFont)
181 self.font_changed.connect(self._call_tip_widget.setFont)
182
182
183 # Configure actions.
183 # Configure actions.
184 action = self._copy_raw_action
184 action = self._copy_raw_action
185 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
185 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
186 action.setEnabled(False)
186 action.setEnabled(False)
187 action.setShortcut(QtGui.QKeySequence(key))
187 action.setShortcut(QtGui.QKeySequence(key))
188 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
188 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
189 action.triggered.connect(self.copy_raw)
189 action.triggered.connect(self.copy_raw)
190 self.copy_available.connect(action.setEnabled)
190 self.copy_available.connect(action.setEnabled)
191 self.addAction(action)
191 self.addAction(action)
192
192
193 # Connect signal handlers.
193 # Connect signal handlers.
194 document = self._control.document()
194 document = self._control.document()
195 document.contentsChange.connect(self._document_contents_change)
195 document.contentsChange.connect(self._document_contents_change)
196
196
197 # Set flag for whether we are connected via localhost.
197 # Set flag for whether we are connected via localhost.
198 self._local_kernel = kw.get('local_kernel',
198 self._local_kernel = kw.get('local_kernel',
199 FrontendWidget._local_kernel)
199 FrontendWidget._local_kernel)
200
200
201 # Whether or not a clear_output call is pending new output.
201 # Whether or not a clear_output call is pending new output.
202 self._pending_clearoutput = False
202 self._pending_clearoutput = False
203
203
204 #---------------------------------------------------------------------------
204 #---------------------------------------------------------------------------
205 # 'ConsoleWidget' public interface
205 # 'ConsoleWidget' public interface
206 #---------------------------------------------------------------------------
206 #---------------------------------------------------------------------------
207
207
208 def copy(self):
208 def copy(self):
209 """ Copy the currently selected text to the clipboard, removing prompts.
209 """ Copy the currently selected text to the clipboard, removing prompts.
210 """
210 """
211 if self._page_control is not None and self._page_control.hasFocus():
211 if self._page_control is not None and self._page_control.hasFocus():
212 self._page_control.copy()
212 self._page_control.copy()
213 elif self._control.hasFocus():
213 elif self._control.hasFocus():
214 text = self._control.textCursor().selection().toPlainText()
214 text = self._control.textCursor().selection().toPlainText()
215 if text:
215 if text:
216 was_newline = text[-1] == '\n'
216 was_newline = text[-1] == '\n'
217 text = self._prompt_transformer.transform_cell(text)
217 text = self._prompt_transformer.transform_cell(text)
218 if not was_newline: # user doesn't need newline
218 if not was_newline: # user doesn't need newline
219 text = text[:-1]
219 text = text[:-1]
220 QtGui.QApplication.clipboard().setText(text)
220 QtGui.QApplication.clipboard().setText(text)
221 else:
221 else:
222 self.log.debug("frontend widget : unknown copy target")
222 self.log.debug("frontend widget : unknown copy target")
223
223
224 #---------------------------------------------------------------------------
224 #---------------------------------------------------------------------------
225 # 'ConsoleWidget' abstract interface
225 # 'ConsoleWidget' abstract interface
226 #---------------------------------------------------------------------------
226 #---------------------------------------------------------------------------
227
227
228 def _is_complete(self, source, interactive):
228 def _is_complete(self, source, interactive):
229 """ Returns whether 'source' can be completely processed and a new
229 """ Returns whether 'source' can be completely processed and a new
230 prompt created. When triggered by an Enter/Return key press,
230 prompt created. When triggered by an Enter/Return key press,
231 'interactive' is True; otherwise, it is False.
231 'interactive' is True; otherwise, it is False.
232 """
232 """
233 self._input_splitter.reset()
233 self._input_splitter.reset()
234 try:
234 try:
235 complete = self._input_splitter.push(source)
235 complete = self._input_splitter.push(source)
236 except SyntaxError:
236 except SyntaxError:
237 return True
237 return True
238 if interactive:
238 if interactive:
239 complete = not self._input_splitter.push_accepts_more()
239 complete = not self._input_splitter.push_accepts_more()
240 return complete
240 return complete
241
241
242 def _execute(self, source, hidden):
242 def _execute(self, source, hidden):
243 """ Execute 'source'. If 'hidden', do not show any output.
243 """ Execute 'source'. If 'hidden', do not show any output.
244
244
245 See parent class :meth:`execute` docstring for full details.
245 See parent class :meth:`execute` docstring for full details.
246 """
246 """
247 msg_id = self.kernel_client.execute(source, hidden)
247 msg_id = self.kernel_client.execute(source, hidden)
248 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
248 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
249 self._hidden = hidden
249 self._hidden = hidden
250 if not hidden:
250 if not hidden:
251 self.executing.emit(source)
251 self.executing.emit(source)
252
252
253 def _prompt_started_hook(self):
253 def _prompt_started_hook(self):
254 """ Called immediately after a new prompt is displayed.
254 """ Called immediately after a new prompt is displayed.
255 """
255 """
256 if not self._reading:
256 if not self._reading:
257 self._highlighter.highlighting_on = True
257 self._highlighter.highlighting_on = True
258
258
259 def _prompt_finished_hook(self):
259 def _prompt_finished_hook(self):
260 """ Called immediately after a prompt is finished, i.e. when some input
260 """ Called immediately after a prompt is finished, i.e. when some input
261 will be processed and a new prompt displayed.
261 will be processed and a new prompt displayed.
262 """
262 """
263 # Flush all state from the input splitter so the next round of
263 # Flush all state from the input splitter so the next round of
264 # reading input starts with a clean buffer.
264 # reading input starts with a clean buffer.
265 self._input_splitter.reset()
265 self._input_splitter.reset()
266
266
267 if not self._reading:
267 if not self._reading:
268 self._highlighter.highlighting_on = False
268 self._highlighter.highlighting_on = False
269
269
270 def _tab_pressed(self):
270 def _tab_pressed(self):
271 """ Called when the tab key is pressed. Returns whether to continue
271 """ Called when the tab key is pressed. Returns whether to continue
272 processing the event.
272 processing the event.
273 """
273 """
274 # Perform tab completion if:
274 # Perform tab completion if:
275 # 1) The cursor is in the input buffer.
275 # 1) The cursor is in the input buffer.
276 # 2) There is a non-whitespace character before the cursor.
276 # 2) There is a non-whitespace character before the cursor.
277 text = self._get_input_buffer_cursor_line()
277 text = self._get_input_buffer_cursor_line()
278 if text is None:
278 if text is None:
279 return False
279 return False
280 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
280 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
281 if complete:
281 if complete:
282 self._complete()
282 self._complete()
283 return not complete
283 return not complete
284
284
285 #---------------------------------------------------------------------------
285 #---------------------------------------------------------------------------
286 # 'ConsoleWidget' protected interface
286 # 'ConsoleWidget' protected interface
287 #---------------------------------------------------------------------------
287 #---------------------------------------------------------------------------
288
288
289 def _context_menu_make(self, pos):
289 def _context_menu_make(self, pos):
290 """ Reimplemented to add an action for raw copy.
290 """ Reimplemented to add an action for raw copy.
291 """
291 """
292 menu = super(FrontendWidget, self)._context_menu_make(pos)
292 menu = super(FrontendWidget, self)._context_menu_make(pos)
293 for before_action in menu.actions():
293 for before_action in menu.actions():
294 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
294 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
295 QtGui.QKeySequence.ExactMatch:
295 QtGui.QKeySequence.ExactMatch:
296 menu.insertAction(before_action, self._copy_raw_action)
296 menu.insertAction(before_action, self._copy_raw_action)
297 break
297 break
298 return menu
298 return menu
299
299
300 def request_interrupt_kernel(self):
300 def request_interrupt_kernel(self):
301 if self._executing:
301 if self._executing:
302 self.interrupt_kernel()
302 self.interrupt_kernel()
303
303
304 def request_restart_kernel(self):
304 def request_restart_kernel(self):
305 message = 'Are you sure you want to restart the kernel?'
305 message = 'Are you sure you want to restart the kernel?'
306 self.restart_kernel(message, now=False)
306 self.restart_kernel(message, now=False)
307
307
308 def _event_filter_console_keypress(self, event):
308 def _event_filter_console_keypress(self, event):
309 """ Reimplemented for execution interruption and smart backspace.
309 """ Reimplemented for execution interruption and smart backspace.
310 """
310 """
311 key = event.key()
311 key = event.key()
312 if self._control_key_down(event.modifiers(), include_command=False):
312 if self._control_key_down(event.modifiers(), include_command=False):
313
313
314 if key == QtCore.Qt.Key_C and self._executing:
314 if key == QtCore.Qt.Key_C and self._executing:
315 self.request_interrupt_kernel()
315 self.request_interrupt_kernel()
316 return True
316 return True
317
317
318 elif key == QtCore.Qt.Key_Period:
318 elif key == QtCore.Qt.Key_Period:
319 self.request_restart_kernel()
319 self.request_restart_kernel()
320 return True
320 return True
321
321
322 elif not event.modifiers() & QtCore.Qt.AltModifier:
322 elif not event.modifiers() & QtCore.Qt.AltModifier:
323
323
324 # Smart backspace: remove four characters in one backspace if:
324 # Smart backspace: remove four characters in one backspace if:
325 # 1) everything left of the cursor is whitespace
325 # 1) everything left of the cursor is whitespace
326 # 2) the four characters immediately left of the cursor are spaces
326 # 2) the four characters immediately left of the cursor are spaces
327 if key == QtCore.Qt.Key_Backspace:
327 if key == QtCore.Qt.Key_Backspace:
328 col = self._get_input_buffer_cursor_column()
328 col = self._get_input_buffer_cursor_column()
329 cursor = self._control.textCursor()
329 cursor = self._control.textCursor()
330 if col > 3 and not cursor.hasSelection():
330 if col > 3 and not cursor.hasSelection():
331 text = self._get_input_buffer_cursor_line()[:col]
331 text = self._get_input_buffer_cursor_line()[:col]
332 if text.endswith(' ') and not text.strip():
332 if text.endswith(' ') and not text.strip():
333 cursor.movePosition(QtGui.QTextCursor.Left,
333 cursor.movePosition(QtGui.QTextCursor.Left,
334 QtGui.QTextCursor.KeepAnchor, 4)
334 QtGui.QTextCursor.KeepAnchor, 4)
335 cursor.removeSelectedText()
335 cursor.removeSelectedText()
336 return True
336 return True
337
337
338 return super(FrontendWidget, self)._event_filter_console_keypress(event)
338 return super(FrontendWidget, self)._event_filter_console_keypress(event)
339
339
340 def _insert_continuation_prompt(self, cursor):
340 def _insert_continuation_prompt(self, cursor):
341 """ Reimplemented for auto-indentation.
341 """ Reimplemented for auto-indentation.
342 """
342 """
343 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
343 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
344 cursor.insertText(' ' * self._input_splitter.indent_spaces)
344 cursor.insertText(' ' * self._input_splitter.indent_spaces)
345
345
346 #---------------------------------------------------------------------------
346 #---------------------------------------------------------------------------
347 # 'BaseFrontendMixin' abstract interface
347 # 'BaseFrontendMixin' abstract interface
348 #---------------------------------------------------------------------------
348 #---------------------------------------------------------------------------
349 def _handle_clear_output(self, msg):
349 def _handle_clear_output(self, msg):
350 """Handle clear output messages."""
350 """Handle clear output messages."""
351 if not self._hidden and self._is_from_this_session(msg):
351 if include_output(msg):
352 wait = msg['content'].get('wait', True)
352 wait = msg['content'].get('wait', True)
353 if wait:
353 if wait:
354 self._pending_clearoutput = True
354 self._pending_clearoutput = True
355 else:
355 else:
356 self.clear_output()
356 self.clear_output()
357
357
358 def _silent_exec_callback(self, expr, callback):
358 def _silent_exec_callback(self, expr, callback):
359 """Silently execute `expr` in the kernel and call `callback` with reply
359 """Silently execute `expr` in the kernel and call `callback` with reply
360
360
361 the `expr` is evaluated silently in the kernel (without) output in
361 the `expr` is evaluated silently in the kernel (without) output in
362 the frontend. Call `callback` with the
362 the frontend. Call `callback` with the
363 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
363 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
364
364
365 Parameters
365 Parameters
366 ----------
366 ----------
367 expr : string
367 expr : string
368 valid string to be executed by the kernel.
368 valid string to be executed by the kernel.
369 callback : function
369 callback : function
370 function accepting one argument, as a string. The string will be
370 function accepting one argument, as a string. The string will be
371 the `repr` of the result of evaluating `expr`
371 the `repr` of the result of evaluating `expr`
372
372
373 The `callback` is called with the `repr()` of the result of `expr` as
373 The `callback` is called with the `repr()` of the result of `expr` as
374 first argument. To get the object, do `eval()` on the passed value.
374 first argument. To get the object, do `eval()` on the passed value.
375
375
376 See Also
376 See Also
377 --------
377 --------
378 _handle_exec_callback : private method, deal with calling callback with reply
378 _handle_exec_callback : private method, deal with calling callback with reply
379
379
380 """
380 """
381
381
382 # generate uuid, which would be used as an indication of whether or
382 # generate uuid, which would be used as an indication of whether or
383 # not the unique request originated from here (can use msg id ?)
383 # not the unique request originated from here (can use msg id ?)
384 local_uuid = str(uuid.uuid1())
384 local_uuid = str(uuid.uuid1())
385 msg_id = self.kernel_client.execute('',
385 msg_id = self.kernel_client.execute('',
386 silent=True, user_expressions={ local_uuid:expr })
386 silent=True, user_expressions={ local_uuid:expr })
387 self._callback_dict[local_uuid] = callback
387 self._callback_dict[local_uuid] = callback
388 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
388 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
389
389
390 def _handle_exec_callback(self, msg):
390 def _handle_exec_callback(self, msg):
391 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
391 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
392
392
393 Parameters
393 Parameters
394 ----------
394 ----------
395 msg : raw message send by the kernel containing an `user_expressions`
395 msg : raw message send by the kernel containing an `user_expressions`
396 and having a 'silent_exec_callback' kind.
396 and having a 'silent_exec_callback' kind.
397
397
398 Notes
398 Notes
399 -----
399 -----
400 This function will look for a `callback` associated with the
400 This function will look for a `callback` associated with the
401 corresponding message id. Association has been made by
401 corresponding message id. Association has been made by
402 `_silent_exec_callback`. `callback` is then called with the `repr()`
402 `_silent_exec_callback`. `callback` is then called with the `repr()`
403 of the value of corresponding `user_expressions` as argument.
403 of the value of corresponding `user_expressions` as argument.
404 `callback` is then removed from the known list so that any message
404 `callback` is then removed from the known list so that any message
405 coming again with the same id won't trigger it.
405 coming again with the same id won't trigger it.
406
406
407 """
407 """
408
408
409 user_exp = msg['content'].get('user_expressions')
409 user_exp = msg['content'].get('user_expressions')
410 if not user_exp:
410 if not user_exp:
411 return
411 return
412 for expression in user_exp:
412 for expression in user_exp:
413 if expression in self._callback_dict:
413 if expression in self._callback_dict:
414 self._callback_dict.pop(expression)(user_exp[expression])
414 self._callback_dict.pop(expression)(user_exp[expression])
415
415
416 def _handle_execute_reply(self, msg):
416 def _handle_execute_reply(self, msg):
417 """ Handles replies for code execution.
417 """ Handles replies for code execution.
418 """
418 """
419 self.log.debug("execute: %s", msg.get('content', ''))
419 self.log.debug("execute: %s", msg.get('content', ''))
420 msg_id = msg['parent_header']['msg_id']
420 msg_id = msg['parent_header']['msg_id']
421 info = self._request_info['execute'].get(msg_id)
421 info = self._request_info['execute'].get(msg_id)
422 # unset reading flag, because if execute finished, raw_input can't
422 # unset reading flag, because if execute finished, raw_input can't
423 # still be pending.
423 # still be pending.
424 self._reading = False
424 self._reading = False
425 if info and info.kind == 'user' and not self._hidden:
425 if info and info.kind == 'user' and not self._hidden:
426 # Make sure that all output from the SUB channel has been processed
426 # Make sure that all output from the SUB channel has been processed
427 # before writing a new prompt.
427 # before writing a new prompt.
428 self.kernel_client.iopub_channel.flush()
428 self.kernel_client.iopub_channel.flush()
429
429
430 # Reset the ANSI style information to prevent bad text in stdout
430 # Reset the ANSI style information to prevent bad text in stdout
431 # from messing up our colors. We're not a true terminal so we're
431 # from messing up our colors. We're not a true terminal so we're
432 # allowed to do this.
432 # allowed to do this.
433 if self.ansi_codes:
433 if self.ansi_codes:
434 self._ansi_processor.reset_sgr()
434 self._ansi_processor.reset_sgr()
435
435
436 content = msg['content']
436 content = msg['content']
437 status = content['status']
437 status = content['status']
438 if status == 'ok':
438 if status == 'ok':
439 self._process_execute_ok(msg)
439 self._process_execute_ok(msg)
440 elif status == 'error':
440 elif status == 'error':
441 self._process_execute_error(msg)
441 self._process_execute_error(msg)
442 elif status == 'aborted':
442 elif status == 'aborted':
443 self._process_execute_abort(msg)
443 self._process_execute_abort(msg)
444
444
445 self._show_interpreter_prompt_for_reply(msg)
445 self._show_interpreter_prompt_for_reply(msg)
446 self.executed.emit(msg)
446 self.executed.emit(msg)
447 self._request_info['execute'].pop(msg_id)
447 self._request_info['execute'].pop(msg_id)
448 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
448 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
449 self._handle_exec_callback(msg)
449 self._handle_exec_callback(msg)
450 self._request_info['execute'].pop(msg_id)
450 self._request_info['execute'].pop(msg_id)
451 else:
451 else:
452 super(FrontendWidget, self)._handle_execute_reply(msg)
452 super(FrontendWidget, self)._handle_execute_reply(msg)
453
453
454 def _handle_input_request(self, msg):
454 def _handle_input_request(self, msg):
455 """ Handle requests for raw_input.
455 """ Handle requests for raw_input.
456 """
456 """
457 self.log.debug("input: %s", msg.get('content', ''))
457 self.log.debug("input: %s", msg.get('content', ''))
458 if self._hidden:
458 if self._hidden:
459 raise RuntimeError('Request for raw input during hidden execution.')
459 raise RuntimeError('Request for raw input during hidden execution.')
460
460
461 # Make sure that all output from the SUB channel has been processed
461 # Make sure that all output from the SUB channel has been processed
462 # before entering readline mode.
462 # before entering readline mode.
463 self.kernel_client.iopub_channel.flush()
463 self.kernel_client.iopub_channel.flush()
464
464
465 def callback(line):
465 def callback(line):
466 self.kernel_client.stdin_channel.input(line)
466 self.kernel_client.stdin_channel.input(line)
467 if self._reading:
467 if self._reading:
468 self.log.debug("Got second input request, assuming first was interrupted.")
468 self.log.debug("Got second input request, assuming first was interrupted.")
469 self._reading = False
469 self._reading = False
470 self._readline(msg['content']['prompt'], callback=callback)
470 self._readline(msg['content']['prompt'], callback=callback)
471
471
472 def _kernel_restarted_message(self, died=True):
472 def _kernel_restarted_message(self, died=True):
473 msg = "Kernel died, restarting" if died else "Kernel restarting"
473 msg = "Kernel died, restarting" if died else "Kernel restarting"
474 self._append_html("<br>%s<hr><br>" % msg,
474 self._append_html("<br>%s<hr><br>" % msg,
475 before_prompt=False
475 before_prompt=False
476 )
476 )
477
477
478 def _handle_kernel_died(self, since_last_heartbeat):
478 def _handle_kernel_died(self, since_last_heartbeat):
479 """Handle the kernel's death (if we do not own the kernel).
479 """Handle the kernel's death (if we do not own the kernel).
480 """
480 """
481 self.log.warn("kernel died: %s", since_last_heartbeat)
481 self.log.warn("kernel died: %s", since_last_heartbeat)
482 if self.custom_restart:
482 if self.custom_restart:
483 self.custom_restart_kernel_died.emit(since_last_heartbeat)
483 self.custom_restart_kernel_died.emit(since_last_heartbeat)
484 else:
484 else:
485 self._kernel_restarted_message(died=True)
485 self._kernel_restarted_message(died=True)
486 self.reset()
486 self.reset()
487
487
488 def _handle_kernel_restarted(self, died=True):
488 def _handle_kernel_restarted(self, died=True):
489 """Notice that the autorestarter restarted the kernel.
489 """Notice that the autorestarter restarted the kernel.
490
490
491 There's nothing to do but show a message.
491 There's nothing to do but show a message.
492 """
492 """
493 self.log.warn("kernel restarted")
493 self.log.warn("kernel restarted")
494 self._kernel_restarted_message(died=died)
494 self._kernel_restarted_message(died=died)
495 self.reset()
495 self.reset()
496
496
497 def _handle_inspect_reply(self, rep):
497 def _handle_inspect_reply(self, rep):
498 """Handle replies for call tips."""
498 """Handle replies for call tips."""
499 self.log.debug("oinfo: %s", rep.get('content', ''))
499 self.log.debug("oinfo: %s", rep.get('content', ''))
500 cursor = self._get_cursor()
500 cursor = self._get_cursor()
501 info = self._request_info.get('call_tip')
501 info = self._request_info.get('call_tip')
502 if info and info.id == rep['parent_header']['msg_id'] and \
502 if info and info.id == rep['parent_header']['msg_id'] and \
503 info.pos == cursor.position():
503 info.pos == cursor.position():
504 content = rep['content']
504 content = rep['content']
505 if content.get('status') == 'ok' and content.get('found', False):
505 if content.get('status') == 'ok' and content.get('found', False):
506 self._call_tip_widget.show_inspect_data(content)
506 self._call_tip_widget.show_inspect_data(content)
507
507
508 def _handle_execute_result(self, msg):
508 def _handle_execute_result(self, msg):
509 """ Handle display hook output.
509 """ Handle display hook output.
510 """
510 """
511 self.log.debug("execute_result: %s", msg.get('content', ''))
511 self.log.debug("execute_result: %s", msg.get('content', ''))
512 if not self._hidden and self._is_from_this_session(msg):
512 if self.include_output(msg):
513 self.flush_clearoutput()
513 self.flush_clearoutput()
514 text = msg['content']['data']
514 text = msg['content']['data']
515 self._append_plain_text(text + '\n', before_prompt=True)
515 self._append_plain_text(text + '\n', before_prompt=True)
516
516
517 def _handle_stream(self, msg):
517 def _handle_stream(self, msg):
518 """ Handle stdout, stderr, and stdin.
518 """ Handle stdout, stderr, and stdin.
519 """
519 """
520 self.log.debug("stream: %s", msg.get('content', ''))
520 self.log.debug("stream: %s", msg.get('content', ''))
521 if not self._hidden and self._is_from_this_session(msg):
521 if self.include_output(msg):
522 self.flush_clearoutput()
522 self.flush_clearoutput()
523 self.append_stream(msg['content']['text'])
523 self.append_stream(msg['content']['text'])
524
524
525 def _handle_shutdown_reply(self, msg):
525 def _handle_shutdown_reply(self, msg):
526 """ Handle shutdown signal, only if from other console.
526 """ Handle shutdown signal, only if from other console.
527 """
527 """
528 self.log.info("shutdown: %s", msg.get('content', ''))
528 self.log.info("shutdown: %s", msg.get('content', ''))
529 restart = msg.get('content', {}).get('restart', False)
529 restart = msg.get('content', {}).get('restart', False)
530 if not self._hidden and not self._is_from_this_session(msg):
530 if not self._hidden and not self.from_here(msg):
531 # got shutdown reply, request came from session other than ours
531 # got shutdown reply, request came from session other than ours
532 if restart:
532 if restart:
533 # someone restarted the kernel, handle it
533 # someone restarted the kernel, handle it
534 self._handle_kernel_restarted(died=False)
534 self._handle_kernel_restarted(died=False)
535 else:
535 else:
536 # kernel was shutdown permanently
536 # kernel was shutdown permanently
537 # this triggers exit_requested if the kernel was local,
537 # this triggers exit_requested if the kernel was local,
538 # and a dialog if the kernel was remote,
538 # and a dialog if the kernel was remote,
539 # so we don't suddenly clear the qtconsole without asking.
539 # so we don't suddenly clear the qtconsole without asking.
540 if self._local_kernel:
540 if self._local_kernel:
541 self.exit_requested.emit(self)
541 self.exit_requested.emit(self)
542 else:
542 else:
543 title = self.window().windowTitle()
543 title = self.window().windowTitle()
544 reply = QtGui.QMessageBox.question(self, title,
544 reply = QtGui.QMessageBox.question(self, title,
545 "Kernel has been shutdown permanently. "
545 "Kernel has been shutdown permanently. "
546 "Close the Console?",
546 "Close the Console?",
547 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
547 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
548 if reply == QtGui.QMessageBox.Yes:
548 if reply == QtGui.QMessageBox.Yes:
549 self.exit_requested.emit(self)
549 self.exit_requested.emit(self)
550
550
551 def _handle_status(self, msg):
551 def _handle_status(self, msg):
552 """Handle status message"""
552 """Handle status message"""
553 # This is where a busy/idle indicator would be triggered,
553 # This is where a busy/idle indicator would be triggered,
554 # when we make one.
554 # when we make one.
555 state = msg['content'].get('execution_state', '')
555 state = msg['content'].get('execution_state', '')
556 if state == 'starting':
556 if state == 'starting':
557 # kernel started while we were running
557 # kernel started while we were running
558 if self._executing:
558 if self._executing:
559 self._handle_kernel_restarted(died=True)
559 self._handle_kernel_restarted(died=True)
560 elif state == 'idle':
560 elif state == 'idle':
561 pass
561 pass
562 elif state == 'busy':
562 elif state == 'busy':
563 pass
563 pass
564
564
565 def _started_channels(self):
565 def _started_channels(self):
566 """ Called when the KernelManager channels have started listening or
566 """ Called when the KernelManager channels have started listening or
567 when the frontend is assigned an already listening KernelManager.
567 when the frontend is assigned an already listening KernelManager.
568 """
568 """
569 self.reset(clear=True)
569 self.reset(clear=True)
570
570
571 #---------------------------------------------------------------------------
571 #---------------------------------------------------------------------------
572 # 'FrontendWidget' public interface
572 # 'FrontendWidget' public interface
573 #---------------------------------------------------------------------------
573 #---------------------------------------------------------------------------
574
574
575 def copy_raw(self):
575 def copy_raw(self):
576 """ Copy the currently selected text to the clipboard without attempting
576 """ Copy the currently selected text to the clipboard without attempting
577 to remove prompts or otherwise alter the text.
577 to remove prompts or otherwise alter the text.
578 """
578 """
579 self._control.copy()
579 self._control.copy()
580
580
581 def execute_file(self, path, hidden=False):
581 def execute_file(self, path, hidden=False):
582 """ Attempts to execute file with 'path'. If 'hidden', no output is
582 """ Attempts to execute file with 'path'. If 'hidden', no output is
583 shown.
583 shown.
584 """
584 """
585 self.execute('execfile(%r)' % path, hidden=hidden)
585 self.execute('execfile(%r)' % path, hidden=hidden)
586
586
587 def interrupt_kernel(self):
587 def interrupt_kernel(self):
588 """ Attempts to interrupt the running kernel.
588 """ Attempts to interrupt the running kernel.
589
589
590 Also unsets _reading flag, to avoid runtime errors
590 Also unsets _reading flag, to avoid runtime errors
591 if raw_input is called again.
591 if raw_input is called again.
592 """
592 """
593 if self.custom_interrupt:
593 if self.custom_interrupt:
594 self._reading = False
594 self._reading = False
595 self.custom_interrupt_requested.emit()
595 self.custom_interrupt_requested.emit()
596 elif self.kernel_manager:
596 elif self.kernel_manager:
597 self._reading = False
597 self._reading = False
598 self.kernel_manager.interrupt_kernel()
598 self.kernel_manager.interrupt_kernel()
599 else:
599 else:
600 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
600 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
601
601
602 def reset(self, clear=False):
602 def reset(self, clear=False):
603 """ Resets the widget to its initial state if ``clear`` parameter
603 """ Resets the widget to its initial state if ``clear`` parameter
604 is True, otherwise
604 is True, otherwise
605 prints a visual indication of the fact that the kernel restarted, but
605 prints a visual indication of the fact that the kernel restarted, but
606 does not clear the traces from previous usage of the kernel before it
606 does not clear the traces from previous usage of the kernel before it
607 was restarted. With ``clear=True``, it is similar to ``%clear``, but
607 was restarted. With ``clear=True``, it is similar to ``%clear``, but
608 also re-writes the banner and aborts execution if necessary.
608 also re-writes the banner and aborts execution if necessary.
609 """
609 """
610 if self._executing:
610 if self._executing:
611 self._executing = False
611 self._executing = False
612 self._request_info['execute'] = {}
612 self._request_info['execute'] = {}
613 self._reading = False
613 self._reading = False
614 self._highlighter.highlighting_on = False
614 self._highlighter.highlighting_on = False
615
615
616 if clear:
616 if clear:
617 self._control.clear()
617 self._control.clear()
618 self._append_plain_text(self.banner)
618 self._append_plain_text(self.banner)
619 if self.kernel_banner:
619 if self.kernel_banner:
620 self._append_plain_text(self.kernel_banner)
620 self._append_plain_text(self.kernel_banner)
621
621
622 # update output marker for stdout/stderr, so that startup
622 # update output marker for stdout/stderr, so that startup
623 # messages appear after banner:
623 # messages appear after banner:
624 self._append_before_prompt_pos = self._get_cursor().position()
624 self._append_before_prompt_pos = self._get_cursor().position()
625 self._show_interpreter_prompt()
625 self._show_interpreter_prompt()
626
626
627 def restart_kernel(self, message, now=False):
627 def restart_kernel(self, message, now=False):
628 """ Attempts to restart the running kernel.
628 """ Attempts to restart the running kernel.
629 """
629 """
630 # FIXME: now should be configurable via a checkbox in the dialog. Right
630 # FIXME: now should be configurable via a checkbox in the dialog. Right
631 # now at least the heartbeat path sets it to True and the manual restart
631 # now at least the heartbeat path sets it to True and the manual restart
632 # to False. But those should just be the pre-selected states of a
632 # to False. But those should just be the pre-selected states of a
633 # checkbox that the user could override if so desired. But I don't know
633 # checkbox that the user could override if so desired. But I don't know
634 # enough Qt to go implementing the checkbox now.
634 # enough Qt to go implementing the checkbox now.
635
635
636 if self.custom_restart:
636 if self.custom_restart:
637 self.custom_restart_requested.emit()
637 self.custom_restart_requested.emit()
638 return
638 return
639
639
640 if self.kernel_manager:
640 if self.kernel_manager:
641 # Pause the heart beat channel to prevent further warnings.
641 # Pause the heart beat channel to prevent further warnings.
642 self.kernel_client.hb_channel.pause()
642 self.kernel_client.hb_channel.pause()
643
643
644 # Prompt the user to restart the kernel. Un-pause the heartbeat if
644 # Prompt the user to restart the kernel. Un-pause the heartbeat if
645 # they decline. (If they accept, the heartbeat will be un-paused
645 # they decline. (If they accept, the heartbeat will be un-paused
646 # automatically when the kernel is restarted.)
646 # automatically when the kernel is restarted.)
647 if self.confirm_restart:
647 if self.confirm_restart:
648 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
648 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
649 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
649 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
650 message, buttons)
650 message, buttons)
651 do_restart = result == QtGui.QMessageBox.Yes
651 do_restart = result == QtGui.QMessageBox.Yes
652 else:
652 else:
653 # confirm_restart is False, so we don't need to ask user
653 # confirm_restart is False, so we don't need to ask user
654 # anything, just do the restart
654 # anything, just do the restart
655 do_restart = True
655 do_restart = True
656 if do_restart:
656 if do_restart:
657 try:
657 try:
658 self.kernel_manager.restart_kernel(now=now)
658 self.kernel_manager.restart_kernel(now=now)
659 except RuntimeError as e:
659 except RuntimeError as e:
660 self._append_plain_text(
660 self._append_plain_text(
661 'Error restarting kernel: %s\n' % e,
661 'Error restarting kernel: %s\n' % e,
662 before_prompt=True
662 before_prompt=True
663 )
663 )
664 else:
664 else:
665 self._append_html("<br>Restarting kernel...\n<hr><br>",
665 self._append_html("<br>Restarting kernel...\n<hr><br>",
666 before_prompt=True,
666 before_prompt=True,
667 )
667 )
668 else:
668 else:
669 self.kernel_client.hb_channel.unpause()
669 self.kernel_client.hb_channel.unpause()
670
670
671 else:
671 else:
672 self._append_plain_text(
672 self._append_plain_text(
673 'Cannot restart a Kernel I did not start\n',
673 'Cannot restart a Kernel I did not start\n',
674 before_prompt=True
674 before_prompt=True
675 )
675 )
676
676
677 def append_stream(self, text):
677 def append_stream(self, text):
678 """Appends text to the output stream."""
678 """Appends text to the output stream."""
679 # Most consoles treat tabs as being 8 space characters. Convert tabs
679 # Most consoles treat tabs as being 8 space characters. Convert tabs
680 # to spaces so that output looks as expected regardless of this
680 # to spaces so that output looks as expected regardless of this
681 # widget's tab width.
681 # widget's tab width.
682 text = text.expandtabs(8)
682 text = text.expandtabs(8)
683 self._append_plain_text(text, before_prompt=True)
683 self._append_plain_text(text, before_prompt=True)
684 self._control.moveCursor(QtGui.QTextCursor.End)
684 self._control.moveCursor(QtGui.QTextCursor.End)
685
685
686 def flush_clearoutput(self):
686 def flush_clearoutput(self):
687 """If a clearoutput is pending, execute it."""
687 """If a clearoutput is pending, execute it."""
688 if self._pending_clearoutput:
688 if self._pending_clearoutput:
689 self._pending_clearoutput = False
689 self._pending_clearoutput = False
690 self.clear_output()
690 self.clear_output()
691
691
692 def clear_output(self):
692 def clear_output(self):
693 """Clears the current line of output."""
693 """Clears the current line of output."""
694 cursor = self._control.textCursor()
694 cursor = self._control.textCursor()
695 cursor.beginEditBlock()
695 cursor.beginEditBlock()
696 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
696 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
697 cursor.insertText('')
697 cursor.insertText('')
698 cursor.endEditBlock()
698 cursor.endEditBlock()
699
699
700 #---------------------------------------------------------------------------
700 #---------------------------------------------------------------------------
701 # 'FrontendWidget' protected interface
701 # 'FrontendWidget' protected interface
702 #---------------------------------------------------------------------------
702 #---------------------------------------------------------------------------
703
703
704 def _call_tip(self):
704 def _call_tip(self):
705 """ Shows a call tip, if appropriate, at the current cursor location.
705 """ Shows a call tip, if appropriate, at the current cursor location.
706 """
706 """
707 # Decide if it makes sense to show a call tip
707 # Decide if it makes sense to show a call tip
708 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
708 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
709 return False
709 return False
710 cursor_pos = self._get_input_buffer_cursor_pos()
710 cursor_pos = self._get_input_buffer_cursor_pos()
711 code = self.input_buffer
711 code = self.input_buffer
712 # Send the metadata request to the kernel
712 # Send the metadata request to the kernel
713 msg_id = self.kernel_client.inspect(code, cursor_pos)
713 msg_id = self.kernel_client.inspect(code, cursor_pos)
714 pos = self._get_cursor().position()
714 pos = self._get_cursor().position()
715 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
715 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
716 return True
716 return True
717
717
718 def _complete(self):
718 def _complete(self):
719 """ Performs completion at the current cursor location.
719 """ Performs completion at the current cursor location.
720 """
720 """
721 # Send the completion request to the kernel
721 # Send the completion request to the kernel
722 msg_id = self.kernel_client.complete(
722 msg_id = self.kernel_client.complete(
723 code=self.input_buffer,
723 code=self.input_buffer,
724 cursor_pos=self._get_input_buffer_cursor_pos(),
724 cursor_pos=self._get_input_buffer_cursor_pos(),
725 )
725 )
726 pos = self._get_cursor().position()
726 pos = self._get_cursor().position()
727 info = self._CompletionRequest(msg_id, pos)
727 info = self._CompletionRequest(msg_id, pos)
728 self._request_info['complete'] = info
728 self._request_info['complete'] = info
729
729
730 def _process_execute_abort(self, msg):
730 def _process_execute_abort(self, msg):
731 """ Process a reply for an aborted execution request.
731 """ Process a reply for an aborted execution request.
732 """
732 """
733 self._append_plain_text("ERROR: execution aborted\n")
733 self._append_plain_text("ERROR: execution aborted\n")
734
734
735 def _process_execute_error(self, msg):
735 def _process_execute_error(self, msg):
736 """ Process a reply for an execution request that resulted in an error.
736 """ Process a reply for an execution request that resulted in an error.
737 """
737 """
738 content = msg['content']
738 content = msg['content']
739 # If a SystemExit is passed along, this means exit() was called - also
739 # If a SystemExit is passed along, this means exit() was called - also
740 # all the ipython %exit magic syntax of '-k' to be used to keep
740 # all the ipython %exit magic syntax of '-k' to be used to keep
741 # the kernel running
741 # the kernel running
742 if content['ename']=='SystemExit':
742 if content['ename']=='SystemExit':
743 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
743 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
744 self._keep_kernel_on_exit = keepkernel
744 self._keep_kernel_on_exit = keepkernel
745 self.exit_requested.emit(self)
745 self.exit_requested.emit(self)
746 else:
746 else:
747 traceback = ''.join(content['traceback'])
747 traceback = ''.join(content['traceback'])
748 self._append_plain_text(traceback)
748 self._append_plain_text(traceback)
749
749
750 def _process_execute_ok(self, msg):
750 def _process_execute_ok(self, msg):
751 """ Process a reply for a successful execution request.
751 """ Process a reply for a successful execution request.
752 """
752 """
753 payload = msg['content']['payload']
753 payload = msg['content']['payload']
754 for item in payload:
754 for item in payload:
755 if not self._process_execute_payload(item):
755 if not self._process_execute_payload(item):
756 warning = 'Warning: received unknown payload of type %s'
756 warning = 'Warning: received unknown payload of type %s'
757 print(warning % repr(item['source']))
757 print(warning % repr(item['source']))
758
758
759 def _process_execute_payload(self, item):
759 def _process_execute_payload(self, item):
760 """ Process a single payload item from the list of payload items in an
760 """ Process a single payload item from the list of payload items in an
761 execution reply. Returns whether the payload was handled.
761 execution reply. Returns whether the payload was handled.
762 """
762 """
763 # The basic FrontendWidget doesn't handle payloads, as they are a
763 # The basic FrontendWidget doesn't handle payloads, as they are a
764 # mechanism for going beyond the standard Python interpreter model.
764 # mechanism for going beyond the standard Python interpreter model.
765 return False
765 return False
766
766
767 def _show_interpreter_prompt(self):
767 def _show_interpreter_prompt(self):
768 """ Shows a prompt for the interpreter.
768 """ Shows a prompt for the interpreter.
769 """
769 """
770 self._show_prompt('>>> ')
770 self._show_prompt('>>> ')
771
771
772 def _show_interpreter_prompt_for_reply(self, msg):
772 def _show_interpreter_prompt_for_reply(self, msg):
773 """ Shows a prompt for the interpreter given an 'execute_reply' message.
773 """ Shows a prompt for the interpreter given an 'execute_reply' message.
774 """
774 """
775 self._show_interpreter_prompt()
775 self._show_interpreter_prompt()
776
776
777 #------ Signal handlers ----------------------------------------------------
777 #------ Signal handlers ----------------------------------------------------
778
778
779 def _document_contents_change(self, position, removed, added):
779 def _document_contents_change(self, position, removed, added):
780 """ Called whenever the document's content changes. Display a call tip
780 """ Called whenever the document's content changes. Display a call tip
781 if appropriate.
781 if appropriate.
782 """
782 """
783 # Calculate where the cursor should be *after* the change:
783 # Calculate where the cursor should be *after* the change:
784 position += added
784 position += added
785
785
786 document = self._control.document()
786 document = self._control.document()
787 if position == self._get_cursor().position():
787 if position == self._get_cursor().position():
788 self._call_tip()
788 self._call_tip()
789
789
790 #------ Trait default initializers -----------------------------------------
790 #------ Trait default initializers -----------------------------------------
791
791
792 def _banner_default(self):
792 def _banner_default(self):
793 """ Returns the standard Python banner.
793 """ Returns the standard Python banner.
794 """
794 """
795 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
795 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
796 '"license" for more information.'
796 '"license" for more information.'
797 return banner % (sys.version, sys.platform)
797 return banner % (sys.version, sys.platform)
@@ -1,581 +1,599 b''
1 """A FrontendWidget that emulates the interface of the console IPython.
1 """A FrontendWidget that emulates the interface of the console IPython.
2
2
3 This supports the additional functionality provided by the IPython kernel.
3 This supports the additional functionality provided by the IPython kernel.
4 """
4 """
5
5
6 # Copyright (c) IPython Development Team.
6 # Copyright (c) IPython Development Team.
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8
8
9 from collections import namedtuple
9 from collections import namedtuple
10 import os.path
10 import os.path
11 import re
11 import re
12 from subprocess import Popen
12 from subprocess import Popen
13 import sys
13 import sys
14 import time
14 import time
15 from textwrap import dedent
15 from textwrap import dedent
16
16
17 from IPython.external.qt import QtCore, QtGui
17 from IPython.external.qt import QtCore, QtGui
18
18
19 from IPython.core.inputsplitter import IPythonInputSplitter
19 from IPython.core.inputsplitter import IPythonInputSplitter
20 from IPython.core.release import version
20 from IPython.core.release import version
21 from IPython.core.inputtransformer import ipy_prompt
21 from IPython.core.inputtransformer import ipy_prompt
22 from IPython.utils.traitlets import Bool, Unicode
22 from IPython.utils.traitlets import Bool, Unicode
23 from .frontend_widget import FrontendWidget
23 from .frontend_widget import FrontendWidget
24 from . import styles
24 from . import styles
25
25
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 # Constants
27 # Constants
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29
29
30 # Default strings to build and display input and output prompts (and separators
30 # Default strings to build and display input and output prompts (and separators
31 # in between)
31 # in between)
32 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
32 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
33 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
33 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
34 default_input_sep = '\n'
34 default_input_sep = '\n'
35 default_output_sep = ''
35 default_output_sep = ''
36 default_output_sep2 = ''
36 default_output_sep2 = ''
37
37
38 # Base path for most payload sources.
38 # Base path for most payload sources.
39 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
39 zmq_shell_source = 'IPython.kernel.zmq.zmqshell.ZMQInteractiveShell'
40
40
41 if sys.platform.startswith('win'):
41 if sys.platform.startswith('win'):
42 default_editor = 'notepad'
42 default_editor = 'notepad'
43 else:
43 else:
44 default_editor = ''
44 default_editor = ''
45
45
46 #-----------------------------------------------------------------------------
46 #-----------------------------------------------------------------------------
47 # IPythonWidget class
47 # IPythonWidget class
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49
49
50 class IPythonWidget(FrontendWidget):
50 class IPythonWidget(FrontendWidget):
51 """ A FrontendWidget for an IPython kernel.
51 """ A FrontendWidget for an IPython kernel.
52 """
52 """
53
53
54 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
54 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
55 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
55 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
56 # settings.
56 # settings.
57 custom_edit = Bool(False)
57 custom_edit = Bool(False)
58 custom_edit_requested = QtCore.Signal(object, object)
58 custom_edit_requested = QtCore.Signal(object, object)
59
59
60 editor = Unicode(default_editor, config=True,
60 editor = Unicode(default_editor, config=True,
61 help="""
61 help="""
62 A command for invoking a system text editor. If the string contains a
62 A command for invoking a system text editor. If the string contains a
63 {filename} format specifier, it will be used. Otherwise, the filename
63 {filename} format specifier, it will be used. Otherwise, the filename
64 will be appended to the end the command.
64 will be appended to the end the command.
65 """)
65 """)
66
66
67 editor_line = Unicode(config=True,
67 editor_line = Unicode(config=True,
68 help="""
68 help="""
69 The editor command to use when a specific line number is requested. The
69 The editor command to use when a specific line number is requested. The
70 string should contain two format specifiers: {line} and {filename}. If
70 string should contain two format specifiers: {line} and {filename}. If
71 this parameter is not specified, the line number option to the %edit
71 this parameter is not specified, the line number option to the %edit
72 magic will be ignored.
72 magic will be ignored.
73 """)
73 """)
74
74
75 style_sheet = Unicode(config=True,
75 style_sheet = Unicode(config=True,
76 help="""
76 help="""
77 A CSS stylesheet. The stylesheet can contain classes for:
77 A CSS stylesheet. The stylesheet can contain classes for:
78 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
78 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
79 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
79 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
80 3. IPython: .error, .in-prompt, .out-prompt, etc
80 3. IPython: .error, .in-prompt, .out-prompt, etc
81 """)
81 """)
82
82
83 syntax_style = Unicode(config=True,
83 syntax_style = Unicode(config=True,
84 help="""
84 help="""
85 If not empty, use this Pygments style for syntax highlighting.
85 If not empty, use this Pygments style for syntax highlighting.
86 Otherwise, the style sheet is queried for Pygments style
86 Otherwise, the style sheet is queried for Pygments style
87 information.
87 information.
88 """)
88 """)
89
89
90 # Prompts.
90 # Prompts.
91 in_prompt = Unicode(default_in_prompt, config=True)
91 in_prompt = Unicode(default_in_prompt, config=True)
92 out_prompt = Unicode(default_out_prompt, config=True)
92 out_prompt = Unicode(default_out_prompt, config=True)
93 input_sep = Unicode(default_input_sep, config=True)
93 input_sep = Unicode(default_input_sep, config=True)
94 output_sep = Unicode(default_output_sep, config=True)
94 output_sep = Unicode(default_output_sep, config=True)
95 output_sep2 = Unicode(default_output_sep2, config=True)
95 output_sep2 = Unicode(default_output_sep2, config=True)
96
96
97 # FrontendWidget protected class variables.
97 # FrontendWidget protected class variables.
98 _input_splitter_class = IPythonInputSplitter
98 _input_splitter_class = IPythonInputSplitter
99 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
99 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[ipy_prompt()],
100 logical_line_transforms=[],
100 logical_line_transforms=[],
101 python_line_transforms=[],
101 python_line_transforms=[],
102 )
102 )
103
103
104 # IPythonWidget protected class variables.
104 # IPythonWidget protected class variables.
105 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
106 _payload_source_edit = 'edit_magic'
106 _payload_source_edit = 'edit_magic'
107 _payload_source_exit = 'ask_exit'
107 _payload_source_exit = 'ask_exit'
108 _payload_source_next_input = 'set_next_input'
108 _payload_source_next_input = 'set_next_input'
109 _payload_source_page = 'page'
109 _payload_source_page = 'page'
110 _retrying_history_request = False
110 _retrying_history_request = False
111 _starting = False
111 _starting = False
112
112
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114 # 'object' interface
114 # 'object' interface
115 #---------------------------------------------------------------------------
115 #---------------------------------------------------------------------------
116
116
117 def __init__(self, *args, **kw):
117 def __init__(self, *args, **kw):
118 super(IPythonWidget, self).__init__(*args, **kw)
118 super(IPythonWidget, self).__init__(*args, **kw)
119
119
120 # IPythonWidget protected variables.
120 # IPythonWidget protected variables.
121 self._payload_handlers = {
121 self._payload_handlers = {
122 self._payload_source_edit : self._handle_payload_edit,
122 self._payload_source_edit : self._handle_payload_edit,
123 self._payload_source_exit : self._handle_payload_exit,
123 self._payload_source_exit : self._handle_payload_exit,
124 self._payload_source_page : self._handle_payload_page,
124 self._payload_source_page : self._handle_payload_page,
125 self._payload_source_next_input : self._handle_payload_next_input }
125 self._payload_source_next_input : self._handle_payload_next_input }
126 self._previous_prompt_obj = None
126 self._previous_prompt_obj = None
127 self._keep_kernel_on_exit = None
127 self._keep_kernel_on_exit = None
128
128
129 # Initialize widget styling.
129 # Initialize widget styling.
130 if self.style_sheet:
130 if self.style_sheet:
131 self._style_sheet_changed()
131 self._style_sheet_changed()
132 self._syntax_style_changed()
132 self._syntax_style_changed()
133 else:
133 else:
134 self.set_default_style()
134 self.set_default_style()
135
135
136 self._guiref_loaded = False
136 self._guiref_loaded = False
137
137
138 #---------------------------------------------------------------------------
138 #---------------------------------------------------------------------------
139 # 'BaseFrontendMixin' abstract interface
139 # 'BaseFrontendMixin' abstract interface
140 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
141 def _handle_complete_reply(self, rep):
141 def _handle_complete_reply(self, rep):
142 """ Reimplemented to support IPython's improved completion machinery.
142 """ Reimplemented to support IPython's improved completion machinery.
143 """
143 """
144 self.log.debug("complete: %s", rep.get('content', ''))
144 self.log.debug("complete: %s", rep.get('content', ''))
145 cursor = self._get_cursor()
145 cursor = self._get_cursor()
146 info = self._request_info.get('complete')
146 info = self._request_info.get('complete')
147 if info and info.id == rep['parent_header']['msg_id'] and \
147 if info and info.id == rep['parent_header']['msg_id'] and \
148 info.pos == cursor.position():
148 info.pos == cursor.position():
149 content = rep['content']
149 content = rep['content']
150 matches = content['matches']
150 matches = content['matches']
151 start = content['cursor_start']
151 start = content['cursor_start']
152 end = content['cursor_end']
152 end = content['cursor_end']
153
153
154 start = max(start, 0)
154 start = max(start, 0)
155 end = max(end, start)
155 end = max(end, start)
156
156
157 # Move the control's cursor to the desired end point
157 # Move the control's cursor to the desired end point
158 cursor_pos = self._get_input_buffer_cursor_pos()
158 cursor_pos = self._get_input_buffer_cursor_pos()
159 if end < cursor_pos:
159 if end < cursor_pos:
160 cursor.movePosition(QtGui.QTextCursor.Left,
160 cursor.movePosition(QtGui.QTextCursor.Left,
161 n=(cursor_pos - end))
161 n=(cursor_pos - end))
162 elif end > cursor_pos:
162 elif end > cursor_pos:
163 cursor.movePosition(QtGui.QTextCursor.Right,
163 cursor.movePosition(QtGui.QTextCursor.Right,
164 n=(end - cursor_pos))
164 n=(end - cursor_pos))
165 # This line actually applies the move to control's cursor
165 # This line actually applies the move to control's cursor
166 self._control.setTextCursor(cursor)
166 self._control.setTextCursor(cursor)
167
167
168 offset = end - start
168 offset = end - start
169 # Move the local cursor object to the start of the match and
169 # Move the local cursor object to the start of the match and
170 # complete.
170 # complete.
171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
171 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
172 self._complete_with_items(cursor, matches)
172 self._complete_with_items(cursor, matches)
173
173
174 def _handle_execute_reply(self, msg):
174 def _handle_execute_reply(self, msg):
175 """ Reimplemented to support prompt requests.
175 """ Reimplemented to support prompt requests.
176 """
176 """
177 msg_id = msg['parent_header'].get('msg_id')
177 msg_id = msg['parent_header'].get('msg_id')
178 info = self._request_info['execute'].get(msg_id)
178 info = self._request_info['execute'].get(msg_id)
179 if info and info.kind == 'prompt':
179 if info and info.kind == 'prompt':
180 content = msg['content']
180 content = msg['content']
181 if content['status'] == 'aborted':
181 if content['status'] == 'aborted':
182 self._show_interpreter_prompt()
182 self._show_interpreter_prompt()
183 else:
183 else:
184 number = content['execution_count'] + 1
184 number = content['execution_count'] + 1
185 self._show_interpreter_prompt(number)
185 self._show_interpreter_prompt(number)
186 self._request_info['execute'].pop(msg_id)
186 self._request_info['execute'].pop(msg_id)
187 else:
187 else:
188 super(IPythonWidget, self)._handle_execute_reply(msg)
188 super(IPythonWidget, self)._handle_execute_reply(msg)
189
189
190 def _handle_history_reply(self, msg):
190 def _handle_history_reply(self, msg):
191 """ Implemented to handle history tail replies, which are only supported
191 """ Implemented to handle history tail replies, which are only supported
192 by the IPython kernel.
192 by the IPython kernel.
193 """
193 """
194 content = msg['content']
194 content = msg['content']
195 if 'history' not in content:
195 if 'history' not in content:
196 self.log.error("History request failed: %r"%content)
196 self.log.error("History request failed: %r"%content)
197 if content.get('status', '') == 'aborted' and \
197 if content.get('status', '') == 'aborted' and \
198 not self._retrying_history_request:
198 not self._retrying_history_request:
199 # a *different* action caused this request to be aborted, so
199 # a *different* action caused this request to be aborted, so
200 # we should try again.
200 # we should try again.
201 self.log.error("Retrying aborted history request")
201 self.log.error("Retrying aborted history request")
202 # prevent multiple retries of aborted requests:
202 # prevent multiple retries of aborted requests:
203 self._retrying_history_request = True
203 self._retrying_history_request = True
204 # wait out the kernel's queue flush, which is currently timed at 0.1s
204 # wait out the kernel's queue flush, which is currently timed at 0.1s
205 time.sleep(0.25)
205 time.sleep(0.25)
206 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
206 self.kernel_client.shell_channel.history(hist_access_type='tail',n=1000)
207 else:
207 else:
208 self._retrying_history_request = False
208 self._retrying_history_request = False
209 return
209 return
210 # reset retry flag
210 # reset retry flag
211 self._retrying_history_request = False
211 self._retrying_history_request = False
212 history_items = content['history']
212 history_items = content['history']
213 self.log.debug("Received history reply with %i entries", len(history_items))
213 self.log.debug("Received history reply with %i entries", len(history_items))
214 items = []
214 items = []
215 last_cell = u""
215 last_cell = u""
216 for _, _, cell in history_items:
216 for _, _, cell in history_items:
217 cell = cell.rstrip()
217 cell = cell.rstrip()
218 if cell != last_cell:
218 if cell != last_cell:
219 items.append(cell)
219 items.append(cell)
220 last_cell = cell
220 last_cell = cell
221 self._set_history(items)
221 self._set_history(items)
222
223 def _insert_other_input(self, cursor, content):
224 """Insert function for input from other frontends"""
225 cursor.beginEditBlock()
226 start = cursor.position()
227 n = content.get('execution_count', 0)
228 cursor.insertText('\n')
229 self._insert_html(cursor, self._make_in_prompt(n))
230 cursor.insertText(content['code'])
231 self._highlighter.rehighlightBlock(cursor.block())
232 cursor.endEditBlock()
233
234 def _handle_execute_input(self, msg):
235 """Handle an execute_input message"""
236 self.log.debug("execute_input: %s", msg.get('content', ''))
237 if self.include_output(msg):
238 self._append_custom(self._insert_other_input, msg['content'], before_prompt=True)
222
239
240
223 def _handle_execute_result(self, msg):
241 def _handle_execute_result(self, msg):
224 """ Reimplemented for IPython-style "display hook".
242 """ Reimplemented for IPython-style "display hook".
225 """
243 """
226 self.log.debug("execute_result: %s", msg.get('content', ''))
244 self.log.debug("execute_result: %s", msg.get('content', ''))
227 if not self._hidden and self._is_from_this_session(msg):
245 if self.include_output(msg):
228 self.flush_clearoutput()
246 self.flush_clearoutput()
229 content = msg['content']
247 content = msg['content']
230 prompt_number = content.get('execution_count', 0)
248 prompt_number = content.get('execution_count', 0)
231 data = content['data']
249 data = content['data']
232 if 'text/plain' in data:
250 if 'text/plain' in data:
233 self._append_plain_text(self.output_sep, True)
251 self._append_plain_text(self.output_sep, True)
234 self._append_html(self._make_out_prompt(prompt_number), True)
252 self._append_html(self._make_out_prompt(prompt_number), True)
235 text = data['text/plain']
253 text = data['text/plain']
236 # If the repr is multiline, make sure we start on a new line,
254 # If the repr is multiline, make sure we start on a new line,
237 # so that its lines are aligned.
255 # so that its lines are aligned.
238 if "\n" in text and not self.output_sep.endswith("\n"):
256 if "\n" in text and not self.output_sep.endswith("\n"):
239 self._append_plain_text('\n', True)
257 self._append_plain_text('\n', True)
240 self._append_plain_text(text + self.output_sep2, True)
258 self._append_plain_text(text + self.output_sep2, True)
241
259
242 def _handle_display_data(self, msg):
260 def _handle_display_data(self, msg):
243 """ The base handler for the ``display_data`` message.
261 """ The base handler for the ``display_data`` message.
244 """
262 """
245 self.log.debug("display: %s", msg.get('content', ''))
263 self.log.debug("display: %s", msg.get('content', ''))
246 # For now, we don't display data from other frontends, but we
264 # For now, we don't display data from other frontends, but we
247 # eventually will as this allows all frontends to monitor the display
265 # eventually will as this allows all frontends to monitor the display
248 # data. But we need to figure out how to handle this in the GUI.
266 # data. But we need to figure out how to handle this in the GUI.
249 if not self._hidden and self._is_from_this_session(msg):
267 if self.include_output(msg):
250 self.flush_clearoutput()
268 self.flush_clearoutput()
251 data = msg['content']['data']
269 data = msg['content']['data']
252 metadata = msg['content']['metadata']
270 metadata = msg['content']['metadata']
253 # In the regular IPythonWidget, we simply print the plain text
271 # In the regular IPythonWidget, we simply print the plain text
254 # representation.
272 # representation.
255 if 'text/plain' in data:
273 if 'text/plain' in data:
256 text = data['text/plain']
274 text = data['text/plain']
257 self._append_plain_text(text, True)
275 self._append_plain_text(text, True)
258 # This newline seems to be needed for text and html output.
276 # This newline seems to be needed for text and html output.
259 self._append_plain_text(u'\n', True)
277 self._append_plain_text(u'\n', True)
260
278
261 def _handle_kernel_info_reply(self, rep):
279 def _handle_kernel_info_reply(self, rep):
262 """Handle kernel info replies."""
280 """Handle kernel info replies."""
263 content = rep['content']
281 content = rep['content']
264 if not self._guiref_loaded:
282 if not self._guiref_loaded:
265 if content.get('language') == 'python':
283 if content.get('language') == 'python':
266 self._load_guiref_magic()
284 self._load_guiref_magic()
267 self._guiref_loaded = True
285 self._guiref_loaded = True
268
286
269 self.kernel_banner = content.get('banner', '')
287 self.kernel_banner = content.get('banner', '')
270 if self._starting:
288 if self._starting:
271 # finish handling started channels
289 # finish handling started channels
272 self._starting = False
290 self._starting = False
273 super(IPythonWidget, self)._started_channels()
291 super(IPythonWidget, self)._started_channels()
274
292
275 def _started_channels(self):
293 def _started_channels(self):
276 """Reimplemented to make a history request and load %guiref."""
294 """Reimplemented to make a history request and load %guiref."""
277 self._starting = True
295 self._starting = True
278 # The reply will trigger %guiref load provided language=='python'
296 # The reply will trigger %guiref load provided language=='python'
279 self.kernel_client.kernel_info()
297 self.kernel_client.kernel_info()
280
298
281 self.kernel_client.shell_channel.history(hist_access_type='tail',
299 self.kernel_client.shell_channel.history(hist_access_type='tail',
282 n=1000)
300 n=1000)
283
301
284 def _load_guiref_magic(self):
302 def _load_guiref_magic(self):
285 """Load %guiref magic."""
303 """Load %guiref magic."""
286 self.kernel_client.shell_channel.execute('\n'.join([
304 self.kernel_client.shell_channel.execute('\n'.join([
287 "try:",
305 "try:",
288 " _usage",
306 " _usage",
289 "except:",
307 "except:",
290 " from IPython.core import usage as _usage",
308 " from IPython.core import usage as _usage",
291 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
309 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
292 " del _usage",
310 " del _usage",
293 ]), silent=True)
311 ]), silent=True)
294
312
295 #---------------------------------------------------------------------------
313 #---------------------------------------------------------------------------
296 # 'ConsoleWidget' public interface
314 # 'ConsoleWidget' public interface
297 #---------------------------------------------------------------------------
315 #---------------------------------------------------------------------------
298
316
299 #---------------------------------------------------------------------------
317 #---------------------------------------------------------------------------
300 # 'FrontendWidget' public interface
318 # 'FrontendWidget' public interface
301 #---------------------------------------------------------------------------
319 #---------------------------------------------------------------------------
302
320
303 def execute_file(self, path, hidden=False):
321 def execute_file(self, path, hidden=False):
304 """ Reimplemented to use the 'run' magic.
322 """ Reimplemented to use the 'run' magic.
305 """
323 """
306 # Use forward slashes on Windows to avoid escaping each separator.
324 # Use forward slashes on Windows to avoid escaping each separator.
307 if sys.platform == 'win32':
325 if sys.platform == 'win32':
308 path = os.path.normpath(path).replace('\\', '/')
326 path = os.path.normpath(path).replace('\\', '/')
309
327
310 # Perhaps we should not be using %run directly, but while we
328 # Perhaps we should not be using %run directly, but while we
311 # are, it is necessary to quote or escape filenames containing spaces
329 # are, it is necessary to quote or escape filenames containing spaces
312 # or quotes.
330 # or quotes.
313
331
314 # In earlier code here, to minimize escaping, we sometimes quoted the
332 # In earlier code here, to minimize escaping, we sometimes quoted the
315 # filename with single quotes. But to do this, this code must be
333 # filename with single quotes. But to do this, this code must be
316 # platform-aware, because run uses shlex rather than python string
334 # platform-aware, because run uses shlex rather than python string
317 # parsing, so that:
335 # parsing, so that:
318 # * In Win: single quotes can be used in the filename without quoting,
336 # * In Win: single quotes can be used in the filename without quoting,
319 # and we cannot use single quotes to quote the filename.
337 # and we cannot use single quotes to quote the filename.
320 # * In *nix: we can escape double quotes in a double quoted filename,
338 # * In *nix: we can escape double quotes in a double quoted filename,
321 # but can't escape single quotes in a single quoted filename.
339 # but can't escape single quotes in a single quoted filename.
322
340
323 # So to keep this code non-platform-specific and simple, we now only
341 # So to keep this code non-platform-specific and simple, we now only
324 # use double quotes to quote filenames, and escape when needed:
342 # use double quotes to quote filenames, and escape when needed:
325 if ' ' in path or "'" in path or '"' in path:
343 if ' ' in path or "'" in path or '"' in path:
326 path = '"%s"' % path.replace('"', '\\"')
344 path = '"%s"' % path.replace('"', '\\"')
327 self.execute('%%run %s' % path, hidden=hidden)
345 self.execute('%%run %s' % path, hidden=hidden)
328
346
329 #---------------------------------------------------------------------------
347 #---------------------------------------------------------------------------
330 # 'FrontendWidget' protected interface
348 # 'FrontendWidget' protected interface
331 #---------------------------------------------------------------------------
349 #---------------------------------------------------------------------------
332
350
333 def _process_execute_error(self, msg):
351 def _process_execute_error(self, msg):
334 """ Reimplemented for IPython-style traceback formatting.
352 """ Reimplemented for IPython-style traceback formatting.
335 """
353 """
336 content = msg['content']
354 content = msg['content']
337 traceback = '\n'.join(content['traceback']) + '\n'
355 traceback = '\n'.join(content['traceback']) + '\n'
338 if False:
356 if False:
339 # FIXME: For now, tracebacks come as plain text, so we can't use
357 # FIXME: For now, tracebacks come as plain text, so we can't use
340 # the html renderer yet. Once we refactor ultratb to produce
358 # the html renderer yet. Once we refactor ultratb to produce
341 # properly styled tracebacks, this branch should be the default
359 # properly styled tracebacks, this branch should be the default
342 traceback = traceback.replace(' ', '&nbsp;')
360 traceback = traceback.replace(' ', '&nbsp;')
343 traceback = traceback.replace('\n', '<br/>')
361 traceback = traceback.replace('\n', '<br/>')
344
362
345 ename = content['ename']
363 ename = content['ename']
346 ename_styled = '<span class="error">%s</span>' % ename
364 ename_styled = '<span class="error">%s</span>' % ename
347 traceback = traceback.replace(ename, ename_styled)
365 traceback = traceback.replace(ename, ename_styled)
348
366
349 self._append_html(traceback)
367 self._append_html(traceback)
350 else:
368 else:
351 # This is the fallback for now, using plain text with ansi escapes
369 # This is the fallback for now, using plain text with ansi escapes
352 self._append_plain_text(traceback)
370 self._append_plain_text(traceback)
353
371
354 def _process_execute_payload(self, item):
372 def _process_execute_payload(self, item):
355 """ Reimplemented to dispatch payloads to handler methods.
373 """ Reimplemented to dispatch payloads to handler methods.
356 """
374 """
357 handler = self._payload_handlers.get(item['source'])
375 handler = self._payload_handlers.get(item['source'])
358 if handler is None:
376 if handler is None:
359 # We have no handler for this type of payload, simply ignore it
377 # We have no handler for this type of payload, simply ignore it
360 return False
378 return False
361 else:
379 else:
362 handler(item)
380 handler(item)
363 return True
381 return True
364
382
365 def _show_interpreter_prompt(self, number=None):
383 def _show_interpreter_prompt(self, number=None):
366 """ Reimplemented for IPython-style prompts.
384 """ Reimplemented for IPython-style prompts.
367 """
385 """
368 # If a number was not specified, make a prompt number request.
386 # If a number was not specified, make a prompt number request.
369 if number is None:
387 if number is None:
370 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
388 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
371 info = self._ExecutionRequest(msg_id, 'prompt')
389 info = self._ExecutionRequest(msg_id, 'prompt')
372 self._request_info['execute'][msg_id] = info
390 self._request_info['execute'][msg_id] = info
373 return
391 return
374
392
375 # Show a new prompt and save information about it so that it can be
393 # Show a new prompt and save information about it so that it can be
376 # updated later if the prompt number turns out to be wrong.
394 # updated later if the prompt number turns out to be wrong.
377 self._prompt_sep = self.input_sep
395 self._prompt_sep = self.input_sep
378 self._show_prompt(self._make_in_prompt(number), html=True)
396 self._show_prompt(self._make_in_prompt(number), html=True)
379 block = self._control.document().lastBlock()
397 block = self._control.document().lastBlock()
380 length = len(self._prompt)
398 length = len(self._prompt)
381 self._previous_prompt_obj = self._PromptBlock(block, length, number)
399 self._previous_prompt_obj = self._PromptBlock(block, length, number)
382
400
383 # Update continuation prompt to reflect (possibly) new prompt length.
401 # Update continuation prompt to reflect (possibly) new prompt length.
384 self._set_continuation_prompt(
402 self._set_continuation_prompt(
385 self._make_continuation_prompt(self._prompt), html=True)
403 self._make_continuation_prompt(self._prompt), html=True)
386
404
387 def _show_interpreter_prompt_for_reply(self, msg):
405 def _show_interpreter_prompt_for_reply(self, msg):
388 """ Reimplemented for IPython-style prompts.
406 """ Reimplemented for IPython-style prompts.
389 """
407 """
390 # Update the old prompt number if necessary.
408 # Update the old prompt number if necessary.
391 content = msg['content']
409 content = msg['content']
392 # abort replies do not have any keys:
410 # abort replies do not have any keys:
393 if content['status'] == 'aborted':
411 if content['status'] == 'aborted':
394 if self._previous_prompt_obj:
412 if self._previous_prompt_obj:
395 previous_prompt_number = self._previous_prompt_obj.number
413 previous_prompt_number = self._previous_prompt_obj.number
396 else:
414 else:
397 previous_prompt_number = 0
415 previous_prompt_number = 0
398 else:
416 else:
399 previous_prompt_number = content['execution_count']
417 previous_prompt_number = content['execution_count']
400 if self._previous_prompt_obj and \
418 if self._previous_prompt_obj and \
401 self._previous_prompt_obj.number != previous_prompt_number:
419 self._previous_prompt_obj.number != previous_prompt_number:
402 block = self._previous_prompt_obj.block
420 block = self._previous_prompt_obj.block
403
421
404 # Make sure the prompt block has not been erased.
422 # Make sure the prompt block has not been erased.
405 if block.isValid() and block.text():
423 if block.isValid() and block.text():
406
424
407 # Remove the old prompt and insert a new prompt.
425 # Remove the old prompt and insert a new prompt.
408 cursor = QtGui.QTextCursor(block)
426 cursor = QtGui.QTextCursor(block)
409 cursor.movePosition(QtGui.QTextCursor.Right,
427 cursor.movePosition(QtGui.QTextCursor.Right,
410 QtGui.QTextCursor.KeepAnchor,
428 QtGui.QTextCursor.KeepAnchor,
411 self._previous_prompt_obj.length)
429 self._previous_prompt_obj.length)
412 prompt = self._make_in_prompt(previous_prompt_number)
430 prompt = self._make_in_prompt(previous_prompt_number)
413 self._prompt = self._insert_html_fetching_plain_text(
431 self._prompt = self._insert_html_fetching_plain_text(
414 cursor, prompt)
432 cursor, prompt)
415
433
416 # When the HTML is inserted, Qt blows away the syntax
434 # When the HTML is inserted, Qt blows away the syntax
417 # highlighting for the line, so we need to rehighlight it.
435 # highlighting for the line, so we need to rehighlight it.
418 self._highlighter.rehighlightBlock(cursor.block())
436 self._highlighter.rehighlightBlock(cursor.block())
419
437
420 self._previous_prompt_obj = None
438 self._previous_prompt_obj = None
421
439
422 # Show a new prompt with the kernel's estimated prompt number.
440 # Show a new prompt with the kernel's estimated prompt number.
423 self._show_interpreter_prompt(previous_prompt_number + 1)
441 self._show_interpreter_prompt(previous_prompt_number + 1)
424
442
425 #---------------------------------------------------------------------------
443 #---------------------------------------------------------------------------
426 # 'IPythonWidget' interface
444 # 'IPythonWidget' interface
427 #---------------------------------------------------------------------------
445 #---------------------------------------------------------------------------
428
446
429 def set_default_style(self, colors='lightbg'):
447 def set_default_style(self, colors='lightbg'):
430 """ Sets the widget style to the class defaults.
448 """ Sets the widget style to the class defaults.
431
449
432 Parameters
450 Parameters
433 ----------
451 ----------
434 colors : str, optional (default lightbg)
452 colors : str, optional (default lightbg)
435 Whether to use the default IPython light background or dark
453 Whether to use the default IPython light background or dark
436 background or B&W style.
454 background or B&W style.
437 """
455 """
438 colors = colors.lower()
456 colors = colors.lower()
439 if colors=='lightbg':
457 if colors=='lightbg':
440 self.style_sheet = styles.default_light_style_sheet
458 self.style_sheet = styles.default_light_style_sheet
441 self.syntax_style = styles.default_light_syntax_style
459 self.syntax_style = styles.default_light_syntax_style
442 elif colors=='linux':
460 elif colors=='linux':
443 self.style_sheet = styles.default_dark_style_sheet
461 self.style_sheet = styles.default_dark_style_sheet
444 self.syntax_style = styles.default_dark_syntax_style
462 self.syntax_style = styles.default_dark_syntax_style
445 elif colors=='nocolor':
463 elif colors=='nocolor':
446 self.style_sheet = styles.default_bw_style_sheet
464 self.style_sheet = styles.default_bw_style_sheet
447 self.syntax_style = styles.default_bw_syntax_style
465 self.syntax_style = styles.default_bw_syntax_style
448 else:
466 else:
449 raise KeyError("No such color scheme: %s"%colors)
467 raise KeyError("No such color scheme: %s"%colors)
450
468
451 #---------------------------------------------------------------------------
469 #---------------------------------------------------------------------------
452 # 'IPythonWidget' protected interface
470 # 'IPythonWidget' protected interface
453 #---------------------------------------------------------------------------
471 #---------------------------------------------------------------------------
454
472
455 def _edit(self, filename, line=None):
473 def _edit(self, filename, line=None):
456 """ Opens a Python script for editing.
474 """ Opens a Python script for editing.
457
475
458 Parameters
476 Parameters
459 ----------
477 ----------
460 filename : str
478 filename : str
461 A path to a local system file.
479 A path to a local system file.
462
480
463 line : int, optional
481 line : int, optional
464 A line of interest in the file.
482 A line of interest in the file.
465 """
483 """
466 if self.custom_edit:
484 if self.custom_edit:
467 self.custom_edit_requested.emit(filename, line)
485 self.custom_edit_requested.emit(filename, line)
468 elif not self.editor:
486 elif not self.editor:
469 self._append_plain_text('No default editor available.\n'
487 self._append_plain_text('No default editor available.\n'
470 'Specify a GUI text editor in the `IPythonWidget.editor` '
488 'Specify a GUI text editor in the `IPythonWidget.editor` '
471 'configurable to enable the %edit magic')
489 'configurable to enable the %edit magic')
472 else:
490 else:
473 try:
491 try:
474 filename = '"%s"' % filename
492 filename = '"%s"' % filename
475 if line and self.editor_line:
493 if line and self.editor_line:
476 command = self.editor_line.format(filename=filename,
494 command = self.editor_line.format(filename=filename,
477 line=line)
495 line=line)
478 else:
496 else:
479 try:
497 try:
480 command = self.editor.format()
498 command = self.editor.format()
481 except KeyError:
499 except KeyError:
482 command = self.editor.format(filename=filename)
500 command = self.editor.format(filename=filename)
483 else:
501 else:
484 command += ' ' + filename
502 command += ' ' + filename
485 except KeyError:
503 except KeyError:
486 self._append_plain_text('Invalid editor command.\n')
504 self._append_plain_text('Invalid editor command.\n')
487 else:
505 else:
488 try:
506 try:
489 Popen(command, shell=True)
507 Popen(command, shell=True)
490 except OSError:
508 except OSError:
491 msg = 'Opening editor with command "%s" failed.\n'
509 msg = 'Opening editor with command "%s" failed.\n'
492 self._append_plain_text(msg % command)
510 self._append_plain_text(msg % command)
493
511
494 def _make_in_prompt(self, number):
512 def _make_in_prompt(self, number):
495 """ Given a prompt number, returns an HTML In prompt.
513 """ Given a prompt number, returns an HTML In prompt.
496 """
514 """
497 try:
515 try:
498 body = self.in_prompt % number
516 body = self.in_prompt % number
499 except TypeError:
517 except TypeError:
500 # allow in_prompt to leave out number, e.g. '>>> '
518 # allow in_prompt to leave out number, e.g. '>>> '
501 from xml.sax.saxutils import escape
519 from xml.sax.saxutils import escape
502 body = escape(self.in_prompt)
520 body = escape(self.in_prompt)
503 return '<span class="in-prompt">%s</span>' % body
521 return '<span class="in-prompt">%s</span>' % body
504
522
505 def _make_continuation_prompt(self, prompt):
523 def _make_continuation_prompt(self, prompt):
506 """ Given a plain text version of an In prompt, returns an HTML
524 """ Given a plain text version of an In prompt, returns an HTML
507 continuation prompt.
525 continuation prompt.
508 """
526 """
509 end_chars = '...: '
527 end_chars = '...: '
510 space_count = len(prompt.lstrip('\n')) - len(end_chars)
528 space_count = len(prompt.lstrip('\n')) - len(end_chars)
511 body = '&nbsp;' * space_count + end_chars
529 body = '&nbsp;' * space_count + end_chars
512 return '<span class="in-prompt">%s</span>' % body
530 return '<span class="in-prompt">%s</span>' % body
513
531
514 def _make_out_prompt(self, number):
532 def _make_out_prompt(self, number):
515 """ Given a prompt number, returns an HTML Out prompt.
533 """ Given a prompt number, returns an HTML Out prompt.
516 """
534 """
517 try:
535 try:
518 body = self.out_prompt % number
536 body = self.out_prompt % number
519 except TypeError:
537 except TypeError:
520 # allow out_prompt to leave out number, e.g. '<<< '
538 # allow out_prompt to leave out number, e.g. '<<< '
521 from xml.sax.saxutils import escape
539 from xml.sax.saxutils import escape
522 body = escape(self.out_prompt)
540 body = escape(self.out_prompt)
523 return '<span class="out-prompt">%s</span>' % body
541 return '<span class="out-prompt">%s</span>' % body
524
542
525 #------ Payload handlers --------------------------------------------------
543 #------ Payload handlers --------------------------------------------------
526
544
527 # Payload handlers with a generic interface: each takes the opaque payload
545 # Payload handlers with a generic interface: each takes the opaque payload
528 # dict, unpacks it and calls the underlying functions with the necessary
546 # dict, unpacks it and calls the underlying functions with the necessary
529 # arguments.
547 # arguments.
530
548
531 def _handle_payload_edit(self, item):
549 def _handle_payload_edit(self, item):
532 self._edit(item['filename'], item['line_number'])
550 self._edit(item['filename'], item['line_number'])
533
551
534 def _handle_payload_exit(self, item):
552 def _handle_payload_exit(self, item):
535 self._keep_kernel_on_exit = item['keepkernel']
553 self._keep_kernel_on_exit = item['keepkernel']
536 self.exit_requested.emit(self)
554 self.exit_requested.emit(self)
537
555
538 def _handle_payload_next_input(self, item):
556 def _handle_payload_next_input(self, item):
539 self.input_buffer = item['text']
557 self.input_buffer = item['text']
540
558
541 def _handle_payload_page(self, item):
559 def _handle_payload_page(self, item):
542 # Since the plain text widget supports only a very small subset of HTML
560 # Since the plain text widget supports only a very small subset of HTML
543 # and we have no control over the HTML source, we only page HTML
561 # and we have no control over the HTML source, we only page HTML
544 # payloads in the rich text widget.
562 # payloads in the rich text widget.
545 data = item['data']
563 data = item['data']
546 if 'text/html' in data and self.kind == 'rich':
564 if 'text/html' in data and self.kind == 'rich':
547 self._page(data['text/html'], html=True)
565 self._page(data['text/html'], html=True)
548 else:
566 else:
549 self._page(data['text/plain'], html=False)
567 self._page(data['text/plain'], html=False)
550
568
551 #------ Trait change handlers --------------------------------------------
569 #------ Trait change handlers --------------------------------------------
552
570
553 def _style_sheet_changed(self):
571 def _style_sheet_changed(self):
554 """ Set the style sheets of the underlying widgets.
572 """ Set the style sheets of the underlying widgets.
555 """
573 """
556 self.setStyleSheet(self.style_sheet)
574 self.setStyleSheet(self.style_sheet)
557 if self._control is not None:
575 if self._control is not None:
558 self._control.document().setDefaultStyleSheet(self.style_sheet)
576 self._control.document().setDefaultStyleSheet(self.style_sheet)
559 bg_color = self._control.palette().window().color()
577 bg_color = self._control.palette().window().color()
560 self._ansi_processor.set_background_color(bg_color)
578 self._ansi_processor.set_background_color(bg_color)
561
579
562 if self._page_control is not None:
580 if self._page_control is not None:
563 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
581 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
564
582
565
583
566
584
567 def _syntax_style_changed(self):
585 def _syntax_style_changed(self):
568 """ Set the style for the syntax highlighter.
586 """ Set the style for the syntax highlighter.
569 """
587 """
570 if self._highlighter is None:
588 if self._highlighter is None:
571 # ignore premature calls
589 # ignore premature calls
572 return
590 return
573 if self.syntax_style:
591 if self.syntax_style:
574 self._highlighter.set_style(self.syntax_style)
592 self._highlighter.set_style(self.syntax_style)
575 else:
593 else:
576 self._highlighter.set_style_sheet(self.style_sheet)
594 self._highlighter.set_style_sheet(self.style_sheet)
577
595
578 #------ Trait default initializers -----------------------------------------
596 #------ Trait default initializers -----------------------------------------
579
597
580 def _banner_default(self):
598 def _banner_default(self):
581 return "IPython QtConsole {version}\n".format(version=version)
599 return "IPython QtConsole {version}\n".format(version=version)
@@ -1,347 +1,347 b''
1 # Copyright (c) IPython Development Team.
1 # Copyright (c) IPython Development Team.
2 # Distributed under the terms of the Modified BSD License.
2 # Distributed under the terms of the Modified BSD License.
3
3
4 from base64 import decodestring
4 from base64 import decodestring
5 import os
5 import os
6 import re
6 import re
7
7
8 from IPython.external.qt import QtCore, QtGui
8 from IPython.external.qt import QtCore, QtGui
9
9
10 from IPython.lib.latextools import latex_to_png
10 from IPython.lib.latextools import latex_to_png
11 from IPython.utils.path import ensure_dir_exists
11 from IPython.utils.path import ensure_dir_exists
12 from IPython.utils.traitlets import Bool
12 from IPython.utils.traitlets import Bool
13 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
13 from IPython.qt.svg import save_svg, svg_to_clipboard, svg_to_image
14 from .ipython_widget import IPythonWidget
14 from .ipython_widget import IPythonWidget
15
15
16
16
17 class RichIPythonWidget(IPythonWidget):
17 class RichIPythonWidget(IPythonWidget):
18 """ An IPythonWidget that supports rich text, including lists, images, and
18 """ An IPythonWidget that supports rich text, including lists, images, and
19 tables. Note that raw performance will be reduced compared to the plain
19 tables. Note that raw performance will be reduced compared to the plain
20 text version.
20 text version.
21 """
21 """
22
22
23 # RichIPythonWidget protected class variables.
23 # RichIPythonWidget protected class variables.
24 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
24 _payload_source_plot = 'IPython.kernel.zmq.pylab.backend_payload.add_plot_payload'
25 _jpg_supported = Bool(False)
25 _jpg_supported = Bool(False)
26
26
27 # Used to determine whether a given html export attempt has already
27 # Used to determine whether a given html export attempt has already
28 # displayed a warning about being unable to convert a png to svg.
28 # displayed a warning about being unable to convert a png to svg.
29 _svg_warning_displayed = False
29 _svg_warning_displayed = False
30
30
31 #---------------------------------------------------------------------------
31 #---------------------------------------------------------------------------
32 # 'object' interface
32 # 'object' interface
33 #---------------------------------------------------------------------------
33 #---------------------------------------------------------------------------
34
34
35 def __init__(self, *args, **kw):
35 def __init__(self, *args, **kw):
36 """ Create a RichIPythonWidget.
36 """ Create a RichIPythonWidget.
37 """
37 """
38 kw['kind'] = 'rich'
38 kw['kind'] = 'rich'
39 super(RichIPythonWidget, self).__init__(*args, **kw)
39 super(RichIPythonWidget, self).__init__(*args, **kw)
40
40
41 # Configure the ConsoleWidget HTML exporter for our formats.
41 # Configure the ConsoleWidget HTML exporter for our formats.
42 self._html_exporter.image_tag = self._get_image_tag
42 self._html_exporter.image_tag = self._get_image_tag
43
43
44 # Dictionary for resolving document resource names to SVG data.
44 # Dictionary for resolving document resource names to SVG data.
45 self._name_to_svg_map = {}
45 self._name_to_svg_map = {}
46
46
47 # Do we support jpg ?
47 # Do we support jpg ?
48 # it seems that sometime jpg support is a plugin of QT, so try to assume
48 # it seems that sometime jpg support is a plugin of QT, so try to assume
49 # it is not always supported.
49 # it is not always supported.
50 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
50 _supported_format = map(str, QtGui.QImageReader.supportedImageFormats())
51 self._jpg_supported = 'jpeg' in _supported_format
51 self._jpg_supported = 'jpeg' in _supported_format
52
52
53
53
54 #---------------------------------------------------------------------------
54 #---------------------------------------------------------------------------
55 # 'ConsoleWidget' public interface overides
55 # 'ConsoleWidget' public interface overides
56 #---------------------------------------------------------------------------
56 #---------------------------------------------------------------------------
57
57
58 def export_html(self):
58 def export_html(self):
59 """ Shows a dialog to export HTML/XML in various formats.
59 """ Shows a dialog to export HTML/XML in various formats.
60
60
61 Overridden in order to reset the _svg_warning_displayed flag prior
61 Overridden in order to reset the _svg_warning_displayed flag prior
62 to the export running.
62 to the export running.
63 """
63 """
64 self._svg_warning_displayed = False
64 self._svg_warning_displayed = False
65 super(RichIPythonWidget, self).export_html()
65 super(RichIPythonWidget, self).export_html()
66
66
67
67
68 #---------------------------------------------------------------------------
68 #---------------------------------------------------------------------------
69 # 'ConsoleWidget' protected interface
69 # 'ConsoleWidget' protected interface
70 #---------------------------------------------------------------------------
70 #---------------------------------------------------------------------------
71
71
72 def _context_menu_make(self, pos):
72 def _context_menu_make(self, pos):
73 """ Reimplemented to return a custom context menu for images.
73 """ Reimplemented to return a custom context menu for images.
74 """
74 """
75 format = self._control.cursorForPosition(pos).charFormat()
75 format = self._control.cursorForPosition(pos).charFormat()
76 name = format.stringProperty(QtGui.QTextFormat.ImageName)
76 name = format.stringProperty(QtGui.QTextFormat.ImageName)
77 if name:
77 if name:
78 menu = QtGui.QMenu()
78 menu = QtGui.QMenu()
79
79
80 menu.addAction('Copy Image', lambda: self._copy_image(name))
80 menu.addAction('Copy Image', lambda: self._copy_image(name))
81 menu.addAction('Save Image As...', lambda: self._save_image(name))
81 menu.addAction('Save Image As...', lambda: self._save_image(name))
82 menu.addSeparator()
82 menu.addSeparator()
83
83
84 svg = self._name_to_svg_map.get(name, None)
84 svg = self._name_to_svg_map.get(name, None)
85 if svg is not None:
85 if svg is not None:
86 menu.addSeparator()
86 menu.addSeparator()
87 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
87 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
88 menu.addAction('Save SVG As...',
88 menu.addAction('Save SVG As...',
89 lambda: save_svg(svg, self._control))
89 lambda: save_svg(svg, self._control))
90 else:
90 else:
91 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
91 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
92 return menu
92 return menu
93
93
94 #---------------------------------------------------------------------------
94 #---------------------------------------------------------------------------
95 # 'BaseFrontendMixin' abstract interface
95 # 'BaseFrontendMixin' abstract interface
96 #---------------------------------------------------------------------------
96 #---------------------------------------------------------------------------
97 def _pre_image_append(self, msg, prompt_number):
97 def _pre_image_append(self, msg, prompt_number):
98 """ Append the Out[] prompt and make the output nicer
98 """ Append the Out[] prompt and make the output nicer
99
99
100 Shared code for some the following if statement
100 Shared code for some the following if statement
101 """
101 """
102 self.log.debug("execute_result: %s", msg.get('content', ''))
102 self.log.debug("execute_result: %s", msg.get('content', ''))
103 self._append_plain_text(self.output_sep, True)
103 self._append_plain_text(self.output_sep, True)
104 self._append_html(self._make_out_prompt(prompt_number), True)
104 self._append_html(self._make_out_prompt(prompt_number), True)
105 self._append_plain_text('\n', True)
105 self._append_plain_text('\n', True)
106
106
107 def _handle_execute_result(self, msg):
107 def _handle_execute_result(self, msg):
108 """ Overridden to handle rich data types, like SVG.
108 """ Overridden to handle rich data types, like SVG.
109 """
109 """
110 if not self._hidden and self._is_from_this_session(msg):
110 if self.include_output(msg):
111 self.flush_clearoutput()
111 self.flush_clearoutput()
112 content = msg['content']
112 content = msg['content']
113 prompt_number = content.get('execution_count', 0)
113 prompt_number = content.get('execution_count', 0)
114 data = content['data']
114 data = content['data']
115 metadata = msg['content']['metadata']
115 metadata = msg['content']['metadata']
116 if 'image/svg+xml' in data:
116 if 'image/svg+xml' in data:
117 self._pre_image_append(msg, prompt_number)
117 self._pre_image_append(msg, prompt_number)
118 self._append_svg(data['image/svg+xml'], True)
118 self._append_svg(data['image/svg+xml'], True)
119 self._append_html(self.output_sep2, True)
119 self._append_html(self.output_sep2, True)
120 elif 'image/png' in data:
120 elif 'image/png' in data:
121 self._pre_image_append(msg, prompt_number)
121 self._pre_image_append(msg, prompt_number)
122 png = decodestring(data['image/png'].encode('ascii'))
122 png = decodestring(data['image/png'].encode('ascii'))
123 self._append_png(png, True, metadata=metadata.get('image/png', None))
123 self._append_png(png, True, metadata=metadata.get('image/png', None))
124 self._append_html(self.output_sep2, True)
124 self._append_html(self.output_sep2, True)
125 elif 'image/jpeg' in data and self._jpg_supported:
125 elif 'image/jpeg' in data and self._jpg_supported:
126 self._pre_image_append(msg, prompt_number)
126 self._pre_image_append(msg, prompt_number)
127 jpg = decodestring(data['image/jpeg'].encode('ascii'))
127 jpg = decodestring(data['image/jpeg'].encode('ascii'))
128 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
128 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
129 self._append_html(self.output_sep2, True)
129 self._append_html(self.output_sep2, True)
130 elif 'text/latex' in data:
130 elif 'text/latex' in data:
131 self._pre_image_append(msg, prompt_number)
131 self._pre_image_append(msg, prompt_number)
132 latex = data['text/latex'].encode('ascii')
132 latex = data['text/latex'].encode('ascii')
133 # latex_to_png takes care of handling $
133 # latex_to_png takes care of handling $
134 latex = latex.strip('$')
134 latex = latex.strip('$')
135 png = latex_to_png(latex, wrap=True)
135 png = latex_to_png(latex, wrap=True)
136 if png is not None:
136 if png is not None:
137 self._append_png(png, True)
137 self._append_png(png, True)
138 self._append_html(self.output_sep2, True)
138 self._append_html(self.output_sep2, True)
139 else:
139 else:
140 # Print plain text if png can't be generated
140 # Print plain text if png can't be generated
141 return super(RichIPythonWidget, self)._handle_execute_result(msg)
141 return super(RichIPythonWidget, self)._handle_execute_result(msg)
142 else:
142 else:
143 # Default back to the plain text representation.
143 # Default back to the plain text representation.
144 return super(RichIPythonWidget, self)._handle_execute_result(msg)
144 return super(RichIPythonWidget, self)._handle_execute_result(msg)
145
145
146 def _handle_display_data(self, msg):
146 def _handle_display_data(self, msg):
147 """ Overridden to handle rich data types, like SVG.
147 """ Overridden to handle rich data types, like SVG.
148 """
148 """
149 if not self._hidden and self._is_from_this_session(msg):
149 if self.include_output(msg):
150 self.flush_clearoutput()
150 self.flush_clearoutput()
151 data = msg['content']['data']
151 data = msg['content']['data']
152 metadata = msg['content']['metadata']
152 metadata = msg['content']['metadata']
153 # Try to use the svg or html representations.
153 # Try to use the svg or html representations.
154 # FIXME: Is this the right ordering of things to try?
154 # FIXME: Is this the right ordering of things to try?
155 if 'image/svg+xml' in data:
155 if 'image/svg+xml' in data:
156 self.log.debug("display: %s", msg.get('content', ''))
156 self.log.debug("display: %s", msg.get('content', ''))
157 svg = data['image/svg+xml']
157 svg = data['image/svg+xml']
158 self._append_svg(svg, True)
158 self._append_svg(svg, True)
159 elif 'image/png' in data:
159 elif 'image/png' in data:
160 self.log.debug("display: %s", msg.get('content', ''))
160 self.log.debug("display: %s", msg.get('content', ''))
161 # PNG data is base64 encoded as it passes over the network
161 # PNG data is base64 encoded as it passes over the network
162 # in a JSON structure so we decode it.
162 # in a JSON structure so we decode it.
163 png = decodestring(data['image/png'].encode('ascii'))
163 png = decodestring(data['image/png'].encode('ascii'))
164 self._append_png(png, True, metadata=metadata.get('image/png', None))
164 self._append_png(png, True, metadata=metadata.get('image/png', None))
165 elif 'image/jpeg' in data and self._jpg_supported:
165 elif 'image/jpeg' in data and self._jpg_supported:
166 self.log.debug("display: %s", msg.get('content', ''))
166 self.log.debug("display: %s", msg.get('content', ''))
167 jpg = decodestring(data['image/jpeg'].encode('ascii'))
167 jpg = decodestring(data['image/jpeg'].encode('ascii'))
168 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
168 self._append_jpg(jpg, True, metadata=metadata.get('image/jpeg', None))
169 else:
169 else:
170 # Default back to the plain text representation.
170 # Default back to the plain text representation.
171 return super(RichIPythonWidget, self)._handle_display_data(msg)
171 return super(RichIPythonWidget, self)._handle_display_data(msg)
172
172
173 #---------------------------------------------------------------------------
173 #---------------------------------------------------------------------------
174 # 'RichIPythonWidget' protected interface
174 # 'RichIPythonWidget' protected interface
175 #---------------------------------------------------------------------------
175 #---------------------------------------------------------------------------
176
176
177 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
177 def _append_jpg(self, jpg, before_prompt=False, metadata=None):
178 """ Append raw JPG data to the widget."""
178 """ Append raw JPG data to the widget."""
179 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
179 self._append_custom(self._insert_jpg, jpg, before_prompt, metadata=metadata)
180
180
181 def _append_png(self, png, before_prompt=False, metadata=None):
181 def _append_png(self, png, before_prompt=False, metadata=None):
182 """ Append raw PNG data to the widget.
182 """ Append raw PNG data to the widget.
183 """
183 """
184 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
184 self._append_custom(self._insert_png, png, before_prompt, metadata=metadata)
185
185
186 def _append_svg(self, svg, before_prompt=False):
186 def _append_svg(self, svg, before_prompt=False):
187 """ Append raw SVG data to the widget.
187 """ Append raw SVG data to the widget.
188 """
188 """
189 self._append_custom(self._insert_svg, svg, before_prompt)
189 self._append_custom(self._insert_svg, svg, before_prompt)
190
190
191 def _add_image(self, image):
191 def _add_image(self, image):
192 """ Adds the specified QImage to the document and returns a
192 """ Adds the specified QImage to the document and returns a
193 QTextImageFormat that references it.
193 QTextImageFormat that references it.
194 """
194 """
195 document = self._control.document()
195 document = self._control.document()
196 name = str(image.cacheKey())
196 name = str(image.cacheKey())
197 document.addResource(QtGui.QTextDocument.ImageResource,
197 document.addResource(QtGui.QTextDocument.ImageResource,
198 QtCore.QUrl(name), image)
198 QtCore.QUrl(name), image)
199 format = QtGui.QTextImageFormat()
199 format = QtGui.QTextImageFormat()
200 format.setName(name)
200 format.setName(name)
201 return format
201 return format
202
202
203 def _copy_image(self, name):
203 def _copy_image(self, name):
204 """ Copies the ImageResource with 'name' to the clipboard.
204 """ Copies the ImageResource with 'name' to the clipboard.
205 """
205 """
206 image = self._get_image(name)
206 image = self._get_image(name)
207 QtGui.QApplication.clipboard().setImage(image)
207 QtGui.QApplication.clipboard().setImage(image)
208
208
209 def _get_image(self, name):
209 def _get_image(self, name):
210 """ Returns the QImage stored as the ImageResource with 'name'.
210 """ Returns the QImage stored as the ImageResource with 'name'.
211 """
211 """
212 document = self._control.document()
212 document = self._control.document()
213 image = document.resource(QtGui.QTextDocument.ImageResource,
213 image = document.resource(QtGui.QTextDocument.ImageResource,
214 QtCore.QUrl(name))
214 QtCore.QUrl(name))
215 return image
215 return image
216
216
217 def _get_image_tag(self, match, path = None, format = "png"):
217 def _get_image_tag(self, match, path = None, format = "png"):
218 """ Return (X)HTML mark-up for the image-tag given by match.
218 """ Return (X)HTML mark-up for the image-tag given by match.
219
219
220 Parameters
220 Parameters
221 ----------
221 ----------
222 match : re.SRE_Match
222 match : re.SRE_Match
223 A match to an HTML image tag as exported by Qt, with
223 A match to an HTML image tag as exported by Qt, with
224 match.group("Name") containing the matched image ID.
224 match.group("Name") containing the matched image ID.
225
225
226 path : string|None, optional [default None]
226 path : string|None, optional [default None]
227 If not None, specifies a path to which supporting files may be
227 If not None, specifies a path to which supporting files may be
228 written (e.g., for linked images). If None, all images are to be
228 written (e.g., for linked images). If None, all images are to be
229 included inline.
229 included inline.
230
230
231 format : "png"|"svg"|"jpg", optional [default "png"]
231 format : "png"|"svg"|"jpg", optional [default "png"]
232 Format for returned or referenced images.
232 Format for returned or referenced images.
233 """
233 """
234 if format in ("png","jpg"):
234 if format in ("png","jpg"):
235 try:
235 try:
236 image = self._get_image(match.group("name"))
236 image = self._get_image(match.group("name"))
237 except KeyError:
237 except KeyError:
238 return "<b>Couldn't find image %s</b>" % match.group("name")
238 return "<b>Couldn't find image %s</b>" % match.group("name")
239
239
240 if path is not None:
240 if path is not None:
241 ensure_dir_exists(path)
241 ensure_dir_exists(path)
242 relpath = os.path.basename(path)
242 relpath = os.path.basename(path)
243 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
243 if image.save("%s/qt_img%s.%s" % (path, match.group("name"), format),
244 "PNG"):
244 "PNG"):
245 return '<img src="%s/qt_img%s.%s">' % (relpath,
245 return '<img src="%s/qt_img%s.%s">' % (relpath,
246 match.group("name"),format)
246 match.group("name"),format)
247 else:
247 else:
248 return "<b>Couldn't save image!</b>"
248 return "<b>Couldn't save image!</b>"
249 else:
249 else:
250 ba = QtCore.QByteArray()
250 ba = QtCore.QByteArray()
251 buffer_ = QtCore.QBuffer(ba)
251 buffer_ = QtCore.QBuffer(ba)
252 buffer_.open(QtCore.QIODevice.WriteOnly)
252 buffer_.open(QtCore.QIODevice.WriteOnly)
253 image.save(buffer_, format.upper())
253 image.save(buffer_, format.upper())
254 buffer_.close()
254 buffer_.close()
255 return '<img src="data:image/%s;base64,\n%s\n" />' % (
255 return '<img src="data:image/%s;base64,\n%s\n" />' % (
256 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
256 format,re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
257
257
258 elif format == "svg":
258 elif format == "svg":
259 try:
259 try:
260 svg = str(self._name_to_svg_map[match.group("name")])
260 svg = str(self._name_to_svg_map[match.group("name")])
261 except KeyError:
261 except KeyError:
262 if not self._svg_warning_displayed:
262 if not self._svg_warning_displayed:
263 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
263 QtGui.QMessageBox.warning(self, 'Error converting PNG to SVG.',
264 'Cannot convert PNG images to SVG, export with PNG figures instead. '
264 'Cannot convert PNG images to SVG, export with PNG figures instead. '
265 'If you want to export matplotlib figures as SVG, add '
265 'If you want to export matplotlib figures as SVG, add '
266 'to your ipython config:\n\n'
266 'to your ipython config:\n\n'
267 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
267 '\tc.InlineBackend.figure_format = \'svg\'\n\n'
268 'And regenerate the figures.',
268 'And regenerate the figures.',
269 QtGui.QMessageBox.Ok)
269 QtGui.QMessageBox.Ok)
270 self._svg_warning_displayed = True
270 self._svg_warning_displayed = True
271 return ("<b>Cannot convert PNG images to SVG.</b> "
271 return ("<b>Cannot convert PNG images to SVG.</b> "
272 "You must export this session with PNG images. "
272 "You must export this session with PNG images. "
273 "If you want to export matplotlib figures as SVG, add to your config "
273 "If you want to export matplotlib figures as SVG, add to your config "
274 "<span>c.InlineBackend.figure_format = 'svg'</span> "
274 "<span>c.InlineBackend.figure_format = 'svg'</span> "
275 "and regenerate the figures.")
275 "and regenerate the figures.")
276
276
277 # Not currently checking path, because it's tricky to find a
277 # Not currently checking path, because it's tricky to find a
278 # cross-browser way to embed external SVG images (e.g., via
278 # cross-browser way to embed external SVG images (e.g., via
279 # object or embed tags).
279 # object or embed tags).
280
280
281 # Chop stand-alone header from matplotlib SVG
281 # Chop stand-alone header from matplotlib SVG
282 offset = svg.find("<svg")
282 offset = svg.find("<svg")
283 assert(offset > -1)
283 assert(offset > -1)
284
284
285 return svg[offset:]
285 return svg[offset:]
286
286
287 else:
287 else:
288 return '<b>Unrecognized image format</b>'
288 return '<b>Unrecognized image format</b>'
289
289
290 def _insert_jpg(self, cursor, jpg, metadata=None):
290 def _insert_jpg(self, cursor, jpg, metadata=None):
291 """ Insert raw PNG data into the widget."""
291 """ Insert raw PNG data into the widget."""
292 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
292 self._insert_img(cursor, jpg, 'jpg', metadata=metadata)
293
293
294 def _insert_png(self, cursor, png, metadata=None):
294 def _insert_png(self, cursor, png, metadata=None):
295 """ Insert raw PNG data into the widget.
295 """ Insert raw PNG data into the widget.
296 """
296 """
297 self._insert_img(cursor, png, 'png', metadata=metadata)
297 self._insert_img(cursor, png, 'png', metadata=metadata)
298
298
299 def _insert_img(self, cursor, img, fmt, metadata=None):
299 def _insert_img(self, cursor, img, fmt, metadata=None):
300 """ insert a raw image, jpg or png """
300 """ insert a raw image, jpg or png """
301 if metadata:
301 if metadata:
302 width = metadata.get('width', None)
302 width = metadata.get('width', None)
303 height = metadata.get('height', None)
303 height = metadata.get('height', None)
304 else:
304 else:
305 width = height = None
305 width = height = None
306 try:
306 try:
307 image = QtGui.QImage()
307 image = QtGui.QImage()
308 image.loadFromData(img, fmt.upper())
308 image.loadFromData(img, fmt.upper())
309 if width and height:
309 if width and height:
310 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
310 image = image.scaled(width, height, transformMode=QtCore.Qt.SmoothTransformation)
311 elif width and not height:
311 elif width and not height:
312 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
312 image = image.scaledToWidth(width, transformMode=QtCore.Qt.SmoothTransformation)
313 elif height and not width:
313 elif height and not width:
314 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
314 image = image.scaledToHeight(height, transformMode=QtCore.Qt.SmoothTransformation)
315 except ValueError:
315 except ValueError:
316 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
316 self._insert_plain_text(cursor, 'Received invalid %s data.'%fmt)
317 else:
317 else:
318 format = self._add_image(image)
318 format = self._add_image(image)
319 cursor.insertBlock()
319 cursor.insertBlock()
320 cursor.insertImage(format)
320 cursor.insertImage(format)
321 cursor.insertBlock()
321 cursor.insertBlock()
322
322
323 def _insert_svg(self, cursor, svg):
323 def _insert_svg(self, cursor, svg):
324 """ Insert raw SVG data into the widet.
324 """ Insert raw SVG data into the widet.
325 """
325 """
326 try:
326 try:
327 image = svg_to_image(svg)
327 image = svg_to_image(svg)
328 except ValueError:
328 except ValueError:
329 self._insert_plain_text(cursor, 'Received invalid SVG data.')
329 self._insert_plain_text(cursor, 'Received invalid SVG data.')
330 else:
330 else:
331 format = self._add_image(image)
331 format = self._add_image(image)
332 self._name_to_svg_map[format.name()] = svg
332 self._name_to_svg_map[format.name()] = svg
333 cursor.insertBlock()
333 cursor.insertBlock()
334 cursor.insertImage(format)
334 cursor.insertImage(format)
335 cursor.insertBlock()
335 cursor.insertBlock()
336
336
337 def _save_image(self, name, format='PNG'):
337 def _save_image(self, name, format='PNG'):
338 """ Shows a save dialog for the ImageResource with 'name'.
338 """ Shows a save dialog for the ImageResource with 'name'.
339 """
339 """
340 dialog = QtGui.QFileDialog(self._control, 'Save Image')
340 dialog = QtGui.QFileDialog(self._control, 'Save Image')
341 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
341 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
342 dialog.setDefaultSuffix(format.lower())
342 dialog.setDefaultSuffix(format.lower())
343 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
343 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
344 if dialog.exec_():
344 if dialog.exec_():
345 filename = dialog.selectedFiles()[0]
345 filename = dialog.selectedFiles()[0]
346 image = self._get_image(name)
346 image = self._get_image(name)
347 image.save(filename, format)
347 image.save(filename, format)
@@ -1,528 +1,570 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """terminal client to the IPython kernel"""
2 """terminal client to the IPython kernel"""
3
3
4 # Copyright (c) IPython Development Team.
4 # Copyright (c) IPython Development Team.
5 # Distributed under the terms of the Modified BSD License.
5 # Distributed under the terms of the Modified BSD License.
6
6
7 from __future__ import print_function
7 from __future__ import print_function
8
8
9 import base64
9 import base64
10 import bdb
10 import bdb
11 import signal
11 import signal
12 import os
12 import os
13 import sys
13 import sys
14 import time
14 import time
15 import subprocess
15 import subprocess
16 from getpass import getpass
16 from getpass import getpass
17 from io import BytesIO
17 from io import BytesIO
18
18
19 try:
19 try:
20 from queue import Empty # Py 3
20 from queue import Empty # Py 3
21 except ImportError:
21 except ImportError:
22 from Queue import Empty # Py 2
22 from Queue import Empty # Py 2
23
23
24 from IPython.core import page
24 from IPython.core import page
25 from IPython.core import release
25 from IPython.core import release
26 from IPython.utils.warn import warn, error
26 from IPython.utils.warn import warn, error
27 from IPython.utils import io
27 from IPython.utils import io
28 from IPython.utils.py3compat import string_types, input
28 from IPython.utils.py3compat import string_types, input
29 from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float
29 from IPython.utils.traitlets import List, Enum, Any, Instance, Unicode, Float, Bool
30 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
30 from IPython.utils.tempdir import NamedFileInTemporaryDirectory
31
31
32 from IPython.terminal.interactiveshell import TerminalInteractiveShell
32 from IPython.terminal.interactiveshell import TerminalInteractiveShell
33 from IPython.terminal.console.completer import ZMQCompleter
33 from IPython.terminal.console.completer import ZMQCompleter
34
34
35
35
36 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
36 class ZMQTerminalInteractiveShell(TerminalInteractiveShell):
37 """A subclass of TerminalInteractiveShell that uses the 0MQ kernel"""
37 """A subclass of TerminalInteractiveShell that uses the 0MQ kernel"""
38 _executing = False
38 _executing = False
39 _execution_state = Unicode('')
39 _execution_state = Unicode('')
40 _pending_clearoutput = False
40 _pending_clearoutput = False
41 kernel_banner = Unicode('')
41 kernel_banner = Unicode('')
42 kernel_timeout = Float(60, config=True,
42 kernel_timeout = Float(60, config=True,
43 help="""Timeout for giving up on a kernel (in seconds).
43 help="""Timeout for giving up on a kernel (in seconds).
44
44
45 On first connect and restart, the console tests whether the
45 On first connect and restart, the console tests whether the
46 kernel is running and responsive by sending kernel_info_requests.
46 kernel is running and responsive by sending kernel_info_requests.
47 This sets the timeout in seconds for how long the kernel can take
47 This sets the timeout in seconds for how long the kernel can take
48 before being presumed dead.
48 before being presumed dead.
49 """
49 """
50 )
50 )
51
51
52 image_handler = Enum(('PIL', 'stream', 'tempfile', 'callable'),
52 image_handler = Enum(('PIL', 'stream', 'tempfile', 'callable'),
53 config=True, help=
53 config=True, help=
54 """
54 """
55 Handler for image type output. This is useful, for example,
55 Handler for image type output. This is useful, for example,
56 when connecting to the kernel in which pylab inline backend is
56 when connecting to the kernel in which pylab inline backend is
57 activated. There are four handlers defined. 'PIL': Use
57 activated. There are four handlers defined. 'PIL': Use
58 Python Imaging Library to popup image; 'stream': Use an
58 Python Imaging Library to popup image; 'stream': Use an
59 external program to show the image. Image will be fed into
59 external program to show the image. Image will be fed into
60 the STDIN of the program. You will need to configure
60 the STDIN of the program. You will need to configure
61 `stream_image_handler`; 'tempfile': Use an external program to
61 `stream_image_handler`; 'tempfile': Use an external program to
62 show the image. Image will be saved in a temporally file and
62 show the image. Image will be saved in a temporally file and
63 the program is called with the temporally file. You will need
63 the program is called with the temporally file. You will need
64 to configure `tempfile_image_handler`; 'callable': You can set
64 to configure `tempfile_image_handler`; 'callable': You can set
65 any Python callable which is called with the image data. You
65 any Python callable which is called with the image data. You
66 will need to configure `callable_image_handler`.
66 will need to configure `callable_image_handler`.
67 """
67 """
68 )
68 )
69
69
70 stream_image_handler = List(config=True, help=
70 stream_image_handler = List(config=True, help=
71 """
71 """
72 Command to invoke an image viewer program when you are using
72 Command to invoke an image viewer program when you are using
73 'stream' image handler. This option is a list of string where
73 'stream' image handler. This option is a list of string where
74 the first element is the command itself and reminders are the
74 the first element is the command itself and reminders are the
75 options for the command. Raw image data is given as STDIN to
75 options for the command. Raw image data is given as STDIN to
76 the program.
76 the program.
77 """
77 """
78 )
78 )
79
79
80 tempfile_image_handler = List(config=True, help=
80 tempfile_image_handler = List(config=True, help=
81 """
81 """
82 Command to invoke an image viewer program when you are using
82 Command to invoke an image viewer program when you are using
83 'tempfile' image handler. This option is a list of string
83 'tempfile' image handler. This option is a list of string
84 where the first element is the command itself and reminders
84 where the first element is the command itself and reminders
85 are the options for the command. You can use {file} and
85 are the options for the command. You can use {file} and
86 {format} in the string to represent the location of the
86 {format} in the string to represent the location of the
87 generated image file and image format.
87 generated image file and image format.
88 """
88 """
89 )
89 )
90
90
91 callable_image_handler = Any(config=True, help=
91 callable_image_handler = Any(config=True, help=
92 """
92 """
93 Callable object called via 'callable' image handler with one
93 Callable object called via 'callable' image handler with one
94 argument, `data`, which is `msg["content"]["data"]` where
94 argument, `data`, which is `msg["content"]["data"]` where
95 `msg` is the message from iopub channel. For exmaple, you can
95 `msg` is the message from iopub channel. For exmaple, you can
96 find base64 encoded PNG data as `data['image/png']`.
96 find base64 encoded PNG data as `data['image/png']`.
97 """
97 """
98 )
98 )
99
99
100 mime_preference = List(
100 mime_preference = List(
101 default_value=['image/png', 'image/jpeg', 'image/svg+xml'],
101 default_value=['image/png', 'image/jpeg', 'image/svg+xml'],
102 config=True, allow_none=False, help=
102 config=True, allow_none=False, help=
103 """
103 """
104 Preferred object representation MIME type in order. First
104 Preferred object representation MIME type in order. First
105 matched MIME type will be used.
105 matched MIME type will be used.
106 """
106 """
107 )
107 )
108
108
109 manager = Instance('IPython.kernel.KernelManager')
109 manager = Instance('IPython.kernel.KernelManager')
110 client = Instance('IPython.kernel.KernelClient')
110 client = Instance('IPython.kernel.KernelClient')
111 def _client_changed(self, name, old, new):
111 def _client_changed(self, name, old, new):
112 self.session_id = new.session.session
112 self.session_id = new.session.session
113 session_id = Unicode()
113 session_id = Unicode()
114
114
115 def init_completer(self):
115 def init_completer(self):
116 """Initialize the completion machinery.
116 """Initialize the completion machinery.
117
117
118 This creates completion machinery that can be used by client code,
118 This creates completion machinery that can be used by client code,
119 either interactively in-process (typically triggered by the readline
119 either interactively in-process (typically triggered by the readline
120 library), programmatically (such as in test suites) or out-of-process
120 library), programmatically (such as in test suites) or out-of-process
121 (typically over the network by remote frontends).
121 (typically over the network by remote frontends).
122 """
122 """
123 from IPython.core.completerlib import (module_completer,
123 from IPython.core.completerlib import (module_completer,
124 magic_run_completer, cd_completer)
124 magic_run_completer, cd_completer)
125
125
126 self.Completer = ZMQCompleter(self, self.client, config=self.config)
126 self.Completer = ZMQCompleter(self, self.client, config=self.config)
127
127
128
128
129 self.set_hook('complete_command', module_completer, str_key = 'import')
129 self.set_hook('complete_command', module_completer, str_key = 'import')
130 self.set_hook('complete_command', module_completer, str_key = 'from')
130 self.set_hook('complete_command', module_completer, str_key = 'from')
131 self.set_hook('complete_command', magic_run_completer, str_key = '%run')
131 self.set_hook('complete_command', magic_run_completer, str_key = '%run')
132 self.set_hook('complete_command', cd_completer, str_key = '%cd')
132 self.set_hook('complete_command', cd_completer, str_key = '%cd')
133
133
134 # Only configure readline if we truly are using readline. IPython can
134 # Only configure readline if we truly are using readline. IPython can
135 # do tab-completion over the network, in GUIs, etc, where readline
135 # do tab-completion over the network, in GUIs, etc, where readline
136 # itself may be absent
136 # itself may be absent
137 if self.has_readline:
137 if self.has_readline:
138 self.set_readline_completer()
138 self.set_readline_completer()
139
139
140 def run_cell(self, cell, store_history=True):
140 def run_cell(self, cell, store_history=True):
141 """Run a complete IPython cell.
141 """Run a complete IPython cell.
142
142
143 Parameters
143 Parameters
144 ----------
144 ----------
145 cell : str
145 cell : str
146 The code (including IPython code such as %magic functions) to run.
146 The code (including IPython code such as %magic functions) to run.
147 store_history : bool
147 store_history : bool
148 If True, the raw and translated cell will be stored in IPython's
148 If True, the raw and translated cell will be stored in IPython's
149 history. For user code calling back into IPython's machinery, this
149 history. For user code calling back into IPython's machinery, this
150 should be set to False.
150 should be set to False.
151 """
151 """
152 if (not cell) or cell.isspace():
152 if (not cell) or cell.isspace():
153 # pressing enter flushes any pending display
153 # pressing enter flushes any pending display
154 self.handle_iopub()
154 self.handle_iopub()
155 return
155 return
156
156
157 # flush stale replies, which could have been ignored, due to missed heartbeats
157 # flush stale replies, which could have been ignored, due to missed heartbeats
158 while self.client.shell_channel.msg_ready():
158 while self.client.shell_channel.msg_ready():
159 self.client.shell_channel.get_msg()
159 self.client.shell_channel.get_msg()
160 # shell_channel.execute takes 'hidden', which is the inverse of store_hist
160 # shell_channel.execute takes 'hidden', which is the inverse of store_hist
161 msg_id = self.client.shell_channel.execute(cell, not store_history)
161 msg_id = self.client.shell_channel.execute(cell, not store_history)
162
162
163 # first thing is wait for any side effects (output, stdin, etc.)
163 # first thing is wait for any side effects (output, stdin, etc.)
164 self._executing = True
164 self._executing = True
165 self._execution_state = "busy"
165 self._execution_state = "busy"
166 while self._execution_state != 'idle' and self.client.is_alive():
166 while self._execution_state != 'idle' and self.client.is_alive():
167 try:
167 try:
168 self.handle_input_request(msg_id, timeout=0.05)
168 self.handle_input_request(msg_id, timeout=0.05)
169 except Empty:
169 except Empty:
170 # display intermediate print statements, etc.
170 # display intermediate print statements, etc.
171 self.handle_iopub(msg_id)
171 self.handle_iopub(msg_id)
172
172
173 # after all of that is done, wait for the execute reply
173 # after all of that is done, wait for the execute reply
174 while self.client.is_alive():
174 while self.client.is_alive():
175 try:
175 try:
176 self.handle_execute_reply(msg_id, timeout=0.05)
176 self.handle_execute_reply(msg_id, timeout=0.05)
177 except Empty:
177 except Empty:
178 pass
178 pass
179 else:
179 else:
180 break
180 break
181 self._executing = False
181 self._executing = False
182
182
183 #-----------------
183 #-----------------
184 # message handlers
184 # message handlers
185 #-----------------
185 #-----------------
186
186
187 def handle_execute_reply(self, msg_id, timeout=None):
187 def handle_execute_reply(self, msg_id, timeout=None):
188 msg = self.client.shell_channel.get_msg(block=False, timeout=timeout)
188 msg = self.client.shell_channel.get_msg(block=False, timeout=timeout)
189 if msg["parent_header"].get("msg_id", None) == msg_id:
189 if msg["parent_header"].get("msg_id", None) == msg_id:
190
190
191 self.handle_iopub(msg_id)
191 self.handle_iopub(msg_id)
192
192
193 content = msg["content"]
193 content = msg["content"]
194 status = content['status']
194 status = content['status']
195
195
196 if status == 'aborted':
196 if status == 'aborted':
197 self.write('Aborted\n')
197 self.write('Aborted\n')
198 return
198 return
199 elif status == 'ok':
199 elif status == 'ok':
200 # handle payloads
200 # handle payloads
201 for item in content["payload"]:
201 for item in content["payload"]:
202 source = item['source']
202 source = item['source']
203 if source == 'page':
203 if source == 'page':
204 page.page(item['data']['text/plain'])
204 page.page(item['data']['text/plain'])
205 elif source == 'set_next_input':
205 elif source == 'set_next_input':
206 self.set_next_input(item['text'])
206 self.set_next_input(item['text'])
207 elif source == 'ask_exit':
207 elif source == 'ask_exit':
208 self.ask_exit()
208 self.ask_exit()
209
209
210 elif status == 'error':
210 elif status == 'error':
211 for frame in content["traceback"]:
211 for frame in content["traceback"]:
212 print(frame, file=io.stderr)
212 print(frame, file=io.stderr)
213
213
214 self.execution_count = int(content["execution_count"] + 1)
214 self.execution_count = int(content["execution_count"] + 1)
215
215
216
216 include_other_output = Bool(False, config=True,
217 help="""Whether to include output from clients
218 other than this one sharing the same kernel.
219
220 Outputs are not displayed until enter is pressed.
221 """
222 )
223 other_output_prefix = Unicode("[remote] ", config=True,
224 help="""Prefix to add to outputs coming from clients other than this one.
225
226 Only relevant if include_other_output is True.
227 """
228 )
229
230 def from_here(self, msg):
231 """Return whether a message is from this session"""
232 return msg['parent_header'].get("session", self.session_id) == self.session_id
233
234 def include_output(self, msg):
235 """Return whether we should include a given output message"""
236 from_here = self.from_here(msg)
237 if msg['msg_type'] == 'execute_input':
238 # only echo inputs not from here
239 return self.include_other_output and not from_here
240
241 if self.include_other_output:
242 return True
243 else:
244 return from_here
245
217 def handle_iopub(self, msg_id=''):
246 def handle_iopub(self, msg_id=''):
218 """Process messages on the IOPub channel
247 """Process messages on the IOPub channel
219
248
220 This method consumes and processes messages on the IOPub channel,
249 This method consumes and processes messages on the IOPub channel,
221 such as stdout, stderr, execute_result and status.
250 such as stdout, stderr, execute_result and status.
222
251
223 It only displays output that is caused by this session.
252 It only displays output that is caused by this session.
224 """
253 """
225 while self.client.iopub_channel.msg_ready():
254 while self.client.iopub_channel.msg_ready():
226 sub_msg = self.client.iopub_channel.get_msg()
255 sub_msg = self.client.iopub_channel.get_msg()
227 msg_type = sub_msg['header']['msg_type']
256 msg_type = sub_msg['header']['msg_type']
228 parent = sub_msg["parent_header"]
257 parent = sub_msg["parent_header"]
229
258
230 if parent.get("session", self.session_id) == self.session_id:
259 if self.include_output(sub_msg):
231 if msg_type == 'status':
260 if msg_type == 'status':
232 self._execution_state = sub_msg["content"]["execution_state"]
261 self._execution_state = sub_msg["content"]["execution_state"]
233 elif msg_type == 'stream':
262 elif msg_type == 'stream':
234 if sub_msg["content"]["name"] == "stdout":
263 if sub_msg["content"]["name"] == "stdout":
235 if self._pending_clearoutput:
264 if self._pending_clearoutput:
236 print("\r", file=io.stdout, end="")
265 print("\r", file=io.stdout, end="")
237 self._pending_clearoutput = False
266 self._pending_clearoutput = False
238 print(sub_msg["content"]["text"], file=io.stdout, end="")
267 print(sub_msg["content"]["text"], file=io.stdout, end="")
239 io.stdout.flush()
268 io.stdout.flush()
240 elif sub_msg["content"]["name"] == "stderr":
269 elif sub_msg["content"]["name"] == "stderr":
241 if self._pending_clearoutput:
270 if self._pending_clearoutput:
242 print("\r", file=io.stderr, end="")
271 print("\r", file=io.stderr, end="")
243 self._pending_clearoutput = False
272 self._pending_clearoutput = False
244 print(sub_msg["content"]["text"], file=io.stderr, end="")
273 print(sub_msg["content"]["text"], file=io.stderr, end="")
245 io.stderr.flush()
274 io.stderr.flush()
246
275
247 elif msg_type == 'execute_result':
276 elif msg_type == 'execute_result':
248 if self._pending_clearoutput:
277 if self._pending_clearoutput:
249 print("\r", file=io.stdout, end="")
278 print("\r", file=io.stdout, end="")
250 self._pending_clearoutput = False
279 self._pending_clearoutput = False
251 self.execution_count = int(sub_msg["content"]["execution_count"])
280 self.execution_count = int(sub_msg["content"]["execution_count"])
281 if not self.from_here(sub_msg):
282 sys.stdout.write(self.other_output_prefix)
252 format_dict = sub_msg["content"]["data"]
283 format_dict = sub_msg["content"]["data"]
253 self.handle_rich_data(format_dict)
284 self.handle_rich_data(format_dict)
285
254 # taken from DisplayHook.__call__:
286 # taken from DisplayHook.__call__:
255 hook = self.displayhook
287 hook = self.displayhook
256 hook.start_displayhook()
288 hook.start_displayhook()
257 hook.write_output_prompt()
289 hook.write_output_prompt()
258 hook.write_format_data(format_dict)
290 hook.write_format_data(format_dict)
259 hook.log_output(format_dict)
291 hook.log_output(format_dict)
260 hook.finish_displayhook()
292 hook.finish_displayhook()
261
293
262 elif msg_type == 'display_data':
294 elif msg_type == 'display_data':
263 data = sub_msg["content"]["data"]
295 data = sub_msg["content"]["data"]
264 handled = self.handle_rich_data(data)
296 handled = self.handle_rich_data(data)
265 if not handled:
297 if not handled:
298 if not self.from_here(sub_msg):
299 sys.stdout.write(self.other_output_prefix)
266 # if it was an image, we handled it by now
300 # if it was an image, we handled it by now
267 if 'text/plain' in data:
301 if 'text/plain' in data:
268 print(data['text/plain'])
302 print(data['text/plain'])
269
303
304 elif msg_type == 'execute_input':
305 content = sub_msg['content']
306 self.execution_count = content['execution_count']
307 if not self.from_here(sub_msg):
308 sys.stdout.write(self.other_output_prefix)
309 sys.stdout.write(self.prompt_manager.render('in'))
310 sys.stdout.write(content['code'])
311
270 elif msg_type == 'clear_output':
312 elif msg_type == 'clear_output':
271 if sub_msg["content"]["wait"]:
313 if sub_msg["content"]["wait"]:
272 self._pending_clearoutput = True
314 self._pending_clearoutput = True
273 else:
315 else:
274 print("\r", file=io.stdout, end="")
316 print("\r", file=io.stdout, end="")
275
317
276 _imagemime = {
318 _imagemime = {
277 'image/png': 'png',
319 'image/png': 'png',
278 'image/jpeg': 'jpeg',
320 'image/jpeg': 'jpeg',
279 'image/svg+xml': 'svg',
321 'image/svg+xml': 'svg',
280 }
322 }
281
323
282 def handle_rich_data(self, data):
324 def handle_rich_data(self, data):
283 for mime in self.mime_preference:
325 for mime in self.mime_preference:
284 if mime in data and mime in self._imagemime:
326 if mime in data and mime in self._imagemime:
285 self.handle_image(data, mime)
327 self.handle_image(data, mime)
286 return True
328 return True
287
329
288 def handle_image(self, data, mime):
330 def handle_image(self, data, mime):
289 handler = getattr(
331 handler = getattr(
290 self, 'handle_image_{0}'.format(self.image_handler), None)
332 self, 'handle_image_{0}'.format(self.image_handler), None)
291 if handler:
333 if handler:
292 handler(data, mime)
334 handler(data, mime)
293
335
294 def handle_image_PIL(self, data, mime):
336 def handle_image_PIL(self, data, mime):
295 if mime not in ('image/png', 'image/jpeg'):
337 if mime not in ('image/png', 'image/jpeg'):
296 return
338 return
297 import PIL.Image
339 import PIL.Image
298 raw = base64.decodestring(data[mime].encode('ascii'))
340 raw = base64.decodestring(data[mime].encode('ascii'))
299 img = PIL.Image.open(BytesIO(raw))
341 img = PIL.Image.open(BytesIO(raw))
300 img.show()
342 img.show()
301
343
302 def handle_image_stream(self, data, mime):
344 def handle_image_stream(self, data, mime):
303 raw = base64.decodestring(data[mime].encode('ascii'))
345 raw = base64.decodestring(data[mime].encode('ascii'))
304 imageformat = self._imagemime[mime]
346 imageformat = self._imagemime[mime]
305 fmt = dict(format=imageformat)
347 fmt = dict(format=imageformat)
306 args = [s.format(**fmt) for s in self.stream_image_handler]
348 args = [s.format(**fmt) for s in self.stream_image_handler]
307 with open(os.devnull, 'w') as devnull:
349 with open(os.devnull, 'w') as devnull:
308 proc = subprocess.Popen(
350 proc = subprocess.Popen(
309 args, stdin=subprocess.PIPE,
351 args, stdin=subprocess.PIPE,
310 stdout=devnull, stderr=devnull)
352 stdout=devnull, stderr=devnull)
311 proc.communicate(raw)
353 proc.communicate(raw)
312
354
313 def handle_image_tempfile(self, data, mime):
355 def handle_image_tempfile(self, data, mime):
314 raw = base64.decodestring(data[mime].encode('ascii'))
356 raw = base64.decodestring(data[mime].encode('ascii'))
315 imageformat = self._imagemime[mime]
357 imageformat = self._imagemime[mime]
316 filename = 'tmp.{0}'.format(imageformat)
358 filename = 'tmp.{0}'.format(imageformat)
317 with NamedFileInTemporaryDirectory(filename) as f, \
359 with NamedFileInTemporaryDirectory(filename) as f, \
318 open(os.devnull, 'w') as devnull:
360 open(os.devnull, 'w') as devnull:
319 f.write(raw)
361 f.write(raw)
320 f.flush()
362 f.flush()
321 fmt = dict(file=f.name, format=imageformat)
363 fmt = dict(file=f.name, format=imageformat)
322 args = [s.format(**fmt) for s in self.tempfile_image_handler]
364 args = [s.format(**fmt) for s in self.tempfile_image_handler]
323 subprocess.call(args, stdout=devnull, stderr=devnull)
365 subprocess.call(args, stdout=devnull, stderr=devnull)
324
366
325 def handle_image_callable(self, data, mime):
367 def handle_image_callable(self, data, mime):
326 self.callable_image_handler(data)
368 self.callable_image_handler(data)
327
369
328 def handle_input_request(self, msg_id, timeout=0.1):
370 def handle_input_request(self, msg_id, timeout=0.1):
329 """ Method to capture raw_input
371 """ Method to capture raw_input
330 """
372 """
331 req = self.client.stdin_channel.get_msg(timeout=timeout)
373 req = self.client.stdin_channel.get_msg(timeout=timeout)
332 # in case any iopub came while we were waiting:
374 # in case any iopub came while we were waiting:
333 self.handle_iopub(msg_id)
375 self.handle_iopub(msg_id)
334 if msg_id == req["parent_header"].get("msg_id"):
376 if msg_id == req["parent_header"].get("msg_id"):
335 # wrap SIGINT handler
377 # wrap SIGINT handler
336 real_handler = signal.getsignal(signal.SIGINT)
378 real_handler = signal.getsignal(signal.SIGINT)
337 def double_int(sig,frame):
379 def double_int(sig,frame):
338 # call real handler (forwards sigint to kernel),
380 # call real handler (forwards sigint to kernel),
339 # then raise local interrupt, stopping local raw_input
381 # then raise local interrupt, stopping local raw_input
340 real_handler(sig,frame)
382 real_handler(sig,frame)
341 raise KeyboardInterrupt
383 raise KeyboardInterrupt
342 signal.signal(signal.SIGINT, double_int)
384 signal.signal(signal.SIGINT, double_int)
343 content = req['content']
385 content = req['content']
344 read = getpass if content.get('password', False) else input
386 read = getpass if content.get('password', False) else input
345 try:
387 try:
346 raw_data = read(content["prompt"])
388 raw_data = read(content["prompt"])
347 except EOFError:
389 except EOFError:
348 # turn EOFError into EOF character
390 # turn EOFError into EOF character
349 raw_data = '\x04'
391 raw_data = '\x04'
350 except KeyboardInterrupt:
392 except KeyboardInterrupt:
351 sys.stdout.write('\n')
393 sys.stdout.write('\n')
352 return
394 return
353 finally:
395 finally:
354 # restore SIGINT handler
396 # restore SIGINT handler
355 signal.signal(signal.SIGINT, real_handler)
397 signal.signal(signal.SIGINT, real_handler)
356
398
357 # only send stdin reply if there *was not* another request
399 # only send stdin reply if there *was not* another request
358 # or execution finished while we were reading.
400 # or execution finished while we were reading.
359 if not (self.client.stdin_channel.msg_ready() or self.client.shell_channel.msg_ready()):
401 if not (self.client.stdin_channel.msg_ready() or self.client.shell_channel.msg_ready()):
360 self.client.stdin_channel.input(raw_data)
402 self.client.stdin_channel.input(raw_data)
361
403
362 def mainloop(self, display_banner=False):
404 def mainloop(self, display_banner=False):
363 while True:
405 while True:
364 try:
406 try:
365 self.interact(display_banner=display_banner)
407 self.interact(display_banner=display_banner)
366 #self.interact_with_readline()
408 #self.interact_with_readline()
367 # XXX for testing of a readline-decoupled repl loop, call
409 # XXX for testing of a readline-decoupled repl loop, call
368 # interact_with_readline above
410 # interact_with_readline above
369 break
411 break
370 except KeyboardInterrupt:
412 except KeyboardInterrupt:
371 # this should not be necessary, but KeyboardInterrupt
413 # this should not be necessary, but KeyboardInterrupt
372 # handling seems rather unpredictable...
414 # handling seems rather unpredictable...
373 self.write("\nKeyboardInterrupt in interact()\n")
415 self.write("\nKeyboardInterrupt in interact()\n")
374
416
375 def _banner1_default(self):
417 def _banner1_default(self):
376 return "IPython Console {version}\n".format(version=release.version)
418 return "IPython Console {version}\n".format(version=release.version)
377
419
378 def compute_banner(self):
420 def compute_banner(self):
379 super(ZMQTerminalInteractiveShell, self).compute_banner()
421 super(ZMQTerminalInteractiveShell, self).compute_banner()
380 if self.client and not self.kernel_banner:
422 if self.client and not self.kernel_banner:
381 msg_id = self.client.kernel_info()
423 msg_id = self.client.kernel_info()
382 while True:
424 while True:
383 try:
425 try:
384 reply = self.client.get_shell_msg(timeout=1)
426 reply = self.client.get_shell_msg(timeout=1)
385 except Empty:
427 except Empty:
386 break
428 break
387 else:
429 else:
388 if reply['parent_header'].get('msg_id') == msg_id:
430 if reply['parent_header'].get('msg_id') == msg_id:
389 self.kernel_banner = reply['content'].get('banner', '')
431 self.kernel_banner = reply['content'].get('banner', '')
390 break
432 break
391 self.banner += self.kernel_banner
433 self.banner += self.kernel_banner
392
434
393 def wait_for_kernel(self, timeout=None):
435 def wait_for_kernel(self, timeout=None):
394 """method to wait for a kernel to be ready"""
436 """method to wait for a kernel to be ready"""
395 tic = time.time()
437 tic = time.time()
396 self.client.hb_channel.unpause()
438 self.client.hb_channel.unpause()
397 while True:
439 while True:
398 msg_id = self.client.kernel_info()
440 msg_id = self.client.kernel_info()
399 reply = None
441 reply = None
400 while True:
442 while True:
401 try:
443 try:
402 reply = self.client.get_shell_msg(timeout=1)
444 reply = self.client.get_shell_msg(timeout=1)
403 except Empty:
445 except Empty:
404 break
446 break
405 else:
447 else:
406 if reply['parent_header'].get('msg_id') == msg_id:
448 if reply['parent_header'].get('msg_id') == msg_id:
407 return True
449 return True
408 if timeout is not None \
450 if timeout is not None \
409 and (time.time() - tic) > timeout \
451 and (time.time() - tic) > timeout \
410 and not self.client.hb_channel.is_beating():
452 and not self.client.hb_channel.is_beating():
411 # heart failed
453 # heart failed
412 return False
454 return False
413 return True
455 return True
414
456
415 def interact(self, display_banner=None):
457 def interact(self, display_banner=None):
416 """Closely emulate the interactive Python console."""
458 """Closely emulate the interactive Python console."""
417
459
418 # batch run -> do not interact
460 # batch run -> do not interact
419 if self.exit_now:
461 if self.exit_now:
420 return
462 return
421
463
422 if display_banner is None:
464 if display_banner is None:
423 display_banner = self.display_banner
465 display_banner = self.display_banner
424
466
425 if isinstance(display_banner, string_types):
467 if isinstance(display_banner, string_types):
426 self.show_banner(display_banner)
468 self.show_banner(display_banner)
427 elif display_banner:
469 elif display_banner:
428 self.show_banner()
470 self.show_banner()
429
471
430 more = False
472 more = False
431
473
432 # run a non-empty no-op, so that we don't get a prompt until
474 # run a non-empty no-op, so that we don't get a prompt until
433 # we know the kernel is ready. This keeps the connection
475 # we know the kernel is ready. This keeps the connection
434 # message above the first prompt.
476 # message above the first prompt.
435 if not self.wait_for_kernel(self.kernel_timeout):
477 if not self.wait_for_kernel(self.kernel_timeout):
436 error("Kernel did not respond\n")
478 error("Kernel did not respond\n")
437 return
479 return
438
480
439 if self.has_readline:
481 if self.has_readline:
440 self.readline_startup_hook(self.pre_readline)
482 self.readline_startup_hook(self.pre_readline)
441 hlen_b4_cell = self.readline.get_current_history_length()
483 hlen_b4_cell = self.readline.get_current_history_length()
442 else:
484 else:
443 hlen_b4_cell = 0
485 hlen_b4_cell = 0
444 # exit_now is set by a call to %Exit or %Quit, through the
486 # exit_now is set by a call to %Exit or %Quit, through the
445 # ask_exit callback.
487 # ask_exit callback.
446
488
447 while not self.exit_now:
489 while not self.exit_now:
448 if not self.client.is_alive():
490 if not self.client.is_alive():
449 # kernel died, prompt for action or exit
491 # kernel died, prompt for action or exit
450
492
451 action = "restart" if self.manager else "wait for restart"
493 action = "restart" if self.manager else "wait for restart"
452 ans = self.ask_yes_no("kernel died, %s ([y]/n)?" % action, default='y')
494 ans = self.ask_yes_no("kernel died, %s ([y]/n)?" % action, default='y')
453 if ans:
495 if ans:
454 if self.manager:
496 if self.manager:
455 self.manager.restart_kernel(True)
497 self.manager.restart_kernel(True)
456 self.wait_for_kernel(self.kernel_timeout)
498 self.wait_for_kernel(self.kernel_timeout)
457 else:
499 else:
458 self.exit_now = True
500 self.exit_now = True
459 continue
501 continue
460 try:
502 try:
461 # protect prompt block from KeyboardInterrupt
503 # protect prompt block from KeyboardInterrupt
462 # when sitting on ctrl-C
504 # when sitting on ctrl-C
463 self.hooks.pre_prompt_hook()
505 self.hooks.pre_prompt_hook()
464 if more:
506 if more:
465 try:
507 try:
466 prompt = self.prompt_manager.render('in2')
508 prompt = self.prompt_manager.render('in2')
467 except Exception:
509 except Exception:
468 self.showtraceback()
510 self.showtraceback()
469 if self.autoindent:
511 if self.autoindent:
470 self.rl_do_indent = True
512 self.rl_do_indent = True
471
513
472 else:
514 else:
473 try:
515 try:
474 prompt = self.separate_in + self.prompt_manager.render('in')
516 prompt = self.separate_in + self.prompt_manager.render('in')
475 except Exception:
517 except Exception:
476 self.showtraceback()
518 self.showtraceback()
477
519
478 line = self.raw_input(prompt)
520 line = self.raw_input(prompt)
479 if self.exit_now:
521 if self.exit_now:
480 # quick exit on sys.std[in|out] close
522 # quick exit on sys.std[in|out] close
481 break
523 break
482 if self.autoindent:
524 if self.autoindent:
483 self.rl_do_indent = False
525 self.rl_do_indent = False
484
526
485 except KeyboardInterrupt:
527 except KeyboardInterrupt:
486 #double-guard against keyboardinterrupts during kbdint handling
528 #double-guard against keyboardinterrupts during kbdint handling
487 try:
529 try:
488 self.write('\nKeyboardInterrupt\n')
530 self.write('\nKeyboardInterrupt\n')
489 source_raw = self.input_splitter.raw_reset()
531 source_raw = self.input_splitter.raw_reset()
490 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
532 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
491 more = False
533 more = False
492 except KeyboardInterrupt:
534 except KeyboardInterrupt:
493 pass
535 pass
494 except EOFError:
536 except EOFError:
495 if self.autoindent:
537 if self.autoindent:
496 self.rl_do_indent = False
538 self.rl_do_indent = False
497 if self.has_readline:
539 if self.has_readline:
498 self.readline_startup_hook(None)
540 self.readline_startup_hook(None)
499 self.write('\n')
541 self.write('\n')
500 self.exit()
542 self.exit()
501 except bdb.BdbQuit:
543 except bdb.BdbQuit:
502 warn('The Python debugger has exited with a BdbQuit exception.\n'
544 warn('The Python debugger has exited with a BdbQuit exception.\n'
503 'Because of how pdb handles the stack, it is impossible\n'
545 'Because of how pdb handles the stack, it is impossible\n'
504 'for IPython to properly format this particular exception.\n'
546 'for IPython to properly format this particular exception.\n'
505 'IPython will resume normal operation.')
547 'IPython will resume normal operation.')
506 except:
548 except:
507 # exceptions here are VERY RARE, but they can be triggered
549 # exceptions here are VERY RARE, but they can be triggered
508 # asynchronously by signal handlers, for example.
550 # asynchronously by signal handlers, for example.
509 self.showtraceback()
551 self.showtraceback()
510 else:
552 else:
511 try:
553 try:
512 self.input_splitter.push(line)
554 self.input_splitter.push(line)
513 more = self.input_splitter.push_accepts_more()
555 more = self.input_splitter.push_accepts_more()
514 except SyntaxError:
556 except SyntaxError:
515 # Run the code directly - run_cell takes care of displaying
557 # Run the code directly - run_cell takes care of displaying
516 # the exception.
558 # the exception.
517 more = False
559 more = False
518 if (self.SyntaxTB.last_syntax_error and
560 if (self.SyntaxTB.last_syntax_error and
519 self.autoedit_syntax):
561 self.autoedit_syntax):
520 self.edit_syntax_error()
562 self.edit_syntax_error()
521 if not more:
563 if not more:
522 source_raw = self.input_splitter.raw_reset()
564 source_raw = self.input_splitter.raw_reset()
523 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
565 hlen_b4_cell = self._replace_rlhist_multiline(source_raw, hlen_b4_cell)
524 self.run_cell(source_raw)
566 self.run_cell(source_raw)
525
567
526
568
527 # Turn off the exit flag, so the mainloop can be restarted if desired
569 # Turn off the exit flag, so the mainloop can be restarted if desired
528 self.exit_now = False
570 self.exit_now = False
General Comments 0
You need to be logged in to leave comments. Login now