##// END OF EJS Templates
add ConsoleWidget.include_other_output...
MinRK -
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,824 +1,824 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 .completion_lexer import CompletionLexer
24 from .completion_lexer import CompletionLexer
25 from .history_console_widget import HistoryConsoleWidget
25 from .history_console_widget import HistoryConsoleWidget
26 from .pygments_highlighter import PygmentsHighlighter
26 from .pygments_highlighter import PygmentsHighlighter
27
27
28
28
29 class FrontendHighlighter(PygmentsHighlighter):
29 class FrontendHighlighter(PygmentsHighlighter):
30 """ A PygmentsHighlighter that understands and ignores prompts.
30 """ A PygmentsHighlighter that understands and ignores prompts.
31 """
31 """
32
32
33 def __init__(self, frontend, lexer=None):
33 def __init__(self, frontend, lexer=None):
34 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
34 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
35 self._current_offset = 0
35 self._current_offset = 0
36 self._frontend = frontend
36 self._frontend = frontend
37 self.highlighting_on = False
37 self.highlighting_on = False
38
38
39 def highlightBlock(self, string):
39 def highlightBlock(self, string):
40 """ Highlight a block of text. Reimplemented to highlight selectively.
40 """ Highlight a block of text. Reimplemented to highlight selectively.
41 """
41 """
42 if not self.highlighting_on:
42 if not self.highlighting_on:
43 return
43 return
44
44
45 # The input to this function is a unicode string that may contain
45 # The input to this function is a unicode string that may contain
46 # paragraph break characters, non-breaking spaces, etc. Here we acquire
46 # paragraph break characters, non-breaking spaces, etc. Here we acquire
47 # the string as plain text so we can compare it.
47 # the string as plain text so we can compare it.
48 current_block = self.currentBlock()
48 current_block = self.currentBlock()
49 string = self._frontend._get_block_plain_text(current_block)
49 string = self._frontend._get_block_plain_text(current_block)
50
50
51 # Decide whether to check for the regular or continuation prompt.
51 # Decide whether to check for the regular or continuation prompt.
52 if current_block.contains(self._frontend._prompt_pos):
52 if current_block.contains(self._frontend._prompt_pos):
53 prompt = self._frontend._prompt
53 prompt = self._frontend._prompt
54 else:
54 else:
55 prompt = self._frontend._continuation_prompt
55 prompt = self._frontend._continuation_prompt
56
56
57 # Only highlight if we can identify a prompt, but make sure not to
57 # Only highlight if we can identify a prompt, but make sure not to
58 # highlight the prompt.
58 # highlight the prompt.
59 if string.startswith(prompt):
59 if string.startswith(prompt):
60 self._current_offset = len(prompt)
60 self._current_offset = len(prompt)
61 string = string[len(prompt):]
61 string = string[len(prompt):]
62 super(FrontendHighlighter, self).highlightBlock(string)
62 super(FrontendHighlighter, self).highlightBlock(string)
63
63
64 def rehighlightBlock(self, block):
64 def rehighlightBlock(self, block):
65 """ Reimplemented to temporarily enable highlighting if disabled.
65 """ Reimplemented to temporarily enable highlighting if disabled.
66 """
66 """
67 old = self.highlighting_on
67 old = self.highlighting_on
68 self.highlighting_on = True
68 self.highlighting_on = True
69 super(FrontendHighlighter, self).rehighlightBlock(block)
69 super(FrontendHighlighter, self).rehighlightBlock(block)
70 self.highlighting_on = old
70 self.highlighting_on = old
71
71
72 def setFormat(self, start, count, format):
72 def setFormat(self, start, count, format):
73 """ Reimplemented to highlight selectively.
73 """ Reimplemented to highlight selectively.
74 """
74 """
75 start += self._current_offset
75 start += self._current_offset
76 super(FrontendHighlighter, self).setFormat(start, count, format)
76 super(FrontendHighlighter, self).setFormat(start, count, format)
77
77
78
78
79 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
79 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
80 """ A Qt frontend for a generic Python kernel.
80 """ A Qt frontend for a generic Python kernel.
81 """
81 """
82
82
83 # The text to show when the kernel is (re)started.
83 # The text to show when the kernel is (re)started.
84 banner = Unicode(config=True)
84 banner = Unicode(config=True)
85 kernel_banner = Unicode()
85 kernel_banner = Unicode()
86
86
87 # An option and corresponding signal for overriding the default kernel
87 # An option and corresponding signal for overriding the default kernel
88 # interrupt behavior.
88 # interrupt behavior.
89 custom_interrupt = Bool(False)
89 custom_interrupt = Bool(False)
90 custom_interrupt_requested = QtCore.Signal()
90 custom_interrupt_requested = QtCore.Signal()
91
91
92 # An option and corresponding signals for overriding the default kernel
92 # An option and corresponding signals for overriding the default kernel
93 # restart behavior.
93 # restart behavior.
94 custom_restart = Bool(False)
94 custom_restart = Bool(False)
95 custom_restart_kernel_died = QtCore.Signal(float)
95 custom_restart_kernel_died = QtCore.Signal(float)
96 custom_restart_requested = QtCore.Signal()
96 custom_restart_requested = QtCore.Signal()
97
97
98 # Whether to automatically show calltips on open-parentheses.
98 # Whether to automatically show calltips on open-parentheses.
99 enable_calltips = Bool(True, config=True,
99 enable_calltips = Bool(True, config=True,
100 help="Whether to draw information calltips on open-parentheses.")
100 help="Whether to draw information calltips on open-parentheses.")
101
101
102 clear_on_kernel_restart = Bool(True, config=True,
102 clear_on_kernel_restart = Bool(True, config=True,
103 help="Whether to clear the console when the kernel is restarted")
103 help="Whether to clear the console when the kernel is restarted")
104
104
105 confirm_restart = Bool(True, config=True,
105 confirm_restart = Bool(True, config=True,
106 help="Whether to ask for user confirmation when restarting kernel")
106 help="Whether to ask for user confirmation when restarting kernel")
107
107
108 lexer_class = DottedObjectName(config=True,
108 lexer_class = DottedObjectName(config=True,
109 help="The pygments lexer class to use."
109 help="The pygments lexer class to use."
110 )
110 )
111 def _lexer_class_changed(self, name, old, new):
111 def _lexer_class_changed(self, name, old, new):
112 lexer_class = import_item(new)
112 lexer_class = import_item(new)
113 self.lexer = lexer_class()
113 self.lexer = lexer_class()
114
114
115 def _lexer_class_default(self):
115 def _lexer_class_default(self):
116 if py3compat.PY3:
116 if py3compat.PY3:
117 return 'pygments.lexers.Python3Lexer'
117 return 'pygments.lexers.Python3Lexer'
118 else:
118 else:
119 return 'pygments.lexers.PythonLexer'
119 return 'pygments.lexers.PythonLexer'
120
120
121 lexer = Any()
121 lexer = Any()
122 def _lexer_default(self):
122 def _lexer_default(self):
123 lexer_class = import_item(self.lexer_class)
123 lexer_class = import_item(self.lexer_class)
124 return lexer_class()
124 return lexer_class()
125
125
126 # Emitted when a user visible 'execute_request' has been submitted to the
126 # Emitted when a user visible 'execute_request' has been submitted to the
127 # kernel from the FrontendWidget. Contains the code to be executed.
127 # kernel from the FrontendWidget. Contains the code to be executed.
128 executing = QtCore.Signal(object)
128 executing = QtCore.Signal(object)
129
129
130 # Emitted when a user-visible 'execute_reply' has been received from the
130 # Emitted when a user-visible 'execute_reply' has been received from the
131 # kernel and processed by the FrontendWidget. Contains the response message.
131 # kernel and processed by the FrontendWidget. Contains the response message.
132 executed = QtCore.Signal(object)
132 executed = QtCore.Signal(object)
133
133
134 # Emitted when an exit request has been received from the kernel.
134 # Emitted when an exit request has been received from the kernel.
135 exit_requested = QtCore.Signal(object)
135 exit_requested = QtCore.Signal(object)
136
136
137 # Protected class variables.
137 # Protected class variables.
138 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
139 logical_line_transforms=[],
139 logical_line_transforms=[],
140 python_line_transforms=[],
140 python_line_transforms=[],
141 )
141 )
142 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
143 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
144 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
145 _input_splitter_class = InputSplitter
145 _input_splitter_class = InputSplitter
146 _local_kernel = False
146 _local_kernel = False
147 _highlighter = Instance(FrontendHighlighter)
147 _highlighter = Instance(FrontendHighlighter)
148
148
149 #---------------------------------------------------------------------------
149 #---------------------------------------------------------------------------
150 # 'object' interface
150 # 'object' interface
151 #---------------------------------------------------------------------------
151 #---------------------------------------------------------------------------
152
152
153 def __init__(self, *args, **kw):
153 def __init__(self, *args, **kw):
154 super(FrontendWidget, self).__init__(*args, **kw)
154 super(FrontendWidget, self).__init__(*args, **kw)
155 # FIXME: remove this when PySide min version is updated past 1.0.7
155 # FIXME: remove this when PySide min version is updated past 1.0.7
156 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 # forcefully disable calltips if PySide is < 1.0.7, because they crash
157 if qt.QT_API == qt.QT_API_PYSIDE:
157 if qt.QT_API == qt.QT_API_PYSIDE:
158 import PySide
158 import PySide
159 if PySide.__version_info__ < (1,0,7):
159 if PySide.__version_info__ < (1,0,7):
160 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
161 self.enable_calltips = False
161 self.enable_calltips = False
162
162
163 # FrontendWidget protected variables.
163 # FrontendWidget protected variables.
164 self._bracket_matcher = BracketMatcher(self._control)
164 self._bracket_matcher = BracketMatcher(self._control)
165 self._call_tip_widget = CallTipWidget(self._control)
165 self._call_tip_widget = CallTipWidget(self._control)
166 self._completion_lexer = CompletionLexer(self.lexer)
166 self._completion_lexer = CompletionLexer(self.lexer)
167 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
167 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
168 self._hidden = False
168 self._hidden = False
169 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
169 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
170 self._input_splitter = self._input_splitter_class()
170 self._input_splitter = self._input_splitter_class()
171 self._kernel_manager = None
171 self._kernel_manager = None
172 self._kernel_client = None
172 self._kernel_client = None
173 self._request_info = {}
173 self._request_info = {}
174 self._request_info['execute'] = {};
174 self._request_info['execute'] = {};
175 self._callback_dict = {}
175 self._callback_dict = {}
176
176
177 # Configure the ConsoleWidget.
177 # Configure the ConsoleWidget.
178 self.tab_width = 4
178 self.tab_width = 4
179 self._set_continuation_prompt('... ')
179 self._set_continuation_prompt('... ')
180
180
181 # Configure the CallTipWidget.
181 # Configure the CallTipWidget.
182 self._call_tip_widget.setFont(self.font)
182 self._call_tip_widget.setFont(self.font)
183 self.font_changed.connect(self._call_tip_widget.setFont)
183 self.font_changed.connect(self._call_tip_widget.setFont)
184
184
185 # Configure actions.
185 # Configure actions.
186 action = self._copy_raw_action
186 action = self._copy_raw_action
187 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
187 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
188 action.setEnabled(False)
188 action.setEnabled(False)
189 action.setShortcut(QtGui.QKeySequence(key))
189 action.setShortcut(QtGui.QKeySequence(key))
190 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
190 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
191 action.triggered.connect(self.copy_raw)
191 action.triggered.connect(self.copy_raw)
192 self.copy_available.connect(action.setEnabled)
192 self.copy_available.connect(action.setEnabled)
193 self.addAction(action)
193 self.addAction(action)
194
194
195 # Connect signal handlers.
195 # Connect signal handlers.
196 document = self._control.document()
196 document = self._control.document()
197 document.contentsChange.connect(self._document_contents_change)
197 document.contentsChange.connect(self._document_contents_change)
198
198
199 # Set flag for whether we are connected via localhost.
199 # Set flag for whether we are connected via localhost.
200 self._local_kernel = kw.get('local_kernel',
200 self._local_kernel = kw.get('local_kernel',
201 FrontendWidget._local_kernel)
201 FrontendWidget._local_kernel)
202
202
203 # Whether or not a clear_output call is pending new output.
203 # Whether or not a clear_output call is pending new output.
204 self._pending_clearoutput = False
204 self._pending_clearoutput = False
205
205
206 #---------------------------------------------------------------------------
206 #---------------------------------------------------------------------------
207 # 'ConsoleWidget' public interface
207 # 'ConsoleWidget' public interface
208 #---------------------------------------------------------------------------
208 #---------------------------------------------------------------------------
209
209
210 def copy(self):
210 def copy(self):
211 """ Copy the currently selected text to the clipboard, removing prompts.
211 """ Copy the currently selected text to the clipboard, removing prompts.
212 """
212 """
213 if self._page_control is not None and self._page_control.hasFocus():
213 if self._page_control is not None and self._page_control.hasFocus():
214 self._page_control.copy()
214 self._page_control.copy()
215 elif self._control.hasFocus():
215 elif self._control.hasFocus():
216 text = self._control.textCursor().selection().toPlainText()
216 text = self._control.textCursor().selection().toPlainText()
217 if text:
217 if text:
218 was_newline = text[-1] == '\n'
218 was_newline = text[-1] == '\n'
219 text = self._prompt_transformer.transform_cell(text)
219 text = self._prompt_transformer.transform_cell(text)
220 if not was_newline: # user doesn't need newline
220 if not was_newline: # user doesn't need newline
221 text = text[:-1]
221 text = text[:-1]
222 QtGui.QApplication.clipboard().setText(text)
222 QtGui.QApplication.clipboard().setText(text)
223 else:
223 else:
224 self.log.debug("frontend widget : unknown copy target")
224 self.log.debug("frontend widget : unknown copy target")
225
225
226 #---------------------------------------------------------------------------
226 #---------------------------------------------------------------------------
227 # 'ConsoleWidget' abstract interface
227 # 'ConsoleWidget' abstract interface
228 #---------------------------------------------------------------------------
228 #---------------------------------------------------------------------------
229
229
230 def _is_complete(self, source, interactive):
230 def _is_complete(self, source, interactive):
231 """ Returns whether 'source' can be completely processed and a new
231 """ Returns whether 'source' can be completely processed and a new
232 prompt created. When triggered by an Enter/Return key press,
232 prompt created. When triggered by an Enter/Return key press,
233 'interactive' is True; otherwise, it is False.
233 'interactive' is True; otherwise, it is False.
234 """
234 """
235 self._input_splitter.reset()
235 self._input_splitter.reset()
236 try:
236 try:
237 complete = self._input_splitter.push(source)
237 complete = self._input_splitter.push(source)
238 except SyntaxError:
238 except SyntaxError:
239 return True
239 return True
240 if interactive:
240 if interactive:
241 complete = not self._input_splitter.push_accepts_more()
241 complete = not self._input_splitter.push_accepts_more()
242 return complete
242 return complete
243
243
244 def _execute(self, source, hidden):
244 def _execute(self, source, hidden):
245 """ Execute 'source'. If 'hidden', do not show any output.
245 """ Execute 'source'. If 'hidden', do not show any output.
246
246
247 See parent class :meth:`execute` docstring for full details.
247 See parent class :meth:`execute` docstring for full details.
248 """
248 """
249 msg_id = self.kernel_client.execute(source, hidden)
249 msg_id = self.kernel_client.execute(source, hidden)
250 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
250 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
251 self._hidden = hidden
251 self._hidden = hidden
252 if not hidden:
252 if not hidden:
253 self.executing.emit(source)
253 self.executing.emit(source)
254
254
255 def _prompt_started_hook(self):
255 def _prompt_started_hook(self):
256 """ Called immediately after a new prompt is displayed.
256 """ Called immediately after a new prompt is displayed.
257 """
257 """
258 if not self._reading:
258 if not self._reading:
259 self._highlighter.highlighting_on = True
259 self._highlighter.highlighting_on = True
260
260
261 def _prompt_finished_hook(self):
261 def _prompt_finished_hook(self):
262 """ Called immediately after a prompt is finished, i.e. when some input
262 """ Called immediately after a prompt is finished, i.e. when some input
263 will be processed and a new prompt displayed.
263 will be processed and a new prompt displayed.
264 """
264 """
265 # Flush all state from the input splitter so the next round of
265 # Flush all state from the input splitter so the next round of
266 # reading input starts with a clean buffer.
266 # reading input starts with a clean buffer.
267 self._input_splitter.reset()
267 self._input_splitter.reset()
268
268
269 if not self._reading:
269 if not self._reading:
270 self._highlighter.highlighting_on = False
270 self._highlighter.highlighting_on = False
271
271
272 def _tab_pressed(self):
272 def _tab_pressed(self):
273 """ Called when the tab key is pressed. Returns whether to continue
273 """ Called when the tab key is pressed. Returns whether to continue
274 processing the event.
274 processing the event.
275 """
275 """
276 # Perform tab completion if:
276 # Perform tab completion if:
277 # 1) The cursor is in the input buffer.
277 # 1) The cursor is in the input buffer.
278 # 2) There is a non-whitespace character before the cursor.
278 # 2) There is a non-whitespace character before the cursor.
279 text = self._get_input_buffer_cursor_line()
279 text = self._get_input_buffer_cursor_line()
280 if text is None:
280 if text is None:
281 return False
281 return False
282 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
282 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
283 if complete:
283 if complete:
284 self._complete()
284 self._complete()
285 return not complete
285 return not complete
286
286
287 #---------------------------------------------------------------------------
287 #---------------------------------------------------------------------------
288 # 'ConsoleWidget' protected interface
288 # 'ConsoleWidget' protected interface
289 #---------------------------------------------------------------------------
289 #---------------------------------------------------------------------------
290
290
291 def _context_menu_make(self, pos):
291 def _context_menu_make(self, pos):
292 """ Reimplemented to add an action for raw copy.
292 """ Reimplemented to add an action for raw copy.
293 """
293 """
294 menu = super(FrontendWidget, self)._context_menu_make(pos)
294 menu = super(FrontendWidget, self)._context_menu_make(pos)
295 for before_action in menu.actions():
295 for before_action in menu.actions():
296 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
296 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
297 QtGui.QKeySequence.ExactMatch:
297 QtGui.QKeySequence.ExactMatch:
298 menu.insertAction(before_action, self._copy_raw_action)
298 menu.insertAction(before_action, self._copy_raw_action)
299 break
299 break
300 return menu
300 return menu
301
301
302 def request_interrupt_kernel(self):
302 def request_interrupt_kernel(self):
303 if self._executing:
303 if self._executing:
304 self.interrupt_kernel()
304 self.interrupt_kernel()
305
305
306 def request_restart_kernel(self):
306 def request_restart_kernel(self):
307 message = 'Are you sure you want to restart the kernel?'
307 message = 'Are you sure you want to restart the kernel?'
308 self.restart_kernel(message, now=False)
308 self.restart_kernel(message, now=False)
309
309
310 def _event_filter_console_keypress(self, event):
310 def _event_filter_console_keypress(self, event):
311 """ Reimplemented for execution interruption and smart backspace.
311 """ Reimplemented for execution interruption and smart backspace.
312 """
312 """
313 key = event.key()
313 key = event.key()
314 if self._control_key_down(event.modifiers(), include_command=False):
314 if self._control_key_down(event.modifiers(), include_command=False):
315
315
316 if key == QtCore.Qt.Key_C and self._executing:
316 if key == QtCore.Qt.Key_C and self._executing:
317 self.request_interrupt_kernel()
317 self.request_interrupt_kernel()
318 return True
318 return True
319
319
320 elif key == QtCore.Qt.Key_Period:
320 elif key == QtCore.Qt.Key_Period:
321 self.request_restart_kernel()
321 self.request_restart_kernel()
322 return True
322 return True
323
323
324 elif not event.modifiers() & QtCore.Qt.AltModifier:
324 elif not event.modifiers() & QtCore.Qt.AltModifier:
325
325
326 # Smart backspace: remove four characters in one backspace if:
326 # Smart backspace: remove four characters in one backspace if:
327 # 1) everything left of the cursor is whitespace
327 # 1) everything left of the cursor is whitespace
328 # 2) the four characters immediately left of the cursor are spaces
328 # 2) the four characters immediately left of the cursor are spaces
329 if key == QtCore.Qt.Key_Backspace:
329 if key == QtCore.Qt.Key_Backspace:
330 col = self._get_input_buffer_cursor_column()
330 col = self._get_input_buffer_cursor_column()
331 cursor = self._control.textCursor()
331 cursor = self._control.textCursor()
332 if col > 3 and not cursor.hasSelection():
332 if col > 3 and not cursor.hasSelection():
333 text = self._get_input_buffer_cursor_line()[:col]
333 text = self._get_input_buffer_cursor_line()[:col]
334 if text.endswith(' ') and not text.strip():
334 if text.endswith(' ') and not text.strip():
335 cursor.movePosition(QtGui.QTextCursor.Left,
335 cursor.movePosition(QtGui.QTextCursor.Left,
336 QtGui.QTextCursor.KeepAnchor, 4)
336 QtGui.QTextCursor.KeepAnchor, 4)
337 cursor.removeSelectedText()
337 cursor.removeSelectedText()
338 return True
338 return True
339
339
340 return super(FrontendWidget, self)._event_filter_console_keypress(event)
340 return super(FrontendWidget, self)._event_filter_console_keypress(event)
341
341
342 def _insert_continuation_prompt(self, cursor):
342 def _insert_continuation_prompt(self, cursor):
343 """ Reimplemented for auto-indentation.
343 """ Reimplemented for auto-indentation.
344 """
344 """
345 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
345 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
346 cursor.insertText(' ' * self._input_splitter.indent_spaces)
346 cursor.insertText(' ' * self._input_splitter.indent_spaces)
347
347
348 #---------------------------------------------------------------------------
348 #---------------------------------------------------------------------------
349 # 'BaseFrontendMixin' abstract interface
349 # 'BaseFrontendMixin' abstract interface
350 #---------------------------------------------------------------------------
350 #---------------------------------------------------------------------------
351 def _handle_clear_output(self, msg):
351 def _handle_clear_output(self, msg):
352 """Handle clear output messages."""
352 """Handle clear output messages."""
353 if not self._hidden and self._is_from_this_session(msg):
353 if include_output(msg):
354 wait = msg['content'].get('wait', True)
354 wait = msg['content'].get('wait', True)
355 if wait:
355 if wait:
356 self._pending_clearoutput = True
356 self._pending_clearoutput = True
357 else:
357 else:
358 self.clear_output()
358 self.clear_output()
359
359
360 def _handle_complete_reply(self, rep):
360 def _handle_complete_reply(self, rep):
361 """ Handle replies for tab completion.
361 """ Handle replies for tab completion.
362 """
362 """
363 self.log.debug("complete: %s", rep.get('content', ''))
363 self.log.debug("complete: %s", rep.get('content', ''))
364 cursor = self._get_cursor()
364 cursor = self._get_cursor()
365 info = self._request_info.get('complete')
365 info = self._request_info.get('complete')
366 if info and info.id == rep['parent_header']['msg_id'] and \
366 if info and info.id == rep['parent_header']['msg_id'] and \
367 info.pos == cursor.position():
367 info.pos == cursor.position():
368 text = '.'.join(self._get_context())
368 text = '.'.join(self._get_context())
369 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
369 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
370 self._complete_with_items(cursor, rep['content']['matches'])
370 self._complete_with_items(cursor, rep['content']['matches'])
371
371
372 def _silent_exec_callback(self, expr, callback):
372 def _silent_exec_callback(self, expr, callback):
373 """Silently execute `expr` in the kernel and call `callback` with reply
373 """Silently execute `expr` in the kernel and call `callback` with reply
374
374
375 the `expr` is evaluated silently in the kernel (without) output in
375 the `expr` is evaluated silently in the kernel (without) output in
376 the frontend. Call `callback` with the
376 the frontend. Call `callback` with the
377 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
377 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
378
378
379 Parameters
379 Parameters
380 ----------
380 ----------
381 expr : string
381 expr : string
382 valid string to be executed by the kernel.
382 valid string to be executed by the kernel.
383 callback : function
383 callback : function
384 function accepting one argument, as a string. The string will be
384 function accepting one argument, as a string. The string will be
385 the `repr` of the result of evaluating `expr`
385 the `repr` of the result of evaluating `expr`
386
386
387 The `callback` is called with the `repr()` of the result of `expr` as
387 The `callback` is called with the `repr()` of the result of `expr` as
388 first argument. To get the object, do `eval()` on the passed value.
388 first argument. To get the object, do `eval()` on the passed value.
389
389
390 See Also
390 See Also
391 --------
391 --------
392 _handle_exec_callback : private method, deal with calling callback with reply
392 _handle_exec_callback : private method, deal with calling callback with reply
393
393
394 """
394 """
395
395
396 # generate uuid, which would be used as an indication of whether or
396 # generate uuid, which would be used as an indication of whether or
397 # not the unique request originated from here (can use msg id ?)
397 # not the unique request originated from here (can use msg id ?)
398 local_uuid = str(uuid.uuid1())
398 local_uuid = str(uuid.uuid1())
399 msg_id = self.kernel_client.execute('',
399 msg_id = self.kernel_client.execute('',
400 silent=True, user_expressions={ local_uuid:expr })
400 silent=True, user_expressions={ local_uuid:expr })
401 self._callback_dict[local_uuid] = callback
401 self._callback_dict[local_uuid] = callback
402 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
402 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
403
403
404 def _handle_exec_callback(self, msg):
404 def _handle_exec_callback(self, msg):
405 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
405 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
406
406
407 Parameters
407 Parameters
408 ----------
408 ----------
409 msg : raw message send by the kernel containing an `user_expressions`
409 msg : raw message send by the kernel containing an `user_expressions`
410 and having a 'silent_exec_callback' kind.
410 and having a 'silent_exec_callback' kind.
411
411
412 Notes
412 Notes
413 -----
413 -----
414 This function will look for a `callback` associated with the
414 This function will look for a `callback` associated with the
415 corresponding message id. Association has been made by
415 corresponding message id. Association has been made by
416 `_silent_exec_callback`. `callback` is then called with the `repr()`
416 `_silent_exec_callback`. `callback` is then called with the `repr()`
417 of the value of corresponding `user_expressions` as argument.
417 of the value of corresponding `user_expressions` as argument.
418 `callback` is then removed from the known list so that any message
418 `callback` is then removed from the known list so that any message
419 coming again with the same id won't trigger it.
419 coming again with the same id won't trigger it.
420
420
421 """
421 """
422
422
423 user_exp = msg['content'].get('user_expressions')
423 user_exp = msg['content'].get('user_expressions')
424 if not user_exp:
424 if not user_exp:
425 return
425 return
426 for expression in user_exp:
426 for expression in user_exp:
427 if expression in self._callback_dict:
427 if expression in self._callback_dict:
428 self._callback_dict.pop(expression)(user_exp[expression])
428 self._callback_dict.pop(expression)(user_exp[expression])
429
429
430 def _handle_execute_reply(self, msg):
430 def _handle_execute_reply(self, msg):
431 """ Handles replies for code execution.
431 """ Handles replies for code execution.
432 """
432 """
433 self.log.debug("execute: %s", msg.get('content', ''))
433 self.log.debug("execute: %s", msg.get('content', ''))
434 msg_id = msg['parent_header']['msg_id']
434 msg_id = msg['parent_header']['msg_id']
435 info = self._request_info['execute'].get(msg_id)
435 info = self._request_info['execute'].get(msg_id)
436 # unset reading flag, because if execute finished, raw_input can't
436 # unset reading flag, because if execute finished, raw_input can't
437 # still be pending.
437 # still be pending.
438 self._reading = False
438 self._reading = False
439 if info and info.kind == 'user' and not self._hidden:
439 if info and info.kind == 'user' and not self._hidden:
440 # Make sure that all output from the SUB channel has been processed
440 # Make sure that all output from the SUB channel has been processed
441 # before writing a new prompt.
441 # before writing a new prompt.
442 self.kernel_client.iopub_channel.flush()
442 self.kernel_client.iopub_channel.flush()
443
443
444 # Reset the ANSI style information to prevent bad text in stdout
444 # Reset the ANSI style information to prevent bad text in stdout
445 # from messing up our colors. We're not a true terminal so we're
445 # from messing up our colors. We're not a true terminal so we're
446 # allowed to do this.
446 # allowed to do this.
447 if self.ansi_codes:
447 if self.ansi_codes:
448 self._ansi_processor.reset_sgr()
448 self._ansi_processor.reset_sgr()
449
449
450 content = msg['content']
450 content = msg['content']
451 status = content['status']
451 status = content['status']
452 if status == 'ok':
452 if status == 'ok':
453 self._process_execute_ok(msg)
453 self._process_execute_ok(msg)
454 elif status == 'error':
454 elif status == 'error':
455 self._process_execute_error(msg)
455 self._process_execute_error(msg)
456 elif status == 'aborted':
456 elif status == 'aborted':
457 self._process_execute_abort(msg)
457 self._process_execute_abort(msg)
458
458
459 self._show_interpreter_prompt_for_reply(msg)
459 self._show_interpreter_prompt_for_reply(msg)
460 self.executed.emit(msg)
460 self.executed.emit(msg)
461 self._request_info['execute'].pop(msg_id)
461 self._request_info['execute'].pop(msg_id)
462 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
462 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
463 self._handle_exec_callback(msg)
463 self._handle_exec_callback(msg)
464 self._request_info['execute'].pop(msg_id)
464 self._request_info['execute'].pop(msg_id)
465 else:
465 else:
466 super(FrontendWidget, self)._handle_execute_reply(msg)
466 super(FrontendWidget, self)._handle_execute_reply(msg)
467
467
468 def _handle_input_request(self, msg):
468 def _handle_input_request(self, msg):
469 """ Handle requests for raw_input.
469 """ Handle requests for raw_input.
470 """
470 """
471 self.log.debug("input: %s", msg.get('content', ''))
471 self.log.debug("input: %s", msg.get('content', ''))
472 if self._hidden:
472 if self._hidden:
473 raise RuntimeError('Request for raw input during hidden execution.')
473 raise RuntimeError('Request for raw input during hidden execution.')
474
474
475 # Make sure that all output from the SUB channel has been processed
475 # Make sure that all output from the SUB channel has been processed
476 # before entering readline mode.
476 # before entering readline mode.
477 self.kernel_client.iopub_channel.flush()
477 self.kernel_client.iopub_channel.flush()
478
478
479 def callback(line):
479 def callback(line):
480 self.kernel_client.stdin_channel.input(line)
480 self.kernel_client.stdin_channel.input(line)
481 if self._reading:
481 if self._reading:
482 self.log.debug("Got second input request, assuming first was interrupted.")
482 self.log.debug("Got second input request, assuming first was interrupted.")
483 self._reading = False
483 self._reading = False
484 self._readline(msg['content']['prompt'], callback=callback)
484 self._readline(msg['content']['prompt'], callback=callback)
485
485
486 def _kernel_restarted_message(self, died=True):
486 def _kernel_restarted_message(self, died=True):
487 msg = "Kernel died, restarting" if died else "Kernel restarting"
487 msg = "Kernel died, restarting" if died else "Kernel restarting"
488 self._append_html("<br>%s<hr><br>" % msg,
488 self._append_html("<br>%s<hr><br>" % msg,
489 before_prompt=False
489 before_prompt=False
490 )
490 )
491
491
492 def _handle_kernel_died(self, since_last_heartbeat):
492 def _handle_kernel_died(self, since_last_heartbeat):
493 """Handle the kernel's death (if we do not own the kernel).
493 """Handle the kernel's death (if we do not own the kernel).
494 """
494 """
495 self.log.warn("kernel died: %s", since_last_heartbeat)
495 self.log.warn("kernel died: %s", since_last_heartbeat)
496 if self.custom_restart:
496 if self.custom_restart:
497 self.custom_restart_kernel_died.emit(since_last_heartbeat)
497 self.custom_restart_kernel_died.emit(since_last_heartbeat)
498 else:
498 else:
499 self._kernel_restarted_message(died=True)
499 self._kernel_restarted_message(died=True)
500 self.reset()
500 self.reset()
501
501
502 def _handle_kernel_restarted(self, died=True):
502 def _handle_kernel_restarted(self, died=True):
503 """Notice that the autorestarter restarted the kernel.
503 """Notice that the autorestarter restarted the kernel.
504
504
505 There's nothing to do but show a message.
505 There's nothing to do but show a message.
506 """
506 """
507 self.log.warn("kernel restarted")
507 self.log.warn("kernel restarted")
508 self._kernel_restarted_message(died=died)
508 self._kernel_restarted_message(died=died)
509 self.reset()
509 self.reset()
510
510
511 def _handle_inspect_reply(self, rep):
511 def _handle_inspect_reply(self, rep):
512 """Handle replies for call tips."""
512 """Handle replies for call tips."""
513 self.log.debug("oinfo: %s", rep.get('content', ''))
513 self.log.debug("oinfo: %s", rep.get('content', ''))
514 cursor = self._get_cursor()
514 cursor = self._get_cursor()
515 info = self._request_info.get('call_tip')
515 info = self._request_info.get('call_tip')
516 if info and info.id == rep['parent_header']['msg_id'] and \
516 if info and info.id == rep['parent_header']['msg_id'] and \
517 info.pos == cursor.position():
517 info.pos == cursor.position():
518 content = rep['content']
518 content = rep['content']
519 if content.get('status') == 'ok':
519 if content.get('status') == 'ok':
520 self._call_tip_widget.show_inspect_data(content)
520 self._call_tip_widget.show_inspect_data(content)
521
521
522 def _handle_execute_result(self, msg):
522 def _handle_execute_result(self, msg):
523 """ Handle display hook output.
523 """ Handle display hook output.
524 """
524 """
525 self.log.debug("execute_result: %s", msg.get('content', ''))
525 self.log.debug("execute_result: %s", msg.get('content', ''))
526 if not self._hidden and self._is_from_this_session(msg):
526 if self.include_output(msg):
527 self.flush_clearoutput()
527 self.flush_clearoutput()
528 text = msg['content']['data']
528 text = msg['content']['data']
529 self._append_plain_text(text + '\n', before_prompt=True)
529 self._append_plain_text(text + '\n', before_prompt=True)
530
530
531 def _handle_stream(self, msg):
531 def _handle_stream(self, msg):
532 """ Handle stdout, stderr, and stdin.
532 """ Handle stdout, stderr, and stdin.
533 """
533 """
534 self.log.debug("stream: %s", msg.get('content', ''))
534 self.log.debug("stream: %s", msg.get('content', ''))
535 if not self._hidden and self._is_from_this_session(msg):
535 if self.include_output(msg):
536 self.flush_clearoutput()
536 self.flush_clearoutput()
537 self.append_stream(msg['content']['text'])
537 self.append_stream(msg['content']['text'])
538
538
539 def _handle_shutdown_reply(self, msg):
539 def _handle_shutdown_reply(self, msg):
540 """ Handle shutdown signal, only if from other console.
540 """ Handle shutdown signal, only if from other console.
541 """
541 """
542 self.log.info("shutdown: %s", msg.get('content', ''))
542 self.log.info("shutdown: %s", msg.get('content', ''))
543 restart = msg.get('content', {}).get('restart', False)
543 restart = msg.get('content', {}).get('restart', False)
544 if not self._hidden and not self._is_from_this_session(msg):
544 if not self._hidden and not self.from_here(msg):
545 # got shutdown reply, request came from session other than ours
545 # got shutdown reply, request came from session other than ours
546 if restart:
546 if restart:
547 # someone restarted the kernel, handle it
547 # someone restarted the kernel, handle it
548 self._handle_kernel_restarted(died=False)
548 self._handle_kernel_restarted(died=False)
549 else:
549 else:
550 # kernel was shutdown permanently
550 # kernel was shutdown permanently
551 # this triggers exit_requested if the kernel was local,
551 # this triggers exit_requested if the kernel was local,
552 # and a dialog if the kernel was remote,
552 # and a dialog if the kernel was remote,
553 # so we don't suddenly clear the qtconsole without asking.
553 # so we don't suddenly clear the qtconsole without asking.
554 if self._local_kernel:
554 if self._local_kernel:
555 self.exit_requested.emit(self)
555 self.exit_requested.emit(self)
556 else:
556 else:
557 title = self.window().windowTitle()
557 title = self.window().windowTitle()
558 reply = QtGui.QMessageBox.question(self, title,
558 reply = QtGui.QMessageBox.question(self, title,
559 "Kernel has been shutdown permanently. "
559 "Kernel has been shutdown permanently. "
560 "Close the Console?",
560 "Close the Console?",
561 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
561 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
562 if reply == QtGui.QMessageBox.Yes:
562 if reply == QtGui.QMessageBox.Yes:
563 self.exit_requested.emit(self)
563 self.exit_requested.emit(self)
564
564
565 def _handle_status(self, msg):
565 def _handle_status(self, msg):
566 """Handle status message"""
566 """Handle status message"""
567 # This is where a busy/idle indicator would be triggered,
567 # This is where a busy/idle indicator would be triggered,
568 # when we make one.
568 # when we make one.
569 state = msg['content'].get('execution_state', '')
569 state = msg['content'].get('execution_state', '')
570 if state == 'starting':
570 if state == 'starting':
571 # kernel started while we were running
571 # kernel started while we were running
572 if self._executing:
572 if self._executing:
573 self._handle_kernel_restarted(died=True)
573 self._handle_kernel_restarted(died=True)
574 elif state == 'idle':
574 elif state == 'idle':
575 pass
575 pass
576 elif state == 'busy':
576 elif state == 'busy':
577 pass
577 pass
578
578
579 def _started_channels(self):
579 def _started_channels(self):
580 """ Called when the KernelManager channels have started listening or
580 """ Called when the KernelManager channels have started listening or
581 when the frontend is assigned an already listening KernelManager.
581 when the frontend is assigned an already listening KernelManager.
582 """
582 """
583 self.reset(clear=True)
583 self.reset(clear=True)
584
584
585 #---------------------------------------------------------------------------
585 #---------------------------------------------------------------------------
586 # 'FrontendWidget' public interface
586 # 'FrontendWidget' public interface
587 #---------------------------------------------------------------------------
587 #---------------------------------------------------------------------------
588
588
589 def copy_raw(self):
589 def copy_raw(self):
590 """ Copy the currently selected text to the clipboard without attempting
590 """ Copy the currently selected text to the clipboard without attempting
591 to remove prompts or otherwise alter the text.
591 to remove prompts or otherwise alter the text.
592 """
592 """
593 self._control.copy()
593 self._control.copy()
594
594
595 def execute_file(self, path, hidden=False):
595 def execute_file(self, path, hidden=False):
596 """ Attempts to execute file with 'path'. If 'hidden', no output is
596 """ Attempts to execute file with 'path'. If 'hidden', no output is
597 shown.
597 shown.
598 """
598 """
599 self.execute('execfile(%r)' % path, hidden=hidden)
599 self.execute('execfile(%r)' % path, hidden=hidden)
600
600
601 def interrupt_kernel(self):
601 def interrupt_kernel(self):
602 """ Attempts to interrupt the running kernel.
602 """ Attempts to interrupt the running kernel.
603
603
604 Also unsets _reading flag, to avoid runtime errors
604 Also unsets _reading flag, to avoid runtime errors
605 if raw_input is called again.
605 if raw_input is called again.
606 """
606 """
607 if self.custom_interrupt:
607 if self.custom_interrupt:
608 self._reading = False
608 self._reading = False
609 self.custom_interrupt_requested.emit()
609 self.custom_interrupt_requested.emit()
610 elif self.kernel_manager:
610 elif self.kernel_manager:
611 self._reading = False
611 self._reading = False
612 self.kernel_manager.interrupt_kernel()
612 self.kernel_manager.interrupt_kernel()
613 else:
613 else:
614 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
614 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
615
615
616 def reset(self, clear=False):
616 def reset(self, clear=False):
617 """ Resets the widget to its initial state if ``clear`` parameter
617 """ Resets the widget to its initial state if ``clear`` parameter
618 is True, otherwise
618 is True, otherwise
619 prints a visual indication of the fact that the kernel restarted, but
619 prints a visual indication of the fact that the kernel restarted, but
620 does not clear the traces from previous usage of the kernel before it
620 does not clear the traces from previous usage of the kernel before it
621 was restarted. With ``clear=True``, it is similar to ``%clear``, but
621 was restarted. With ``clear=True``, it is similar to ``%clear``, but
622 also re-writes the banner and aborts execution if necessary.
622 also re-writes the banner and aborts execution if necessary.
623 """
623 """
624 if self._executing:
624 if self._executing:
625 self._executing = False
625 self._executing = False
626 self._request_info['execute'] = {}
626 self._request_info['execute'] = {}
627 self._reading = False
627 self._reading = False
628 self._highlighter.highlighting_on = False
628 self._highlighter.highlighting_on = False
629
629
630 if clear:
630 if clear:
631 self._control.clear()
631 self._control.clear()
632 self._append_plain_text(self.banner)
632 self._append_plain_text(self.banner)
633 if self.kernel_banner:
633 if self.kernel_banner:
634 self._append_plain_text(self.kernel_banner)
634 self._append_plain_text(self.kernel_banner)
635
635
636 # update output marker for stdout/stderr, so that startup
636 # update output marker for stdout/stderr, so that startup
637 # messages appear after banner:
637 # messages appear after banner:
638 self._append_before_prompt_pos = self._get_cursor().position()
638 self._append_before_prompt_pos = self._get_cursor().position()
639 self._show_interpreter_prompt()
639 self._show_interpreter_prompt()
640
640
641 def restart_kernel(self, message, now=False):
641 def restart_kernel(self, message, now=False):
642 """ Attempts to restart the running kernel.
642 """ Attempts to restart the running kernel.
643 """
643 """
644 # FIXME: now should be configurable via a checkbox in the dialog. Right
644 # FIXME: now should be configurable via a checkbox in the dialog. Right
645 # now at least the heartbeat path sets it to True and the manual restart
645 # now at least the heartbeat path sets it to True and the manual restart
646 # to False. But those should just be the pre-selected states of a
646 # to False. But those should just be the pre-selected states of a
647 # checkbox that the user could override if so desired. But I don't know
647 # checkbox that the user could override if so desired. But I don't know
648 # enough Qt to go implementing the checkbox now.
648 # enough Qt to go implementing the checkbox now.
649
649
650 if self.custom_restart:
650 if self.custom_restart:
651 self.custom_restart_requested.emit()
651 self.custom_restart_requested.emit()
652 return
652 return
653
653
654 if self.kernel_manager:
654 if self.kernel_manager:
655 # Pause the heart beat channel to prevent further warnings.
655 # Pause the heart beat channel to prevent further warnings.
656 self.kernel_client.hb_channel.pause()
656 self.kernel_client.hb_channel.pause()
657
657
658 # Prompt the user to restart the kernel. Un-pause the heartbeat if
658 # Prompt the user to restart the kernel. Un-pause the heartbeat if
659 # they decline. (If they accept, the heartbeat will be un-paused
659 # they decline. (If they accept, the heartbeat will be un-paused
660 # automatically when the kernel is restarted.)
660 # automatically when the kernel is restarted.)
661 if self.confirm_restart:
661 if self.confirm_restart:
662 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
662 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
663 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
663 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
664 message, buttons)
664 message, buttons)
665 do_restart = result == QtGui.QMessageBox.Yes
665 do_restart = result == QtGui.QMessageBox.Yes
666 else:
666 else:
667 # confirm_restart is False, so we don't need to ask user
667 # confirm_restart is False, so we don't need to ask user
668 # anything, just do the restart
668 # anything, just do the restart
669 do_restart = True
669 do_restart = True
670 if do_restart:
670 if do_restart:
671 try:
671 try:
672 self.kernel_manager.restart_kernel(now=now)
672 self.kernel_manager.restart_kernel(now=now)
673 except RuntimeError as e:
673 except RuntimeError as e:
674 self._append_plain_text(
674 self._append_plain_text(
675 'Error restarting kernel: %s\n' % e,
675 'Error restarting kernel: %s\n' % e,
676 before_prompt=True
676 before_prompt=True
677 )
677 )
678 else:
678 else:
679 self._append_html("<br>Restarting kernel...\n<hr><br>",
679 self._append_html("<br>Restarting kernel...\n<hr><br>",
680 before_prompt=True,
680 before_prompt=True,
681 )
681 )
682 else:
682 else:
683 self.kernel_client.hb_channel.unpause()
683 self.kernel_client.hb_channel.unpause()
684
684
685 else:
685 else:
686 self._append_plain_text(
686 self._append_plain_text(
687 'Cannot restart a Kernel I did not start\n',
687 'Cannot restart a Kernel I did not start\n',
688 before_prompt=True
688 before_prompt=True
689 )
689 )
690
690
691 def append_stream(self, text):
691 def append_stream(self, text):
692 """Appends text to the output stream."""
692 """Appends text to the output stream."""
693 # Most consoles treat tabs as being 8 space characters. Convert tabs
693 # Most consoles treat tabs as being 8 space characters. Convert tabs
694 # to spaces so that output looks as expected regardless of this
694 # to spaces so that output looks as expected regardless of this
695 # widget's tab width.
695 # widget's tab width.
696 text = text.expandtabs(8)
696 text = text.expandtabs(8)
697 self._append_plain_text(text, before_prompt=True)
697 self._append_plain_text(text, before_prompt=True)
698 self._control.moveCursor(QtGui.QTextCursor.End)
698 self._control.moveCursor(QtGui.QTextCursor.End)
699
699
700 def flush_clearoutput(self):
700 def flush_clearoutput(self):
701 """If a clearoutput is pending, execute it."""
701 """If a clearoutput is pending, execute it."""
702 if self._pending_clearoutput:
702 if self._pending_clearoutput:
703 self._pending_clearoutput = False
703 self._pending_clearoutput = False
704 self.clear_output()
704 self.clear_output()
705
705
706 def clear_output(self):
706 def clear_output(self):
707 """Clears the current line of output."""
707 """Clears the current line of output."""
708 cursor = self._control.textCursor()
708 cursor = self._control.textCursor()
709 cursor.beginEditBlock()
709 cursor.beginEditBlock()
710 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
710 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
711 cursor.insertText('')
711 cursor.insertText('')
712 cursor.endEditBlock()
712 cursor.endEditBlock()
713
713
714 #---------------------------------------------------------------------------
714 #---------------------------------------------------------------------------
715 # 'FrontendWidget' protected interface
715 # 'FrontendWidget' protected interface
716 #---------------------------------------------------------------------------
716 #---------------------------------------------------------------------------
717
717
718 def _call_tip(self):
718 def _call_tip(self):
719 """ Shows a call tip, if appropriate, at the current cursor location.
719 """ Shows a call tip, if appropriate, at the current cursor location.
720 """
720 """
721 # Decide if it makes sense to show a call tip
721 # Decide if it makes sense to show a call tip
722 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
722 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
723 return False
723 return False
724 cursor_pos = self._get_input_buffer_cursor_pos()
724 cursor_pos = self._get_input_buffer_cursor_pos()
725 code = self.input_buffer
725 code = self.input_buffer
726 # Send the metadata request to the kernel
726 # Send the metadata request to the kernel
727 msg_id = self.kernel_client.inspect(code, cursor_pos)
727 msg_id = self.kernel_client.inspect(code, cursor_pos)
728 pos = self._get_cursor().position()
728 pos = self._get_cursor().position()
729 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
729 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
730 return True
730 return True
731
731
732 def _complete(self):
732 def _complete(self):
733 """ Performs completion at the current cursor location.
733 """ Performs completion at the current cursor location.
734 """
734 """
735 context = self._get_context()
735 context = self._get_context()
736 if context:
736 if context:
737 # Send the completion request to the kernel
737 # Send the completion request to the kernel
738 msg_id = self.kernel_client.complete(
738 msg_id = self.kernel_client.complete(
739 code=self.input_buffer,
739 code=self.input_buffer,
740 cursor_pos=self._get_input_buffer_cursor_pos(),
740 cursor_pos=self._get_input_buffer_cursor_pos(),
741 )
741 )
742 pos = self._get_cursor().position()
742 pos = self._get_cursor().position()
743 info = self._CompletionRequest(msg_id, pos)
743 info = self._CompletionRequest(msg_id, pos)
744 self._request_info['complete'] = info
744 self._request_info['complete'] = info
745
745
746 def _get_context(self, cursor=None):
746 def _get_context(self, cursor=None):
747 """ Gets the context for the specified cursor (or the current cursor
747 """ Gets the context for the specified cursor (or the current cursor
748 if none is specified).
748 if none is specified).
749 """
749 """
750 if cursor is None:
750 if cursor is None:
751 cursor = self._get_cursor()
751 cursor = self._get_cursor()
752 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
752 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
753 QtGui.QTextCursor.KeepAnchor)
753 QtGui.QTextCursor.KeepAnchor)
754 text = cursor.selection().toPlainText()
754 text = cursor.selection().toPlainText()
755 return self._completion_lexer.get_context(text)
755 return self._completion_lexer.get_context(text)
756
756
757 def _process_execute_abort(self, msg):
757 def _process_execute_abort(self, msg):
758 """ Process a reply for an aborted execution request.
758 """ Process a reply for an aborted execution request.
759 """
759 """
760 self._append_plain_text("ERROR: execution aborted\n")
760 self._append_plain_text("ERROR: execution aborted\n")
761
761
762 def _process_execute_error(self, msg):
762 def _process_execute_error(self, msg):
763 """ Process a reply for an execution request that resulted in an error.
763 """ Process a reply for an execution request that resulted in an error.
764 """
764 """
765 content = msg['content']
765 content = msg['content']
766 # If a SystemExit is passed along, this means exit() was called - also
766 # If a SystemExit is passed along, this means exit() was called - also
767 # all the ipython %exit magic syntax of '-k' to be used to keep
767 # all the ipython %exit magic syntax of '-k' to be used to keep
768 # the kernel running
768 # the kernel running
769 if content['ename']=='SystemExit':
769 if content['ename']=='SystemExit':
770 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
770 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
771 self._keep_kernel_on_exit = keepkernel
771 self._keep_kernel_on_exit = keepkernel
772 self.exit_requested.emit(self)
772 self.exit_requested.emit(self)
773 else:
773 else:
774 traceback = ''.join(content['traceback'])
774 traceback = ''.join(content['traceback'])
775 self._append_plain_text(traceback)
775 self._append_plain_text(traceback)
776
776
777 def _process_execute_ok(self, msg):
777 def _process_execute_ok(self, msg):
778 """ Process a reply for a successful execution request.
778 """ Process a reply for a successful execution request.
779 """
779 """
780 payload = msg['content']['payload']
780 payload = msg['content']['payload']
781 for item in payload:
781 for item in payload:
782 if not self._process_execute_payload(item):
782 if not self._process_execute_payload(item):
783 warning = 'Warning: received unknown payload of type %s'
783 warning = 'Warning: received unknown payload of type %s'
784 print(warning % repr(item['source']))
784 print(warning % repr(item['source']))
785
785
786 def _process_execute_payload(self, item):
786 def _process_execute_payload(self, item):
787 """ Process a single payload item from the list of payload items in an
787 """ Process a single payload item from the list of payload items in an
788 execution reply. Returns whether the payload was handled.
788 execution reply. Returns whether the payload was handled.
789 """
789 """
790 # The basic FrontendWidget doesn't handle payloads, as they are a
790 # The basic FrontendWidget doesn't handle payloads, as they are a
791 # mechanism for going beyond the standard Python interpreter model.
791 # mechanism for going beyond the standard Python interpreter model.
792 return False
792 return False
793
793
794 def _show_interpreter_prompt(self):
794 def _show_interpreter_prompt(self):
795 """ Shows a prompt for the interpreter.
795 """ Shows a prompt for the interpreter.
796 """
796 """
797 self._show_prompt('>>> ')
797 self._show_prompt('>>> ')
798
798
799 def _show_interpreter_prompt_for_reply(self, msg):
799 def _show_interpreter_prompt_for_reply(self, msg):
800 """ Shows a prompt for the interpreter given an 'execute_reply' message.
800 """ Shows a prompt for the interpreter given an 'execute_reply' message.
801 """
801 """
802 self._show_interpreter_prompt()
802 self._show_interpreter_prompt()
803
803
804 #------ Signal handlers ----------------------------------------------------
804 #------ Signal handlers ----------------------------------------------------
805
805
806 def _document_contents_change(self, position, removed, added):
806 def _document_contents_change(self, position, removed, added):
807 """ Called whenever the document's content changes. Display a call tip
807 """ Called whenever the document's content changes. Display a call tip
808 if appropriate.
808 if appropriate.
809 """
809 """
810 # Calculate where the cursor should be *after* the change:
810 # Calculate where the cursor should be *after* the change:
811 position += added
811 position += added
812
812
813 document = self._control.document()
813 document = self._control.document()
814 if position == self._get_cursor().position():
814 if position == self._get_cursor().position():
815 self._call_tip()
815 self._call_tip()
816
816
817 #------ Trait default initializers -----------------------------------------
817 #------ Trait default initializers -----------------------------------------
818
818
819 def _banner_default(self):
819 def _banner_default(self):
820 """ Returns the standard Python banner.
820 """ Returns the standard Python banner.
821 """
821 """
822 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
822 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
823 '"license" for more information.'
823 '"license" for more information.'
824 return banner % (sys.version, sys.platform)
824 return banner % (sys.version, sys.platform)
@@ -1,581 +1,591 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 + 1)
155 end = max(end, start + 1)
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
222
223 def _handle_execute_input(self, msg):
224 """Handle an execute_input message"""
225 self.log.debug("execute_input: %s", msg.get('content', ''))
226 if self.include_output(msg):
227 content = msg['content']
228 prompt_number = content.get('execution_count', 0)
229 self._append_html(self._make_in_prompt(prompt_number), True)
230 self._append_plain_text(content['code'], True)
231
232
223 def _handle_execute_result(self, msg):
233 def _handle_execute_result(self, msg):
224 """ Reimplemented for IPython-style "display hook".
234 """ Reimplemented for IPython-style "display hook".
225 """
235 """
226 self.log.debug("execute_result: %s", msg.get('content', ''))
236 self.log.debug("execute_result: %s", msg.get('content', ''))
227 if not self._hidden and self._is_from_this_session(msg):
237 if self.include_output(msg):
228 self.flush_clearoutput()
238 self.flush_clearoutput()
229 content = msg['content']
239 content = msg['content']
230 prompt_number = content.get('execution_count', 0)
240 prompt_number = content.get('execution_count', 0)
231 data = content['data']
241 data = content['data']
232 if 'text/plain' in data:
242 if 'text/plain' in data:
233 self._append_plain_text(self.output_sep, True)
243 self._append_plain_text(self.output_sep, True)
234 self._append_html(self._make_out_prompt(prompt_number), True)
244 self._append_html(self._make_out_prompt(prompt_number), True)
235 text = data['text/plain']
245 text = data['text/plain']
236 # If the repr is multiline, make sure we start on a new line,
246 # If the repr is multiline, make sure we start on a new line,
237 # so that its lines are aligned.
247 # so that its lines are aligned.
238 if "\n" in text and not self.output_sep.endswith("\n"):
248 if "\n" in text and not self.output_sep.endswith("\n"):
239 self._append_plain_text('\n', True)
249 self._append_plain_text('\n', True)
240 self._append_plain_text(text + self.output_sep2, True)
250 self._append_plain_text(text + self.output_sep2, True)
241
251
242 def _handle_display_data(self, msg):
252 def _handle_display_data(self, msg):
243 """ The base handler for the ``display_data`` message.
253 """ The base handler for the ``display_data`` message.
244 """
254 """
245 self.log.debug("display: %s", msg.get('content', ''))
255 self.log.debug("display: %s", msg.get('content', ''))
246 # For now, we don't display data from other frontends, but we
256 # For now, we don't display data from other frontends, but we
247 # eventually will as this allows all frontends to monitor the display
257 # 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.
258 # 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):
259 if self.include_output(msg):
250 self.flush_clearoutput()
260 self.flush_clearoutput()
251 data = msg['content']['data']
261 data = msg['content']['data']
252 metadata = msg['content']['metadata']
262 metadata = msg['content']['metadata']
253 # In the regular IPythonWidget, we simply print the plain text
263 # In the regular IPythonWidget, we simply print the plain text
254 # representation.
264 # representation.
255 if 'text/plain' in data:
265 if 'text/plain' in data:
256 text = data['text/plain']
266 text = data['text/plain']
257 self._append_plain_text(text, True)
267 self._append_plain_text(text, True)
258 # This newline seems to be needed for text and html output.
268 # This newline seems to be needed for text and html output.
259 self._append_plain_text(u'\n', True)
269 self._append_plain_text(u'\n', True)
260
270
261 def _handle_kernel_info_reply(self, rep):
271 def _handle_kernel_info_reply(self, rep):
262 """Handle kernel info replies."""
272 """Handle kernel info replies."""
263 content = rep['content']
273 content = rep['content']
264 if not self._guiref_loaded:
274 if not self._guiref_loaded:
265 if content.get('language') == 'python':
275 if content.get('language') == 'python':
266 self._load_guiref_magic()
276 self._load_guiref_magic()
267 self._guiref_loaded = True
277 self._guiref_loaded = True
268
278
269 self.kernel_banner = content.get('banner', '')
279 self.kernel_banner = content.get('banner', '')
270 if self._starting:
280 if self._starting:
271 # finish handling started channels
281 # finish handling started channels
272 self._starting = False
282 self._starting = False
273 super(IPythonWidget, self)._started_channels()
283 super(IPythonWidget, self)._started_channels()
274
284
275 def _started_channels(self):
285 def _started_channels(self):
276 """Reimplemented to make a history request and load %guiref."""
286 """Reimplemented to make a history request and load %guiref."""
277 self._starting = True
287 self._starting = True
278 # The reply will trigger %guiref load provided language=='python'
288 # The reply will trigger %guiref load provided language=='python'
279 self.kernel_client.kernel_info()
289 self.kernel_client.kernel_info()
280
290
281 self.kernel_client.shell_channel.history(hist_access_type='tail',
291 self.kernel_client.shell_channel.history(hist_access_type='tail',
282 n=1000)
292 n=1000)
283
293
284 def _load_guiref_magic(self):
294 def _load_guiref_magic(self):
285 """Load %guiref magic."""
295 """Load %guiref magic."""
286 self.kernel_client.shell_channel.execute('\n'.join([
296 self.kernel_client.shell_channel.execute('\n'.join([
287 "try:",
297 "try:",
288 " _usage",
298 " _usage",
289 "except:",
299 "except:",
290 " from IPython.core import usage as _usage",
300 " from IPython.core import usage as _usage",
291 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
301 " get_ipython().register_magic_function(_usage.page_guiref, 'line', 'guiref')",
292 " del _usage",
302 " del _usage",
293 ]), silent=True)
303 ]), silent=True)
294
304
295 #---------------------------------------------------------------------------
305 #---------------------------------------------------------------------------
296 # 'ConsoleWidget' public interface
306 # 'ConsoleWidget' public interface
297 #---------------------------------------------------------------------------
307 #---------------------------------------------------------------------------
298
308
299 #---------------------------------------------------------------------------
309 #---------------------------------------------------------------------------
300 # 'FrontendWidget' public interface
310 # 'FrontendWidget' public interface
301 #---------------------------------------------------------------------------
311 #---------------------------------------------------------------------------
302
312
303 def execute_file(self, path, hidden=False):
313 def execute_file(self, path, hidden=False):
304 """ Reimplemented to use the 'run' magic.
314 """ Reimplemented to use the 'run' magic.
305 """
315 """
306 # Use forward slashes on Windows to avoid escaping each separator.
316 # Use forward slashes on Windows to avoid escaping each separator.
307 if sys.platform == 'win32':
317 if sys.platform == 'win32':
308 path = os.path.normpath(path).replace('\\', '/')
318 path = os.path.normpath(path).replace('\\', '/')
309
319
310 # Perhaps we should not be using %run directly, but while we
320 # Perhaps we should not be using %run directly, but while we
311 # are, it is necessary to quote or escape filenames containing spaces
321 # are, it is necessary to quote or escape filenames containing spaces
312 # or quotes.
322 # or quotes.
313
323
314 # In earlier code here, to minimize escaping, we sometimes quoted the
324 # In earlier code here, to minimize escaping, we sometimes quoted the
315 # filename with single quotes. But to do this, this code must be
325 # filename with single quotes. But to do this, this code must be
316 # platform-aware, because run uses shlex rather than python string
326 # platform-aware, because run uses shlex rather than python string
317 # parsing, so that:
327 # parsing, so that:
318 # * In Win: single quotes can be used in the filename without quoting,
328 # * In Win: single quotes can be used in the filename without quoting,
319 # and we cannot use single quotes to quote the filename.
329 # and we cannot use single quotes to quote the filename.
320 # * In *nix: we can escape double quotes in a double quoted filename,
330 # * In *nix: we can escape double quotes in a double quoted filename,
321 # but can't escape single quotes in a single quoted filename.
331 # but can't escape single quotes in a single quoted filename.
322
332
323 # So to keep this code non-platform-specific and simple, we now only
333 # So to keep this code non-platform-specific and simple, we now only
324 # use double quotes to quote filenames, and escape when needed:
334 # use double quotes to quote filenames, and escape when needed:
325 if ' ' in path or "'" in path or '"' in path:
335 if ' ' in path or "'" in path or '"' in path:
326 path = '"%s"' % path.replace('"', '\\"')
336 path = '"%s"' % path.replace('"', '\\"')
327 self.execute('%%run %s' % path, hidden=hidden)
337 self.execute('%%run %s' % path, hidden=hidden)
328
338
329 #---------------------------------------------------------------------------
339 #---------------------------------------------------------------------------
330 # 'FrontendWidget' protected interface
340 # 'FrontendWidget' protected interface
331 #---------------------------------------------------------------------------
341 #---------------------------------------------------------------------------
332
342
333 def _process_execute_error(self, msg):
343 def _process_execute_error(self, msg):
334 """ Reimplemented for IPython-style traceback formatting.
344 """ Reimplemented for IPython-style traceback formatting.
335 """
345 """
336 content = msg['content']
346 content = msg['content']
337 traceback = '\n'.join(content['traceback']) + '\n'
347 traceback = '\n'.join(content['traceback']) + '\n'
338 if False:
348 if False:
339 # FIXME: For now, tracebacks come as plain text, so we can't use
349 # FIXME: For now, tracebacks come as plain text, so we can't use
340 # the html renderer yet. Once we refactor ultratb to produce
350 # the html renderer yet. Once we refactor ultratb to produce
341 # properly styled tracebacks, this branch should be the default
351 # properly styled tracebacks, this branch should be the default
342 traceback = traceback.replace(' ', '&nbsp;')
352 traceback = traceback.replace(' ', '&nbsp;')
343 traceback = traceback.replace('\n', '<br/>')
353 traceback = traceback.replace('\n', '<br/>')
344
354
345 ename = content['ename']
355 ename = content['ename']
346 ename_styled = '<span class="error">%s</span>' % ename
356 ename_styled = '<span class="error">%s</span>' % ename
347 traceback = traceback.replace(ename, ename_styled)
357 traceback = traceback.replace(ename, ename_styled)
348
358
349 self._append_html(traceback)
359 self._append_html(traceback)
350 else:
360 else:
351 # This is the fallback for now, using plain text with ansi escapes
361 # This is the fallback for now, using plain text with ansi escapes
352 self._append_plain_text(traceback)
362 self._append_plain_text(traceback)
353
363
354 def _process_execute_payload(self, item):
364 def _process_execute_payload(self, item):
355 """ Reimplemented to dispatch payloads to handler methods.
365 """ Reimplemented to dispatch payloads to handler methods.
356 """
366 """
357 handler = self._payload_handlers.get(item['source'])
367 handler = self._payload_handlers.get(item['source'])
358 if handler is None:
368 if handler is None:
359 # We have no handler for this type of payload, simply ignore it
369 # We have no handler for this type of payload, simply ignore it
360 return False
370 return False
361 else:
371 else:
362 handler(item)
372 handler(item)
363 return True
373 return True
364
374
365 def _show_interpreter_prompt(self, number=None):
375 def _show_interpreter_prompt(self, number=None):
366 """ Reimplemented for IPython-style prompts.
376 """ Reimplemented for IPython-style prompts.
367 """
377 """
368 # If a number was not specified, make a prompt number request.
378 # If a number was not specified, make a prompt number request.
369 if number is None:
379 if number is None:
370 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
380 msg_id = self.kernel_client.shell_channel.execute('', silent=True)
371 info = self._ExecutionRequest(msg_id, 'prompt')
381 info = self._ExecutionRequest(msg_id, 'prompt')
372 self._request_info['execute'][msg_id] = info
382 self._request_info['execute'][msg_id] = info
373 return
383 return
374
384
375 # Show a new prompt and save information about it so that it can be
385 # 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.
386 # updated later if the prompt number turns out to be wrong.
377 self._prompt_sep = self.input_sep
387 self._prompt_sep = self.input_sep
378 self._show_prompt(self._make_in_prompt(number), html=True)
388 self._show_prompt(self._make_in_prompt(number), html=True)
379 block = self._control.document().lastBlock()
389 block = self._control.document().lastBlock()
380 length = len(self._prompt)
390 length = len(self._prompt)
381 self._previous_prompt_obj = self._PromptBlock(block, length, number)
391 self._previous_prompt_obj = self._PromptBlock(block, length, number)
382
392
383 # Update continuation prompt to reflect (possibly) new prompt length.
393 # Update continuation prompt to reflect (possibly) new prompt length.
384 self._set_continuation_prompt(
394 self._set_continuation_prompt(
385 self._make_continuation_prompt(self._prompt), html=True)
395 self._make_continuation_prompt(self._prompt), html=True)
386
396
387 def _show_interpreter_prompt_for_reply(self, msg):
397 def _show_interpreter_prompt_for_reply(self, msg):
388 """ Reimplemented for IPython-style prompts.
398 """ Reimplemented for IPython-style prompts.
389 """
399 """
390 # Update the old prompt number if necessary.
400 # Update the old prompt number if necessary.
391 content = msg['content']
401 content = msg['content']
392 # abort replies do not have any keys:
402 # abort replies do not have any keys:
393 if content['status'] == 'aborted':
403 if content['status'] == 'aborted':
394 if self._previous_prompt_obj:
404 if self._previous_prompt_obj:
395 previous_prompt_number = self._previous_prompt_obj.number
405 previous_prompt_number = self._previous_prompt_obj.number
396 else:
406 else:
397 previous_prompt_number = 0
407 previous_prompt_number = 0
398 else:
408 else:
399 previous_prompt_number = content['execution_count']
409 previous_prompt_number = content['execution_count']
400 if self._previous_prompt_obj and \
410 if self._previous_prompt_obj and \
401 self._previous_prompt_obj.number != previous_prompt_number:
411 self._previous_prompt_obj.number != previous_prompt_number:
402 block = self._previous_prompt_obj.block
412 block = self._previous_prompt_obj.block
403
413
404 # Make sure the prompt block has not been erased.
414 # Make sure the prompt block has not been erased.
405 if block.isValid() and block.text():
415 if block.isValid() and block.text():
406
416
407 # Remove the old prompt and insert a new prompt.
417 # Remove the old prompt and insert a new prompt.
408 cursor = QtGui.QTextCursor(block)
418 cursor = QtGui.QTextCursor(block)
409 cursor.movePosition(QtGui.QTextCursor.Right,
419 cursor.movePosition(QtGui.QTextCursor.Right,
410 QtGui.QTextCursor.KeepAnchor,
420 QtGui.QTextCursor.KeepAnchor,
411 self._previous_prompt_obj.length)
421 self._previous_prompt_obj.length)
412 prompt = self._make_in_prompt(previous_prompt_number)
422 prompt = self._make_in_prompt(previous_prompt_number)
413 self._prompt = self._insert_html_fetching_plain_text(
423 self._prompt = self._insert_html_fetching_plain_text(
414 cursor, prompt)
424 cursor, prompt)
415
425
416 # When the HTML is inserted, Qt blows away the syntax
426 # When the HTML is inserted, Qt blows away the syntax
417 # highlighting for the line, so we need to rehighlight it.
427 # highlighting for the line, so we need to rehighlight it.
418 self._highlighter.rehighlightBlock(cursor.block())
428 self._highlighter.rehighlightBlock(cursor.block())
419
429
420 self._previous_prompt_obj = None
430 self._previous_prompt_obj = None
421
431
422 # Show a new prompt with the kernel's estimated prompt number.
432 # Show a new prompt with the kernel's estimated prompt number.
423 self._show_interpreter_prompt(previous_prompt_number + 1)
433 self._show_interpreter_prompt(previous_prompt_number + 1)
424
434
425 #---------------------------------------------------------------------------
435 #---------------------------------------------------------------------------
426 # 'IPythonWidget' interface
436 # 'IPythonWidget' interface
427 #---------------------------------------------------------------------------
437 #---------------------------------------------------------------------------
428
438
429 def set_default_style(self, colors='lightbg'):
439 def set_default_style(self, colors='lightbg'):
430 """ Sets the widget style to the class defaults.
440 """ Sets the widget style to the class defaults.
431
441
432 Parameters
442 Parameters
433 ----------
443 ----------
434 colors : str, optional (default lightbg)
444 colors : str, optional (default lightbg)
435 Whether to use the default IPython light background or dark
445 Whether to use the default IPython light background or dark
436 background or B&W style.
446 background or B&W style.
437 """
447 """
438 colors = colors.lower()
448 colors = colors.lower()
439 if colors=='lightbg':
449 if colors=='lightbg':
440 self.style_sheet = styles.default_light_style_sheet
450 self.style_sheet = styles.default_light_style_sheet
441 self.syntax_style = styles.default_light_syntax_style
451 self.syntax_style = styles.default_light_syntax_style
442 elif colors=='linux':
452 elif colors=='linux':
443 self.style_sheet = styles.default_dark_style_sheet
453 self.style_sheet = styles.default_dark_style_sheet
444 self.syntax_style = styles.default_dark_syntax_style
454 self.syntax_style = styles.default_dark_syntax_style
445 elif colors=='nocolor':
455 elif colors=='nocolor':
446 self.style_sheet = styles.default_bw_style_sheet
456 self.style_sheet = styles.default_bw_style_sheet
447 self.syntax_style = styles.default_bw_syntax_style
457 self.syntax_style = styles.default_bw_syntax_style
448 else:
458 else:
449 raise KeyError("No such color scheme: %s"%colors)
459 raise KeyError("No such color scheme: %s"%colors)
450
460
451 #---------------------------------------------------------------------------
461 #---------------------------------------------------------------------------
452 # 'IPythonWidget' protected interface
462 # 'IPythonWidget' protected interface
453 #---------------------------------------------------------------------------
463 #---------------------------------------------------------------------------
454
464
455 def _edit(self, filename, line=None):
465 def _edit(self, filename, line=None):
456 """ Opens a Python script for editing.
466 """ Opens a Python script for editing.
457
467
458 Parameters
468 Parameters
459 ----------
469 ----------
460 filename : str
470 filename : str
461 A path to a local system file.
471 A path to a local system file.
462
472
463 line : int, optional
473 line : int, optional
464 A line of interest in the file.
474 A line of interest in the file.
465 """
475 """
466 if self.custom_edit:
476 if self.custom_edit:
467 self.custom_edit_requested.emit(filename, line)
477 self.custom_edit_requested.emit(filename, line)
468 elif not self.editor:
478 elif not self.editor:
469 self._append_plain_text('No default editor available.\n'
479 self._append_plain_text('No default editor available.\n'
470 'Specify a GUI text editor in the `IPythonWidget.editor` '
480 'Specify a GUI text editor in the `IPythonWidget.editor` '
471 'configurable to enable the %edit magic')
481 'configurable to enable the %edit magic')
472 else:
482 else:
473 try:
483 try:
474 filename = '"%s"' % filename
484 filename = '"%s"' % filename
475 if line and self.editor_line:
485 if line and self.editor_line:
476 command = self.editor_line.format(filename=filename,
486 command = self.editor_line.format(filename=filename,
477 line=line)
487 line=line)
478 else:
488 else:
479 try:
489 try:
480 command = self.editor.format()
490 command = self.editor.format()
481 except KeyError:
491 except KeyError:
482 command = self.editor.format(filename=filename)
492 command = self.editor.format(filename=filename)
483 else:
493 else:
484 command += ' ' + filename
494 command += ' ' + filename
485 except KeyError:
495 except KeyError:
486 self._append_plain_text('Invalid editor command.\n')
496 self._append_plain_text('Invalid editor command.\n')
487 else:
497 else:
488 try:
498 try:
489 Popen(command, shell=True)
499 Popen(command, shell=True)
490 except OSError:
500 except OSError:
491 msg = 'Opening editor with command "%s" failed.\n'
501 msg = 'Opening editor with command "%s" failed.\n'
492 self._append_plain_text(msg % command)
502 self._append_plain_text(msg % command)
493
503
494 def _make_in_prompt(self, number):
504 def _make_in_prompt(self, number):
495 """ Given a prompt number, returns an HTML In prompt.
505 """ Given a prompt number, returns an HTML In prompt.
496 """
506 """
497 try:
507 try:
498 body = self.in_prompt % number
508 body = self.in_prompt % number
499 except TypeError:
509 except TypeError:
500 # allow in_prompt to leave out number, e.g. '>>> '
510 # allow in_prompt to leave out number, e.g. '>>> '
501 from xml.sax.saxutils import escape
511 from xml.sax.saxutils import escape
502 body = escape(self.in_prompt)
512 body = escape(self.in_prompt)
503 return '<span class="in-prompt">%s</span>' % body
513 return '<span class="in-prompt">%s</span>' % body
504
514
505 def _make_continuation_prompt(self, prompt):
515 def _make_continuation_prompt(self, prompt):
506 """ Given a plain text version of an In prompt, returns an HTML
516 """ Given a plain text version of an In prompt, returns an HTML
507 continuation prompt.
517 continuation prompt.
508 """
518 """
509 end_chars = '...: '
519 end_chars = '...: '
510 space_count = len(prompt.lstrip('\n')) - len(end_chars)
520 space_count = len(prompt.lstrip('\n')) - len(end_chars)
511 body = '&nbsp;' * space_count + end_chars
521 body = '&nbsp;' * space_count + end_chars
512 return '<span class="in-prompt">%s</span>' % body
522 return '<span class="in-prompt">%s</span>' % body
513
523
514 def _make_out_prompt(self, number):
524 def _make_out_prompt(self, number):
515 """ Given a prompt number, returns an HTML Out prompt.
525 """ Given a prompt number, returns an HTML Out prompt.
516 """
526 """
517 try:
527 try:
518 body = self.out_prompt % number
528 body = self.out_prompt % number
519 except TypeError:
529 except TypeError:
520 # allow out_prompt to leave out number, e.g. '<<< '
530 # allow out_prompt to leave out number, e.g. '<<< '
521 from xml.sax.saxutils import escape
531 from xml.sax.saxutils import escape
522 body = escape(self.out_prompt)
532 body = escape(self.out_prompt)
523 return '<span class="out-prompt">%s</span>' % body
533 return '<span class="out-prompt">%s</span>' % body
524
534
525 #------ Payload handlers --------------------------------------------------
535 #------ Payload handlers --------------------------------------------------
526
536
527 # Payload handlers with a generic interface: each takes the opaque payload
537 # Payload handlers with a generic interface: each takes the opaque payload
528 # dict, unpacks it and calls the underlying functions with the necessary
538 # dict, unpacks it and calls the underlying functions with the necessary
529 # arguments.
539 # arguments.
530
540
531 def _handle_payload_edit(self, item):
541 def _handle_payload_edit(self, item):
532 self._edit(item['filename'], item['line_number'])
542 self._edit(item['filename'], item['line_number'])
533
543
534 def _handle_payload_exit(self, item):
544 def _handle_payload_exit(self, item):
535 self._keep_kernel_on_exit = item['keepkernel']
545 self._keep_kernel_on_exit = item['keepkernel']
536 self.exit_requested.emit(self)
546 self.exit_requested.emit(self)
537
547
538 def _handle_payload_next_input(self, item):
548 def _handle_payload_next_input(self, item):
539 self.input_buffer = item['text']
549 self.input_buffer = item['text']
540
550
541 def _handle_payload_page(self, item):
551 def _handle_payload_page(self, item):
542 # Since the plain text widget supports only a very small subset of HTML
552 # 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
553 # and we have no control over the HTML source, we only page HTML
544 # payloads in the rich text widget.
554 # payloads in the rich text widget.
545 data = item['data']
555 data = item['data']
546 if 'text/html' in data and self.kind == 'rich':
556 if 'text/html' in data and self.kind == 'rich':
547 self._page(data['text/html'], html=True)
557 self._page(data['text/html'], html=True)
548 else:
558 else:
549 self._page(data['text/plain'], html=False)
559 self._page(data['text/plain'], html=False)
550
560
551 #------ Trait change handlers --------------------------------------------
561 #------ Trait change handlers --------------------------------------------
552
562
553 def _style_sheet_changed(self):
563 def _style_sheet_changed(self):
554 """ Set the style sheets of the underlying widgets.
564 """ Set the style sheets of the underlying widgets.
555 """
565 """
556 self.setStyleSheet(self.style_sheet)
566 self.setStyleSheet(self.style_sheet)
557 if self._control is not None:
567 if self._control is not None:
558 self._control.document().setDefaultStyleSheet(self.style_sheet)
568 self._control.document().setDefaultStyleSheet(self.style_sheet)
559 bg_color = self._control.palette().window().color()
569 bg_color = self._control.palette().window().color()
560 self._ansi_processor.set_background_color(bg_color)
570 self._ansi_processor.set_background_color(bg_color)
561
571
562 if self._page_control is not None:
572 if self._page_control is not None:
563 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
573 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
564
574
565
575
566
576
567 def _syntax_style_changed(self):
577 def _syntax_style_changed(self):
568 """ Set the style for the syntax highlighter.
578 """ Set the style for the syntax highlighter.
569 """
579 """
570 if self._highlighter is None:
580 if self._highlighter is None:
571 # ignore premature calls
581 # ignore premature calls
572 return
582 return
573 if self.syntax_style:
583 if self.syntax_style:
574 self._highlighter.set_style(self.syntax_style)
584 self._highlighter.set_style(self.syntax_style)
575 else:
585 else:
576 self._highlighter.set_style_sheet(self.style_sheet)
586 self._highlighter.set_style_sheet(self.style_sheet)
577
587
578 #------ Trait default initializers -----------------------------------------
588 #------ Trait default initializers -----------------------------------------
579
589
580 def _banner_default(self):
590 def _banner_default(self):
581 return "IPython QtConsole {version}\n".format(version=version)
591 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)
General Comments 0
You need to be logged in to leave comments. Login now