##// END OF EJS Templates
Merge pull request #1363 from minrk/qtstyle...
Min RK -
r6095:2b3bc441 merge
parent child Browse files
Show More
@@ -1,1852 +1,1855 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
8 import os
9 from os.path import commonprefix
9 from os.path import commonprefix
10 import re
10 import re
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13 from unicodedata import category
14
14
15 # System library imports
15 # System library imports
16 from IPython.external.qt import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import LoggingConfigurable
19 from IPython.config.configurable import LoggingConfigurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
20 from IPython.frontend.qt.rich_text import HtmlExporter
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 from IPython.utils.text import columnize
22 from IPython.utils.text import columnize
23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 from ansi_code_processor import QtAnsiCodeProcessor
24 from ansi_code_processor import QtAnsiCodeProcessor
25 from completion_widget import CompletionWidget
25 from completion_widget import CompletionWidget
26 from kill_ring import QtKillRing
26 from kill_ring import QtKillRing
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Functions
29 # Functions
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 def is_letter_or_number(char):
32 def is_letter_or_number(char):
33 """ Returns whether the specified unicode character is a letter or a number.
33 """ Returns whether the specified unicode character is a letter or a number.
34 """
34 """
35 cat = category(char)
35 cat = category(char)
36 return cat.startswith('L') or cat.startswith('N')
36 return cat.startswith('L') or cat.startswith('N')
37
37
38 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
39 # Classes
39 # Classes
40 #-----------------------------------------------------------------------------
40 #-----------------------------------------------------------------------------
41
41
42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 """ An abstract base class for console-type widgets. This class has
43 """ An abstract base class for console-type widgets. This class has
44 functionality for:
44 functionality for:
45
45
46 * Maintaining a prompt and editing region
46 * Maintaining a prompt and editing region
47 * Providing the traditional Unix-style console keyboard shortcuts
47 * Providing the traditional Unix-style console keyboard shortcuts
48 * Performing tab completion
48 * Performing tab completion
49 * Paging text
49 * Paging text
50 * Handling ANSI escape codes
50 * Handling ANSI escape codes
51
51
52 ConsoleWidget also provides a number of utility methods that will be
52 ConsoleWidget also provides a number of utility methods that will be
53 convenient to implementors of a console-style widget.
53 convenient to implementors of a console-style widget.
54 """
54 """
55 __metaclass__ = MetaQObjectHasTraits
55 __metaclass__ = MetaQObjectHasTraits
56
56
57 #------ Configuration ------------------------------------------------------
57 #------ Configuration ------------------------------------------------------
58
58
59 ansi_codes = Bool(True, config=True,
59 ansi_codes = Bool(True, config=True,
60 help="Whether to process ANSI escape codes."
60 help="Whether to process ANSI escape codes."
61 )
61 )
62 buffer_size = Integer(500, config=True,
62 buffer_size = Integer(500, config=True,
63 help="""
63 help="""
64 The maximum number of lines of text before truncation. Specifying a
64 The maximum number of lines of text before truncation. Specifying a
65 non-positive number disables text truncation (not recommended).
65 non-positive number disables text truncation (not recommended).
66 """
66 """
67 )
67 )
68 gui_completion = Bool(False, config=True,
68 gui_completion = Bool(False, config=True,
69 help="""
69 help="""
70 Use a list widget instead of plain text output for tab completion.
70 Use a list widget instead of plain text output for tab completion.
71 """
71 """
72 )
72 )
73 # NOTE: this value can only be specified during initialization.
73 # NOTE: this value can only be specified during initialization.
74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 help="""
75 help="""
76 The type of underlying text widget to use. Valid values are 'plain',
76 The type of underlying text widget to use. Valid values are 'plain',
77 which specifies a QPlainTextEdit, and 'rich', which specifies a
77 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 QTextEdit.
78 QTextEdit.
79 """
79 """
80 )
80 )
81 # NOTE: this value can only be specified during initialization.
81 # NOTE: this value can only be specified during initialization.
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 default_value='inside', config=True,
83 default_value='inside', config=True,
84 help="""
84 help="""
85 The type of paging to use. Valid values are:
85 The type of paging to use. Valid values are:
86
86
87 'inside' : The widget pages like a traditional terminal.
87 'inside' : The widget pages like a traditional terminal.
88 'hsplit' : When paging is requested, the widget is split
88 'hsplit' : When paging is requested, the widget is split
89 horizontally. The top pane contains the console, and the
89 horizontally. The top pane contains the console, and the
90 bottom pane contains the paged text.
90 bottom pane contains the paged text.
91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 used.
92 used.
93 'custom' : No action is taken by the widget beyond emitting a
93 'custom' : No action is taken by the widget beyond emitting a
94 'custom_page_requested(str)' signal.
94 'custom_page_requested(str)' signal.
95 'none' : The text is written directly to the console.
95 'none' : The text is written directly to the console.
96 """)
96 """)
97
97
98 font_family = Unicode(config=True,
98 font_family = Unicode(config=True,
99 help="""The font family to use for the console.
99 help="""The font family to use for the console.
100 On OSX this defaults to Monaco, on Windows the default is
100 On OSX this defaults to Monaco, on Windows the default is
101 Consolas with fallback of Courier, and on other platforms
101 Consolas with fallback of Courier, and on other platforms
102 the default is Monospace.
102 the default is Monospace.
103 """)
103 """)
104 def _font_family_default(self):
104 def _font_family_default(self):
105 if sys.platform == 'win32':
105 if sys.platform == 'win32':
106 # Consolas ships with Vista/Win7, fallback to Courier if needed
106 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 return 'Consolas'
107 return 'Consolas'
108 elif sys.platform == 'darwin':
108 elif sys.platform == 'darwin':
109 # OSX always has Monaco, no need for a fallback
109 # OSX always has Monaco, no need for a fallback
110 return 'Monaco'
110 return 'Monaco'
111 else:
111 else:
112 # Monospace should always exist, no need for a fallback
112 # Monospace should always exist, no need for a fallback
113 return 'Monospace'
113 return 'Monospace'
114
114
115 font_size = Integer(config=True,
115 font_size = Integer(config=True,
116 help="""The font size. If unconfigured, Qt will be entrusted
116 help="""The font size. If unconfigured, Qt will be entrusted
117 with the size of the font.
117 with the size of the font.
118 """)
118 """)
119
119
120 # Whether to override ShortcutEvents for the keybindings defined by this
120 # Whether to override ShortcutEvents for the keybindings defined by this
121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 override_shortcuts = Bool(False)
123 override_shortcuts = Bool(False)
124
124
125 #------ Signals ------------------------------------------------------------
125 #------ Signals ------------------------------------------------------------
126
126
127 # Signals that indicate ConsoleWidget state.
127 # Signals that indicate ConsoleWidget state.
128 copy_available = QtCore.Signal(bool)
128 copy_available = QtCore.Signal(bool)
129 redo_available = QtCore.Signal(bool)
129 redo_available = QtCore.Signal(bool)
130 undo_available = QtCore.Signal(bool)
130 undo_available = QtCore.Signal(bool)
131
131
132 # Signal emitted when paging is needed and the paging style has been
132 # Signal emitted when paging is needed and the paging style has been
133 # specified as 'custom'.
133 # specified as 'custom'.
134 custom_page_requested = QtCore.Signal(object)
134 custom_page_requested = QtCore.Signal(object)
135
135
136 # Signal emitted when the font is changed.
136 # Signal emitted when the font is changed.
137 font_changed = QtCore.Signal(QtGui.QFont)
137 font_changed = QtCore.Signal(QtGui.QFont)
138
138
139 #------ Protected class variables ------------------------------------------
139 #------ Protected class variables ------------------------------------------
140
140
141 # control handles
142 _control = None
143 _page_control = None
144 _splitter = None
145
141 # When the control key is down, these keys are mapped.
146 # When the control key is down, these keys are mapped.
142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
147 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
148 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
149 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
150 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
151 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
152 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
148 if not sys.platform == 'darwin':
153 if not sys.platform == 'darwin':
149 # On OS X, Ctrl-E already does the right thing, whereas End moves the
154 # On OS X, Ctrl-E already does the right thing, whereas End moves the
150 # cursor to the bottom of the buffer.
155 # cursor to the bottom of the buffer.
151 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
156 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
152
157
153 # The shortcuts defined by this widget. We need to keep track of these to
158 # The shortcuts defined by this widget. We need to keep track of these to
154 # support 'override_shortcuts' above.
159 # support 'override_shortcuts' above.
155 _shortcuts = set(_ctrl_down_remap.keys() +
160 _shortcuts = set(_ctrl_down_remap.keys() +
156 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
161 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
157 QtCore.Qt.Key_V ])
162 QtCore.Qt.Key_V ])
158
163
159 #---------------------------------------------------------------------------
164 #---------------------------------------------------------------------------
160 # 'QObject' interface
165 # 'QObject' interface
161 #---------------------------------------------------------------------------
166 #---------------------------------------------------------------------------
162
167
163 def __init__(self, parent=None, **kw):
168 def __init__(self, parent=None, **kw):
164 """ Create a ConsoleWidget.
169 """ Create a ConsoleWidget.
165
170
166 Parameters:
171 Parameters:
167 -----------
172 -----------
168 parent : QWidget, optional [default None]
173 parent : QWidget, optional [default None]
169 The parent for this widget.
174 The parent for this widget.
170 """
175 """
171 QtGui.QWidget.__init__(self, parent)
176 QtGui.QWidget.__init__(self, parent)
172 LoggingConfigurable.__init__(self, **kw)
177 LoggingConfigurable.__init__(self, **kw)
173
178
174 # While scrolling the pager on Mac OS X, it tears badly. The
179 # While scrolling the pager on Mac OS X, it tears badly. The
175 # NativeGesture is platform and perhaps build-specific hence
180 # NativeGesture is platform and perhaps build-specific hence
176 # we take adequate precautions here.
181 # we take adequate precautions here.
177 self._pager_scroll_events = [QtCore.QEvent.Wheel]
182 self._pager_scroll_events = [QtCore.QEvent.Wheel]
178 if hasattr(QtCore.QEvent, 'NativeGesture'):
183 if hasattr(QtCore.QEvent, 'NativeGesture'):
179 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
184 self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
180
185
181 # Create the layout and underlying text widget.
186 # Create the layout and underlying text widget.
182 layout = QtGui.QStackedLayout(self)
187 layout = QtGui.QStackedLayout(self)
183 layout.setContentsMargins(0, 0, 0, 0)
188 layout.setContentsMargins(0, 0, 0, 0)
184 self._control = self._create_control()
189 self._control = self._create_control()
185 self._page_control = None
186 self._splitter = None
187 if self.paging in ('hsplit', 'vsplit'):
190 if self.paging in ('hsplit', 'vsplit'):
188 self._splitter = QtGui.QSplitter()
191 self._splitter = QtGui.QSplitter()
189 if self.paging == 'hsplit':
192 if self.paging == 'hsplit':
190 self._splitter.setOrientation(QtCore.Qt.Horizontal)
193 self._splitter.setOrientation(QtCore.Qt.Horizontal)
191 else:
194 else:
192 self._splitter.setOrientation(QtCore.Qt.Vertical)
195 self._splitter.setOrientation(QtCore.Qt.Vertical)
193 self._splitter.addWidget(self._control)
196 self._splitter.addWidget(self._control)
194 layout.addWidget(self._splitter)
197 layout.addWidget(self._splitter)
195 else:
198 else:
196 layout.addWidget(self._control)
199 layout.addWidget(self._control)
197
200
198 # Create the paging widget, if necessary.
201 # Create the paging widget, if necessary.
199 if self.paging in ('inside', 'hsplit', 'vsplit'):
202 if self.paging in ('inside', 'hsplit', 'vsplit'):
200 self._page_control = self._create_page_control()
203 self._page_control = self._create_page_control()
201 if self._splitter:
204 if self._splitter:
202 self._page_control.hide()
205 self._page_control.hide()
203 self._splitter.addWidget(self._page_control)
206 self._splitter.addWidget(self._page_control)
204 else:
207 else:
205 layout.addWidget(self._page_control)
208 layout.addWidget(self._page_control)
206
209
207 # Initialize protected variables. Some variables contain useful state
210 # Initialize protected variables. Some variables contain useful state
208 # information for subclasses; they should be considered read-only.
211 # information for subclasses; they should be considered read-only.
209 self._append_before_prompt_pos = 0
212 self._append_before_prompt_pos = 0
210 self._ansi_processor = QtAnsiCodeProcessor()
213 self._ansi_processor = QtAnsiCodeProcessor()
211 self._completion_widget = CompletionWidget(self._control)
214 self._completion_widget = CompletionWidget(self._control)
212 self._continuation_prompt = '> '
215 self._continuation_prompt = '> '
213 self._continuation_prompt_html = None
216 self._continuation_prompt_html = None
214 self._executing = False
217 self._executing = False
215 self._filter_drag = False
218 self._filter_drag = False
216 self._filter_resize = False
219 self._filter_resize = False
217 self._html_exporter = HtmlExporter(self._control)
220 self._html_exporter = HtmlExporter(self._control)
218 self._input_buffer_executing = ''
221 self._input_buffer_executing = ''
219 self._input_buffer_pending = ''
222 self._input_buffer_pending = ''
220 self._kill_ring = QtKillRing(self._control)
223 self._kill_ring = QtKillRing(self._control)
221 self._prompt = ''
224 self._prompt = ''
222 self._prompt_html = None
225 self._prompt_html = None
223 self._prompt_pos = 0
226 self._prompt_pos = 0
224 self._prompt_sep = ''
227 self._prompt_sep = ''
225 self._reading = False
228 self._reading = False
226 self._reading_callback = None
229 self._reading_callback = None
227 self._tab_width = 8
230 self._tab_width = 8
228 self._text_completing_pos = 0
231 self._text_completing_pos = 0
229
232
230 # Set a monospaced font.
233 # Set a monospaced font.
231 self.reset_font()
234 self.reset_font()
232
235
233 # Configure actions.
236 # Configure actions.
234 action = QtGui.QAction('Print', None)
237 action = QtGui.QAction('Print', None)
235 action.setEnabled(True)
238 action.setEnabled(True)
236 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
239 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
237 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
240 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
238 # Only override the default if there is a collision.
241 # Only override the default if there is a collision.
239 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
242 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
240 printkey = "Ctrl+Shift+P"
243 printkey = "Ctrl+Shift+P"
241 action.setShortcut(printkey)
244 action.setShortcut(printkey)
242 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
245 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
243 action.triggered.connect(self.print_)
246 action.triggered.connect(self.print_)
244 self.addAction(action)
247 self.addAction(action)
245 self.print_action = action
248 self.print_action = action
246
249
247 action = QtGui.QAction('Save as HTML/XML', None)
250 action = QtGui.QAction('Save as HTML/XML', None)
248 action.setShortcut(QtGui.QKeySequence.Save)
251 action.setShortcut(QtGui.QKeySequence.Save)
249 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
252 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
250 action.triggered.connect(self.export_html)
253 action.triggered.connect(self.export_html)
251 self.addAction(action)
254 self.addAction(action)
252 self.export_action = action
255 self.export_action = action
253
256
254 action = QtGui.QAction('Select All', None)
257 action = QtGui.QAction('Select All', None)
255 action.setEnabled(True)
258 action.setEnabled(True)
256 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
259 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
257 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
260 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
258 # Only override the default if there is a collision.
261 # Only override the default if there is a collision.
259 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
262 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
260 selectall = "Ctrl+Shift+A"
263 selectall = "Ctrl+Shift+A"
261 action.setShortcut(selectall)
264 action.setShortcut(selectall)
262 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
265 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
263 action.triggered.connect(self.select_all)
266 action.triggered.connect(self.select_all)
264 self.addAction(action)
267 self.addAction(action)
265 self.select_all_action = action
268 self.select_all_action = action
266
269
267 self.increase_font_size = QtGui.QAction("Bigger Font",
270 self.increase_font_size = QtGui.QAction("Bigger Font",
268 self,
271 self,
269 shortcut=QtGui.QKeySequence.ZoomIn,
272 shortcut=QtGui.QKeySequence.ZoomIn,
270 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
273 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
271 statusTip="Increase the font size by one point",
274 statusTip="Increase the font size by one point",
272 triggered=self._increase_font_size)
275 triggered=self._increase_font_size)
273 self.addAction(self.increase_font_size)
276 self.addAction(self.increase_font_size)
274
277
275 self.decrease_font_size = QtGui.QAction("Smaller Font",
278 self.decrease_font_size = QtGui.QAction("Smaller Font",
276 self,
279 self,
277 shortcut=QtGui.QKeySequence.ZoomOut,
280 shortcut=QtGui.QKeySequence.ZoomOut,
278 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
281 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
279 statusTip="Decrease the font size by one point",
282 statusTip="Decrease the font size by one point",
280 triggered=self._decrease_font_size)
283 triggered=self._decrease_font_size)
281 self.addAction(self.decrease_font_size)
284 self.addAction(self.decrease_font_size)
282
285
283 self.reset_font_size = QtGui.QAction("Normal Font",
286 self.reset_font_size = QtGui.QAction("Normal Font",
284 self,
287 self,
285 shortcut="Ctrl+0",
288 shortcut="Ctrl+0",
286 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
289 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
287 statusTip="Restore the Normal font size",
290 statusTip="Restore the Normal font size",
288 triggered=self.reset_font)
291 triggered=self.reset_font)
289 self.addAction(self.reset_font_size)
292 self.addAction(self.reset_font_size)
290
293
291
294
292
295
293 def eventFilter(self, obj, event):
296 def eventFilter(self, obj, event):
294 """ Reimplemented to ensure a console-like behavior in the underlying
297 """ Reimplemented to ensure a console-like behavior in the underlying
295 text widgets.
298 text widgets.
296 """
299 """
297 etype = event.type()
300 etype = event.type()
298 if etype == QtCore.QEvent.KeyPress:
301 if etype == QtCore.QEvent.KeyPress:
299
302
300 # Re-map keys for all filtered widgets.
303 # Re-map keys for all filtered widgets.
301 key = event.key()
304 key = event.key()
302 if self._control_key_down(event.modifiers()) and \
305 if self._control_key_down(event.modifiers()) and \
303 key in self._ctrl_down_remap:
306 key in self._ctrl_down_remap:
304 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
307 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
305 self._ctrl_down_remap[key],
308 self._ctrl_down_remap[key],
306 QtCore.Qt.NoModifier)
309 QtCore.Qt.NoModifier)
307 QtGui.qApp.sendEvent(obj, new_event)
310 QtGui.qApp.sendEvent(obj, new_event)
308 return True
311 return True
309
312
310 elif obj == self._control:
313 elif obj == self._control:
311 return self._event_filter_console_keypress(event)
314 return self._event_filter_console_keypress(event)
312
315
313 elif obj == self._page_control:
316 elif obj == self._page_control:
314 return self._event_filter_page_keypress(event)
317 return self._event_filter_page_keypress(event)
315
318
316 # Make middle-click paste safe.
319 # Make middle-click paste safe.
317 elif etype == QtCore.QEvent.MouseButtonRelease and \
320 elif etype == QtCore.QEvent.MouseButtonRelease and \
318 event.button() == QtCore.Qt.MidButton and \
321 event.button() == QtCore.Qt.MidButton and \
319 obj == self._control.viewport():
322 obj == self._control.viewport():
320 cursor = self._control.cursorForPosition(event.pos())
323 cursor = self._control.cursorForPosition(event.pos())
321 self._control.setTextCursor(cursor)
324 self._control.setTextCursor(cursor)
322 self.paste(QtGui.QClipboard.Selection)
325 self.paste(QtGui.QClipboard.Selection)
323 return True
326 return True
324
327
325 # Manually adjust the scrollbars *after* a resize event is dispatched.
328 # Manually adjust the scrollbars *after* a resize event is dispatched.
326 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
329 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
327 self._filter_resize = True
330 self._filter_resize = True
328 QtGui.qApp.sendEvent(obj, event)
331 QtGui.qApp.sendEvent(obj, event)
329 self._adjust_scrollbars()
332 self._adjust_scrollbars()
330 self._filter_resize = False
333 self._filter_resize = False
331 return True
334 return True
332
335
333 # Override shortcuts for all filtered widgets.
336 # Override shortcuts for all filtered widgets.
334 elif etype == QtCore.QEvent.ShortcutOverride and \
337 elif etype == QtCore.QEvent.ShortcutOverride and \
335 self.override_shortcuts and \
338 self.override_shortcuts and \
336 self._control_key_down(event.modifiers()) and \
339 self._control_key_down(event.modifiers()) and \
337 event.key() in self._shortcuts:
340 event.key() in self._shortcuts:
338 event.accept()
341 event.accept()
339
342
340 # Ensure that drags are safe. The problem is that the drag starting
343 # Ensure that drags are safe. The problem is that the drag starting
341 # logic, which determines whether the drag is a Copy or Move, is locked
344 # logic, which determines whether the drag is a Copy or Move, is locked
342 # down in QTextControl. If the widget is editable, which it must be if
345 # down in QTextControl. If the widget is editable, which it must be if
343 # we're not executing, the drag will be a Move. The following hack
346 # we're not executing, the drag will be a Move. The following hack
344 # prevents QTextControl from deleting the text by clearing the selection
347 # prevents QTextControl from deleting the text by clearing the selection
345 # when a drag leave event originating from this widget is dispatched.
348 # when a drag leave event originating from this widget is dispatched.
346 # The fact that we have to clear the user's selection is unfortunate,
349 # The fact that we have to clear the user's selection is unfortunate,
347 # but the alternative--trying to prevent Qt from using its hardwired
350 # but the alternative--trying to prevent Qt from using its hardwired
348 # drag logic and writing our own--is worse.
351 # drag logic and writing our own--is worse.
349 elif etype == QtCore.QEvent.DragEnter and \
352 elif etype == QtCore.QEvent.DragEnter and \
350 obj == self._control.viewport() and \
353 obj == self._control.viewport() and \
351 event.source() == self._control.viewport():
354 event.source() == self._control.viewport():
352 self._filter_drag = True
355 self._filter_drag = True
353 elif etype == QtCore.QEvent.DragLeave and \
356 elif etype == QtCore.QEvent.DragLeave and \
354 obj == self._control.viewport() and \
357 obj == self._control.viewport() and \
355 self._filter_drag:
358 self._filter_drag:
356 cursor = self._control.textCursor()
359 cursor = self._control.textCursor()
357 cursor.clearSelection()
360 cursor.clearSelection()
358 self._control.setTextCursor(cursor)
361 self._control.setTextCursor(cursor)
359 self._filter_drag = False
362 self._filter_drag = False
360
363
361 # Ensure that drops are safe.
364 # Ensure that drops are safe.
362 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
365 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
363 cursor = self._control.cursorForPosition(event.pos())
366 cursor = self._control.cursorForPosition(event.pos())
364 if self._in_buffer(cursor.position()):
367 if self._in_buffer(cursor.position()):
365 text = event.mimeData().text()
368 text = event.mimeData().text()
366 self._insert_plain_text_into_buffer(cursor, text)
369 self._insert_plain_text_into_buffer(cursor, text)
367
370
368 # Qt is expecting to get something here--drag and drop occurs in its
371 # Qt is expecting to get something here--drag and drop occurs in its
369 # own event loop. Send a DragLeave event to end it.
372 # own event loop. Send a DragLeave event to end it.
370 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
373 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
371 return True
374 return True
372
375
373 # Handle scrolling of the vsplit pager. This hack attempts to solve
376 # Handle scrolling of the vsplit pager. This hack attempts to solve
374 # problems with tearing of the help text inside the pager window. This
377 # problems with tearing of the help text inside the pager window. This
375 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
378 # happens only on Mac OS X with both PySide and PyQt. This fix isn't
376 # perfect but makes the pager more usable.
379 # perfect but makes the pager more usable.
377 elif etype in self._pager_scroll_events and \
380 elif etype in self._pager_scroll_events and \
378 obj == self._page_control:
381 obj == self._page_control:
379 self._page_control.repaint()
382 self._page_control.repaint()
380 return True
383 return True
381 return super(ConsoleWidget, self).eventFilter(obj, event)
384 return super(ConsoleWidget, self).eventFilter(obj, event)
382
385
383 #---------------------------------------------------------------------------
386 #---------------------------------------------------------------------------
384 # 'QWidget' interface
387 # 'QWidget' interface
385 #---------------------------------------------------------------------------
388 #---------------------------------------------------------------------------
386
389
387 def sizeHint(self):
390 def sizeHint(self):
388 """ Reimplemented to suggest a size that is 80 characters wide and
391 """ Reimplemented to suggest a size that is 80 characters wide and
389 25 lines high.
392 25 lines high.
390 """
393 """
391 font_metrics = QtGui.QFontMetrics(self.font)
394 font_metrics = QtGui.QFontMetrics(self.font)
392 margin = (self._control.frameWidth() +
395 margin = (self._control.frameWidth() +
393 self._control.document().documentMargin()) * 2
396 self._control.document().documentMargin()) * 2
394 style = self.style()
397 style = self.style()
395 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
398 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
396
399
397 # Note 1: Despite my best efforts to take the various margins into
400 # Note 1: Despite my best efforts to take the various margins into
398 # account, the width is still coming out a bit too small, so we include
401 # account, the width is still coming out a bit too small, so we include
399 # a fudge factor of one character here.
402 # a fudge factor of one character here.
400 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
403 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
401 # to a Qt bug on certain Mac OS systems where it returns 0.
404 # to a Qt bug on certain Mac OS systems where it returns 0.
402 width = font_metrics.width(' ') * 81 + margin
405 width = font_metrics.width(' ') * 81 + margin
403 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
406 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
404 if self.paging == 'hsplit':
407 if self.paging == 'hsplit':
405 width = width * 2 + splitwidth
408 width = width * 2 + splitwidth
406
409
407 height = font_metrics.height() * 25 + margin
410 height = font_metrics.height() * 25 + margin
408 if self.paging == 'vsplit':
411 if self.paging == 'vsplit':
409 height = height * 2 + splitwidth
412 height = height * 2 + splitwidth
410
413
411 return QtCore.QSize(width, height)
414 return QtCore.QSize(width, height)
412
415
413 #---------------------------------------------------------------------------
416 #---------------------------------------------------------------------------
414 # 'ConsoleWidget' public interface
417 # 'ConsoleWidget' public interface
415 #---------------------------------------------------------------------------
418 #---------------------------------------------------------------------------
416
419
417 def can_copy(self):
420 def can_copy(self):
418 """ Returns whether text can be copied to the clipboard.
421 """ Returns whether text can be copied to the clipboard.
419 """
422 """
420 return self._control.textCursor().hasSelection()
423 return self._control.textCursor().hasSelection()
421
424
422 def can_cut(self):
425 def can_cut(self):
423 """ Returns whether text can be cut to the clipboard.
426 """ Returns whether text can be cut to the clipboard.
424 """
427 """
425 cursor = self._control.textCursor()
428 cursor = self._control.textCursor()
426 return (cursor.hasSelection() and
429 return (cursor.hasSelection() and
427 self._in_buffer(cursor.anchor()) and
430 self._in_buffer(cursor.anchor()) and
428 self._in_buffer(cursor.position()))
431 self._in_buffer(cursor.position()))
429
432
430 def can_paste(self):
433 def can_paste(self):
431 """ Returns whether text can be pasted from the clipboard.
434 """ Returns whether text can be pasted from the clipboard.
432 """
435 """
433 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
436 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
434 return bool(QtGui.QApplication.clipboard().text())
437 return bool(QtGui.QApplication.clipboard().text())
435 return False
438 return False
436
439
437 def clear(self, keep_input=True):
440 def clear(self, keep_input=True):
438 """ Clear the console.
441 """ Clear the console.
439
442
440 Parameters:
443 Parameters:
441 -----------
444 -----------
442 keep_input : bool, optional (default True)
445 keep_input : bool, optional (default True)
443 If set, restores the old input buffer if a new prompt is written.
446 If set, restores the old input buffer if a new prompt is written.
444 """
447 """
445 if self._executing:
448 if self._executing:
446 self._control.clear()
449 self._control.clear()
447 else:
450 else:
448 if keep_input:
451 if keep_input:
449 input_buffer = self.input_buffer
452 input_buffer = self.input_buffer
450 self._control.clear()
453 self._control.clear()
451 self._show_prompt()
454 self._show_prompt()
452 if keep_input:
455 if keep_input:
453 self.input_buffer = input_buffer
456 self.input_buffer = input_buffer
454
457
455 def copy(self):
458 def copy(self):
456 """ Copy the currently selected text to the clipboard.
459 """ Copy the currently selected text to the clipboard.
457 """
460 """
458 self.layout().currentWidget().copy()
461 self.layout().currentWidget().copy()
459
462
460 def cut(self):
463 def cut(self):
461 """ Copy the currently selected text to the clipboard and delete it
464 """ Copy the currently selected text to the clipboard and delete it
462 if it's inside the input buffer.
465 if it's inside the input buffer.
463 """
466 """
464 self.copy()
467 self.copy()
465 if self.can_cut():
468 if self.can_cut():
466 self._control.textCursor().removeSelectedText()
469 self._control.textCursor().removeSelectedText()
467
470
468 def execute(self, source=None, hidden=False, interactive=False):
471 def execute(self, source=None, hidden=False, interactive=False):
469 """ Executes source or the input buffer, possibly prompting for more
472 """ Executes source or the input buffer, possibly prompting for more
470 input.
473 input.
471
474
472 Parameters:
475 Parameters:
473 -----------
476 -----------
474 source : str, optional
477 source : str, optional
475
478
476 The source to execute. If not specified, the input buffer will be
479 The source to execute. If not specified, the input buffer will be
477 used. If specified and 'hidden' is False, the input buffer will be
480 used. If specified and 'hidden' is False, the input buffer will be
478 replaced with the source before execution.
481 replaced with the source before execution.
479
482
480 hidden : bool, optional (default False)
483 hidden : bool, optional (default False)
481
484
482 If set, no output will be shown and the prompt will not be modified.
485 If set, no output will be shown and the prompt will not be modified.
483 In other words, it will be completely invisible to the user that
486 In other words, it will be completely invisible to the user that
484 an execution has occurred.
487 an execution has occurred.
485
488
486 interactive : bool, optional (default False)
489 interactive : bool, optional (default False)
487
490
488 Whether the console is to treat the source as having been manually
491 Whether the console is to treat the source as having been manually
489 entered by the user. The effect of this parameter depends on the
492 entered by the user. The effect of this parameter depends on the
490 subclass implementation.
493 subclass implementation.
491
494
492 Raises:
495 Raises:
493 -------
496 -------
494 RuntimeError
497 RuntimeError
495 If incomplete input is given and 'hidden' is True. In this case,
498 If incomplete input is given and 'hidden' is True. In this case,
496 it is not possible to prompt for more input.
499 it is not possible to prompt for more input.
497
500
498 Returns:
501 Returns:
499 --------
502 --------
500 A boolean indicating whether the source was executed.
503 A boolean indicating whether the source was executed.
501 """
504 """
502 # WARNING: The order in which things happen here is very particular, in
505 # WARNING: The order in which things happen here is very particular, in
503 # large part because our syntax highlighting is fragile. If you change
506 # large part because our syntax highlighting is fragile. If you change
504 # something, test carefully!
507 # something, test carefully!
505
508
506 # Decide what to execute.
509 # Decide what to execute.
507 if source is None:
510 if source is None:
508 source = self.input_buffer
511 source = self.input_buffer
509 if not hidden:
512 if not hidden:
510 # A newline is appended later, but it should be considered part
513 # A newline is appended later, but it should be considered part
511 # of the input buffer.
514 # of the input buffer.
512 source += '\n'
515 source += '\n'
513 elif not hidden:
516 elif not hidden:
514 self.input_buffer = source
517 self.input_buffer = source
515
518
516 # Execute the source or show a continuation prompt if it is incomplete.
519 # Execute the source or show a continuation prompt if it is incomplete.
517 complete = self._is_complete(source, interactive)
520 complete = self._is_complete(source, interactive)
518 if hidden:
521 if hidden:
519 if complete:
522 if complete:
520 self._execute(source, hidden)
523 self._execute(source, hidden)
521 else:
524 else:
522 error = 'Incomplete noninteractive input: "%s"'
525 error = 'Incomplete noninteractive input: "%s"'
523 raise RuntimeError(error % source)
526 raise RuntimeError(error % source)
524 else:
527 else:
525 if complete:
528 if complete:
526 self._append_plain_text('\n')
529 self._append_plain_text('\n')
527 self._input_buffer_executing = self.input_buffer
530 self._input_buffer_executing = self.input_buffer
528 self._executing = True
531 self._executing = True
529 self._prompt_finished()
532 self._prompt_finished()
530
533
531 # The maximum block count is only in effect during execution.
534 # The maximum block count is only in effect during execution.
532 # This ensures that _prompt_pos does not become invalid due to
535 # This ensures that _prompt_pos does not become invalid due to
533 # text truncation.
536 # text truncation.
534 self._control.document().setMaximumBlockCount(self.buffer_size)
537 self._control.document().setMaximumBlockCount(self.buffer_size)
535
538
536 # Setting a positive maximum block count will automatically
539 # Setting a positive maximum block count will automatically
537 # disable the undo/redo history, but just to be safe:
540 # disable the undo/redo history, but just to be safe:
538 self._control.setUndoRedoEnabled(False)
541 self._control.setUndoRedoEnabled(False)
539
542
540 # Perform actual execution.
543 # Perform actual execution.
541 self._execute(source, hidden)
544 self._execute(source, hidden)
542
545
543 else:
546 else:
544 # Do this inside an edit block so continuation prompts are
547 # Do this inside an edit block so continuation prompts are
545 # removed seamlessly via undo/redo.
548 # removed seamlessly via undo/redo.
546 cursor = self._get_end_cursor()
549 cursor = self._get_end_cursor()
547 cursor.beginEditBlock()
550 cursor.beginEditBlock()
548 cursor.insertText('\n')
551 cursor.insertText('\n')
549 self._insert_continuation_prompt(cursor)
552 self._insert_continuation_prompt(cursor)
550 cursor.endEditBlock()
553 cursor.endEditBlock()
551
554
552 # Do not do this inside the edit block. It works as expected
555 # Do not do this inside the edit block. It works as expected
553 # when using a QPlainTextEdit control, but does not have an
556 # when using a QPlainTextEdit control, but does not have an
554 # effect when using a QTextEdit. I believe this is a Qt bug.
557 # effect when using a QTextEdit. I believe this is a Qt bug.
555 self._control.moveCursor(QtGui.QTextCursor.End)
558 self._control.moveCursor(QtGui.QTextCursor.End)
556
559
557 return complete
560 return complete
558
561
559 def export_html(self):
562 def export_html(self):
560 """ Shows a dialog to export HTML/XML in various formats.
563 """ Shows a dialog to export HTML/XML in various formats.
561 """
564 """
562 self._html_exporter.export()
565 self._html_exporter.export()
563
566
564 def _get_input_buffer(self, force=False):
567 def _get_input_buffer(self, force=False):
565 """ The text that the user has entered entered at the current prompt.
568 """ The text that the user has entered entered at the current prompt.
566
569
567 If the console is currently executing, the text that is executing will
570 If the console is currently executing, the text that is executing will
568 always be returned.
571 always be returned.
569 """
572 """
570 # If we're executing, the input buffer may not even exist anymore due to
573 # If we're executing, the input buffer may not even exist anymore due to
571 # the limit imposed by 'buffer_size'. Therefore, we store it.
574 # the limit imposed by 'buffer_size'. Therefore, we store it.
572 if self._executing and not force:
575 if self._executing and not force:
573 return self._input_buffer_executing
576 return self._input_buffer_executing
574
577
575 cursor = self._get_end_cursor()
578 cursor = self._get_end_cursor()
576 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
579 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
577 input_buffer = cursor.selection().toPlainText()
580 input_buffer = cursor.selection().toPlainText()
578
581
579 # Strip out continuation prompts.
582 # Strip out continuation prompts.
580 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
583 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
581
584
582 def _set_input_buffer(self, string):
585 def _set_input_buffer(self, string):
583 """ Sets the text in the input buffer.
586 """ Sets the text in the input buffer.
584
587
585 If the console is currently executing, this call has no *immediate*
588 If the console is currently executing, this call has no *immediate*
586 effect. When the execution is finished, the input buffer will be updated
589 effect. When the execution is finished, the input buffer will be updated
587 appropriately.
590 appropriately.
588 """
591 """
589 # If we're executing, store the text for later.
592 # If we're executing, store the text for later.
590 if self._executing:
593 if self._executing:
591 self._input_buffer_pending = string
594 self._input_buffer_pending = string
592 return
595 return
593
596
594 # Remove old text.
597 # Remove old text.
595 cursor = self._get_end_cursor()
598 cursor = self._get_end_cursor()
596 cursor.beginEditBlock()
599 cursor.beginEditBlock()
597 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
600 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
598 cursor.removeSelectedText()
601 cursor.removeSelectedText()
599
602
600 # Insert new text with continuation prompts.
603 # Insert new text with continuation prompts.
601 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
604 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
602 cursor.endEditBlock()
605 cursor.endEditBlock()
603 self._control.moveCursor(QtGui.QTextCursor.End)
606 self._control.moveCursor(QtGui.QTextCursor.End)
604
607
605 input_buffer = property(_get_input_buffer, _set_input_buffer)
608 input_buffer = property(_get_input_buffer, _set_input_buffer)
606
609
607 def _get_font(self):
610 def _get_font(self):
608 """ The base font being used by the ConsoleWidget.
611 """ The base font being used by the ConsoleWidget.
609 """
612 """
610 return self._control.document().defaultFont()
613 return self._control.document().defaultFont()
611
614
612 def _set_font(self, font):
615 def _set_font(self, font):
613 """ Sets the base font for the ConsoleWidget to the specified QFont.
616 """ Sets the base font for the ConsoleWidget to the specified QFont.
614 """
617 """
615 font_metrics = QtGui.QFontMetrics(font)
618 font_metrics = QtGui.QFontMetrics(font)
616 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
619 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
617
620
618 self._completion_widget.setFont(font)
621 self._completion_widget.setFont(font)
619 self._control.document().setDefaultFont(font)
622 self._control.document().setDefaultFont(font)
620 if self._page_control:
623 if self._page_control:
621 self._page_control.document().setDefaultFont(font)
624 self._page_control.document().setDefaultFont(font)
622
625
623 self.font_changed.emit(font)
626 self.font_changed.emit(font)
624
627
625 font = property(_get_font, _set_font)
628 font = property(_get_font, _set_font)
626
629
627 def paste(self, mode=QtGui.QClipboard.Clipboard):
630 def paste(self, mode=QtGui.QClipboard.Clipboard):
628 """ Paste the contents of the clipboard into the input region.
631 """ Paste the contents of the clipboard into the input region.
629
632
630 Parameters:
633 Parameters:
631 -----------
634 -----------
632 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
635 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
633
636
634 Controls which part of the system clipboard is used. This can be
637 Controls which part of the system clipboard is used. This can be
635 used to access the selection clipboard in X11 and the Find buffer
638 used to access the selection clipboard in X11 and the Find buffer
636 in Mac OS. By default, the regular clipboard is used.
639 in Mac OS. By default, the regular clipboard is used.
637 """
640 """
638 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
641 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
639 # Make sure the paste is safe.
642 # Make sure the paste is safe.
640 self._keep_cursor_in_buffer()
643 self._keep_cursor_in_buffer()
641 cursor = self._control.textCursor()
644 cursor = self._control.textCursor()
642
645
643 # Remove any trailing newline, which confuses the GUI and forces the
646 # Remove any trailing newline, which confuses the GUI and forces the
644 # user to backspace.
647 # user to backspace.
645 text = QtGui.QApplication.clipboard().text(mode).rstrip()
648 text = QtGui.QApplication.clipboard().text(mode).rstrip()
646 self._insert_plain_text_into_buffer(cursor, dedent(text))
649 self._insert_plain_text_into_buffer(cursor, dedent(text))
647
650
648 def print_(self, printer = None):
651 def print_(self, printer = None):
649 """ Print the contents of the ConsoleWidget to the specified QPrinter.
652 """ Print the contents of the ConsoleWidget to the specified QPrinter.
650 """
653 """
651 if (not printer):
654 if (not printer):
652 printer = QtGui.QPrinter()
655 printer = QtGui.QPrinter()
653 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
656 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
654 return
657 return
655 self._control.print_(printer)
658 self._control.print_(printer)
656
659
657 def prompt_to_top(self):
660 def prompt_to_top(self):
658 """ Moves the prompt to the top of the viewport.
661 """ Moves the prompt to the top of the viewport.
659 """
662 """
660 if not self._executing:
663 if not self._executing:
661 prompt_cursor = self._get_prompt_cursor()
664 prompt_cursor = self._get_prompt_cursor()
662 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
665 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
663 self._set_cursor(prompt_cursor)
666 self._set_cursor(prompt_cursor)
664 self._set_top_cursor(prompt_cursor)
667 self._set_top_cursor(prompt_cursor)
665
668
666 def redo(self):
669 def redo(self):
667 """ Redo the last operation. If there is no operation to redo, nothing
670 """ Redo the last operation. If there is no operation to redo, nothing
668 happens.
671 happens.
669 """
672 """
670 self._control.redo()
673 self._control.redo()
671
674
672 def reset_font(self):
675 def reset_font(self):
673 """ Sets the font to the default fixed-width font for this platform.
676 """ Sets the font to the default fixed-width font for this platform.
674 """
677 """
675 if sys.platform == 'win32':
678 if sys.platform == 'win32':
676 # Consolas ships with Vista/Win7, fallback to Courier if needed
679 # Consolas ships with Vista/Win7, fallback to Courier if needed
677 fallback = 'Courier'
680 fallback = 'Courier'
678 elif sys.platform == 'darwin':
681 elif sys.platform == 'darwin':
679 # OSX always has Monaco
682 # OSX always has Monaco
680 fallback = 'Monaco'
683 fallback = 'Monaco'
681 else:
684 else:
682 # Monospace should always exist
685 # Monospace should always exist
683 fallback = 'Monospace'
686 fallback = 'Monospace'
684 font = get_font(self.font_family, fallback)
687 font = get_font(self.font_family, fallback)
685 if self.font_size:
688 if self.font_size:
686 font.setPointSize(self.font_size)
689 font.setPointSize(self.font_size)
687 else:
690 else:
688 font.setPointSize(QtGui.qApp.font().pointSize())
691 font.setPointSize(QtGui.qApp.font().pointSize())
689 font.setStyleHint(QtGui.QFont.TypeWriter)
692 font.setStyleHint(QtGui.QFont.TypeWriter)
690 self._set_font(font)
693 self._set_font(font)
691
694
692 def change_font_size(self, delta):
695 def change_font_size(self, delta):
693 """Change the font size by the specified amount (in points).
696 """Change the font size by the specified amount (in points).
694 """
697 """
695 font = self.font
698 font = self.font
696 size = max(font.pointSize() + delta, 1) # minimum 1 point
699 size = max(font.pointSize() + delta, 1) # minimum 1 point
697 font.setPointSize(size)
700 font.setPointSize(size)
698 self._set_font(font)
701 self._set_font(font)
699
702
700 def _increase_font_size(self):
703 def _increase_font_size(self):
701 self.change_font_size(1)
704 self.change_font_size(1)
702
705
703 def _decrease_font_size(self):
706 def _decrease_font_size(self):
704 self.change_font_size(-1)
707 self.change_font_size(-1)
705
708
706 def select_all(self):
709 def select_all(self):
707 """ Selects all the text in the buffer.
710 """ Selects all the text in the buffer.
708 """
711 """
709 self._control.selectAll()
712 self._control.selectAll()
710
713
711 def _get_tab_width(self):
714 def _get_tab_width(self):
712 """ The width (in terms of space characters) for tab characters.
715 """ The width (in terms of space characters) for tab characters.
713 """
716 """
714 return self._tab_width
717 return self._tab_width
715
718
716 def _set_tab_width(self, tab_width):
719 def _set_tab_width(self, tab_width):
717 """ Sets the width (in terms of space characters) for tab characters.
720 """ Sets the width (in terms of space characters) for tab characters.
718 """
721 """
719 font_metrics = QtGui.QFontMetrics(self.font)
722 font_metrics = QtGui.QFontMetrics(self.font)
720 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
723 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
721
724
722 self._tab_width = tab_width
725 self._tab_width = tab_width
723
726
724 tab_width = property(_get_tab_width, _set_tab_width)
727 tab_width = property(_get_tab_width, _set_tab_width)
725
728
726 def undo(self):
729 def undo(self):
727 """ Undo the last operation. If there is no operation to undo, nothing
730 """ Undo the last operation. If there is no operation to undo, nothing
728 happens.
731 happens.
729 """
732 """
730 self._control.undo()
733 self._control.undo()
731
734
732 #---------------------------------------------------------------------------
735 #---------------------------------------------------------------------------
733 # 'ConsoleWidget' abstract interface
736 # 'ConsoleWidget' abstract interface
734 #---------------------------------------------------------------------------
737 #---------------------------------------------------------------------------
735
738
736 def _is_complete(self, source, interactive):
739 def _is_complete(self, source, interactive):
737 """ Returns whether 'source' can be executed. When triggered by an
740 """ Returns whether 'source' can be executed. When triggered by an
738 Enter/Return key press, 'interactive' is True; otherwise, it is
741 Enter/Return key press, 'interactive' is True; otherwise, it is
739 False.
742 False.
740 """
743 """
741 raise NotImplementedError
744 raise NotImplementedError
742
745
743 def _execute(self, source, hidden):
746 def _execute(self, source, hidden):
744 """ Execute 'source'. If 'hidden', do not show any output.
747 """ Execute 'source'. If 'hidden', do not show any output.
745 """
748 """
746 raise NotImplementedError
749 raise NotImplementedError
747
750
748 def _prompt_started_hook(self):
751 def _prompt_started_hook(self):
749 """ Called immediately after a new prompt is displayed.
752 """ Called immediately after a new prompt is displayed.
750 """
753 """
751 pass
754 pass
752
755
753 def _prompt_finished_hook(self):
756 def _prompt_finished_hook(self):
754 """ Called immediately after a prompt is finished, i.e. when some input
757 """ Called immediately after a prompt is finished, i.e. when some input
755 will be processed and a new prompt displayed.
758 will be processed and a new prompt displayed.
756 """
759 """
757 pass
760 pass
758
761
759 def _up_pressed(self, shift_modifier):
762 def _up_pressed(self, shift_modifier):
760 """ Called when the up key is pressed. Returns whether to continue
763 """ Called when the up key is pressed. Returns whether to continue
761 processing the event.
764 processing the event.
762 """
765 """
763 return True
766 return True
764
767
765 def _down_pressed(self, shift_modifier):
768 def _down_pressed(self, shift_modifier):
766 """ Called when the down key is pressed. Returns whether to continue
769 """ Called when the down key is pressed. Returns whether to continue
767 processing the event.
770 processing the event.
768 """
771 """
769 return True
772 return True
770
773
771 def _tab_pressed(self):
774 def _tab_pressed(self):
772 """ Called when the tab key is pressed. Returns whether to continue
775 """ Called when the tab key is pressed. Returns whether to continue
773 processing the event.
776 processing the event.
774 """
777 """
775 return False
778 return False
776
779
777 #--------------------------------------------------------------------------
780 #--------------------------------------------------------------------------
778 # 'ConsoleWidget' protected interface
781 # 'ConsoleWidget' protected interface
779 #--------------------------------------------------------------------------
782 #--------------------------------------------------------------------------
780
783
781 def _append_custom(self, insert, input, before_prompt=False):
784 def _append_custom(self, insert, input, before_prompt=False):
782 """ A low-level method for appending content to the end of the buffer.
785 """ A low-level method for appending content to the end of the buffer.
783
786
784 If 'before_prompt' is enabled, the content will be inserted before the
787 If 'before_prompt' is enabled, the content will be inserted before the
785 current prompt, if there is one.
788 current prompt, if there is one.
786 """
789 """
787 # Determine where to insert the content.
790 # Determine where to insert the content.
788 cursor = self._control.textCursor()
791 cursor = self._control.textCursor()
789 if before_prompt and (self._reading or not self._executing):
792 if before_prompt and (self._reading or not self._executing):
790 cursor.setPosition(self._append_before_prompt_pos)
793 cursor.setPosition(self._append_before_prompt_pos)
791 else:
794 else:
792 cursor.movePosition(QtGui.QTextCursor.End)
795 cursor.movePosition(QtGui.QTextCursor.End)
793 start_pos = cursor.position()
796 start_pos = cursor.position()
794
797
795 # Perform the insertion.
798 # Perform the insertion.
796 result = insert(cursor, input)
799 result = insert(cursor, input)
797
800
798 # Adjust the prompt position if we have inserted before it. This is safe
801 # Adjust the prompt position if we have inserted before it. This is safe
799 # because buffer truncation is disabled when not executing.
802 # because buffer truncation is disabled when not executing.
800 if before_prompt and not self._executing:
803 if before_prompt and not self._executing:
801 diff = cursor.position() - start_pos
804 diff = cursor.position() - start_pos
802 self._append_before_prompt_pos += diff
805 self._append_before_prompt_pos += diff
803 self._prompt_pos += diff
806 self._prompt_pos += diff
804
807
805 return result
808 return result
806
809
807 def _append_html(self, html, before_prompt=False):
810 def _append_html(self, html, before_prompt=False):
808 """ Appends HTML at the end of the console buffer.
811 """ Appends HTML at the end of the console buffer.
809 """
812 """
810 self._append_custom(self._insert_html, html, before_prompt)
813 self._append_custom(self._insert_html, html, before_prompt)
811
814
812 def _append_html_fetching_plain_text(self, html, before_prompt=False):
815 def _append_html_fetching_plain_text(self, html, before_prompt=False):
813 """ Appends HTML, then returns the plain text version of it.
816 """ Appends HTML, then returns the plain text version of it.
814 """
817 """
815 return self._append_custom(self._insert_html_fetching_plain_text,
818 return self._append_custom(self._insert_html_fetching_plain_text,
816 html, before_prompt)
819 html, before_prompt)
817
820
818 def _append_plain_text(self, text, before_prompt=False):
821 def _append_plain_text(self, text, before_prompt=False):
819 """ Appends plain text, processing ANSI codes if enabled.
822 """ Appends plain text, processing ANSI codes if enabled.
820 """
823 """
821 self._append_custom(self._insert_plain_text, text, before_prompt)
824 self._append_custom(self._insert_plain_text, text, before_prompt)
822
825
823 def _cancel_text_completion(self):
826 def _cancel_text_completion(self):
824 """ If text completion is progress, cancel it.
827 """ If text completion is progress, cancel it.
825 """
828 """
826 if self._text_completing_pos:
829 if self._text_completing_pos:
827 self._clear_temporary_buffer()
830 self._clear_temporary_buffer()
828 self._text_completing_pos = 0
831 self._text_completing_pos = 0
829
832
830 def _clear_temporary_buffer(self):
833 def _clear_temporary_buffer(self):
831 """ Clears the "temporary text" buffer, i.e. all the text following
834 """ Clears the "temporary text" buffer, i.e. all the text following
832 the prompt region.
835 the prompt region.
833 """
836 """
834 # Select and remove all text below the input buffer.
837 # Select and remove all text below the input buffer.
835 cursor = self._get_prompt_cursor()
838 cursor = self._get_prompt_cursor()
836 prompt = self._continuation_prompt.lstrip()
839 prompt = self._continuation_prompt.lstrip()
837 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
840 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
838 temp_cursor = QtGui.QTextCursor(cursor)
841 temp_cursor = QtGui.QTextCursor(cursor)
839 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
842 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
840 text = temp_cursor.selection().toPlainText().lstrip()
843 text = temp_cursor.selection().toPlainText().lstrip()
841 if not text.startswith(prompt):
844 if not text.startswith(prompt):
842 break
845 break
843 else:
846 else:
844 # We've reached the end of the input buffer and no text follows.
847 # We've reached the end of the input buffer and no text follows.
845 return
848 return
846 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
849 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
847 cursor.movePosition(QtGui.QTextCursor.End,
850 cursor.movePosition(QtGui.QTextCursor.End,
848 QtGui.QTextCursor.KeepAnchor)
851 QtGui.QTextCursor.KeepAnchor)
849 cursor.removeSelectedText()
852 cursor.removeSelectedText()
850
853
851 # After doing this, we have no choice but to clear the undo/redo
854 # After doing this, we have no choice but to clear the undo/redo
852 # history. Otherwise, the text is not "temporary" at all, because it
855 # history. Otherwise, the text is not "temporary" at all, because it
853 # can be recalled with undo/redo. Unfortunately, Qt does not expose
856 # can be recalled with undo/redo. Unfortunately, Qt does not expose
854 # fine-grained control to the undo/redo system.
857 # fine-grained control to the undo/redo system.
855 if self._control.isUndoRedoEnabled():
858 if self._control.isUndoRedoEnabled():
856 self._control.setUndoRedoEnabled(False)
859 self._control.setUndoRedoEnabled(False)
857 self._control.setUndoRedoEnabled(True)
860 self._control.setUndoRedoEnabled(True)
858
861
859 def _complete_with_items(self, cursor, items):
862 def _complete_with_items(self, cursor, items):
860 """ Performs completion with 'items' at the specified cursor location.
863 """ Performs completion with 'items' at the specified cursor location.
861 """
864 """
862 self._cancel_text_completion()
865 self._cancel_text_completion()
863
866
864 if len(items) == 1:
867 if len(items) == 1:
865 cursor.setPosition(self._control.textCursor().position(),
868 cursor.setPosition(self._control.textCursor().position(),
866 QtGui.QTextCursor.KeepAnchor)
869 QtGui.QTextCursor.KeepAnchor)
867 cursor.insertText(items[0])
870 cursor.insertText(items[0])
868
871
869 elif len(items) > 1:
872 elif len(items) > 1:
870 current_pos = self._control.textCursor().position()
873 current_pos = self._control.textCursor().position()
871 prefix = commonprefix(items)
874 prefix = commonprefix(items)
872 if prefix:
875 if prefix:
873 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
876 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
874 cursor.insertText(prefix)
877 cursor.insertText(prefix)
875 current_pos = cursor.position()
878 current_pos = cursor.position()
876
879
877 if self.gui_completion:
880 if self.gui_completion:
878 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
881 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
879 self._completion_widget.show_items(cursor, items)
882 self._completion_widget.show_items(cursor, items)
880 else:
883 else:
881 cursor.beginEditBlock()
884 cursor.beginEditBlock()
882 self._append_plain_text('\n')
885 self._append_plain_text('\n')
883 self._page(self._format_as_columns(items))
886 self._page(self._format_as_columns(items))
884 cursor.endEditBlock()
887 cursor.endEditBlock()
885
888
886 cursor.setPosition(current_pos)
889 cursor.setPosition(current_pos)
887 self._control.moveCursor(QtGui.QTextCursor.End)
890 self._control.moveCursor(QtGui.QTextCursor.End)
888 self._control.setTextCursor(cursor)
891 self._control.setTextCursor(cursor)
889 self._text_completing_pos = current_pos
892 self._text_completing_pos = current_pos
890
893
891 def _context_menu_make(self, pos):
894 def _context_menu_make(self, pos):
892 """ Creates a context menu for the given QPoint (in widget coordinates).
895 """ Creates a context menu for the given QPoint (in widget coordinates).
893 """
896 """
894 menu = QtGui.QMenu(self)
897 menu = QtGui.QMenu(self)
895
898
896 self.cut_action = menu.addAction('Cut', self.cut)
899 self.cut_action = menu.addAction('Cut', self.cut)
897 self.cut_action.setEnabled(self.can_cut())
900 self.cut_action.setEnabled(self.can_cut())
898 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
901 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
899
902
900 self.copy_action = menu.addAction('Copy', self.copy)
903 self.copy_action = menu.addAction('Copy', self.copy)
901 self.copy_action.setEnabled(self.can_copy())
904 self.copy_action.setEnabled(self.can_copy())
902 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
905 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
903
906
904 self.paste_action = menu.addAction('Paste', self.paste)
907 self.paste_action = menu.addAction('Paste', self.paste)
905 self.paste_action.setEnabled(self.can_paste())
908 self.paste_action.setEnabled(self.can_paste())
906 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
909 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
907
910
908 menu.addSeparator()
911 menu.addSeparator()
909 menu.addAction(self.select_all_action)
912 menu.addAction(self.select_all_action)
910
913
911 menu.addSeparator()
914 menu.addSeparator()
912 menu.addAction(self.export_action)
915 menu.addAction(self.export_action)
913 menu.addAction(self.print_action)
916 menu.addAction(self.print_action)
914
917
915 return menu
918 return menu
916
919
917 def _control_key_down(self, modifiers, include_command=False):
920 def _control_key_down(self, modifiers, include_command=False):
918 """ Given a KeyboardModifiers flags object, return whether the Control
921 """ Given a KeyboardModifiers flags object, return whether the Control
919 key is down.
922 key is down.
920
923
921 Parameters:
924 Parameters:
922 -----------
925 -----------
923 include_command : bool, optional (default True)
926 include_command : bool, optional (default True)
924 Whether to treat the Command key as a (mutually exclusive) synonym
927 Whether to treat the Command key as a (mutually exclusive) synonym
925 for Control when in Mac OS.
928 for Control when in Mac OS.
926 """
929 """
927 # Note that on Mac OS, ControlModifier corresponds to the Command key
930 # Note that on Mac OS, ControlModifier corresponds to the Command key
928 # while MetaModifier corresponds to the Control key.
931 # while MetaModifier corresponds to the Control key.
929 if sys.platform == 'darwin':
932 if sys.platform == 'darwin':
930 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
933 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
931 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
934 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
932 else:
935 else:
933 return bool(modifiers & QtCore.Qt.ControlModifier)
936 return bool(modifiers & QtCore.Qt.ControlModifier)
934
937
935 def _create_control(self):
938 def _create_control(self):
936 """ Creates and connects the underlying text widget.
939 """ Creates and connects the underlying text widget.
937 """
940 """
938 # Create the underlying control.
941 # Create the underlying control.
939 if self.kind == 'plain':
942 if self.kind == 'plain':
940 control = QtGui.QPlainTextEdit()
943 control = QtGui.QPlainTextEdit()
941 elif self.kind == 'rich':
944 elif self.kind == 'rich':
942 control = QtGui.QTextEdit()
945 control = QtGui.QTextEdit()
943 control.setAcceptRichText(False)
946 control.setAcceptRichText(False)
944
947
945 # Install event filters. The filter on the viewport is needed for
948 # Install event filters. The filter on the viewport is needed for
946 # mouse events and drag events.
949 # mouse events and drag events.
947 control.installEventFilter(self)
950 control.installEventFilter(self)
948 control.viewport().installEventFilter(self)
951 control.viewport().installEventFilter(self)
949
952
950 # Connect signals.
953 # Connect signals.
951 control.cursorPositionChanged.connect(self._cursor_position_changed)
954 control.cursorPositionChanged.connect(self._cursor_position_changed)
952 control.customContextMenuRequested.connect(
955 control.customContextMenuRequested.connect(
953 self._custom_context_menu_requested)
956 self._custom_context_menu_requested)
954 control.copyAvailable.connect(self.copy_available)
957 control.copyAvailable.connect(self.copy_available)
955 control.redoAvailable.connect(self.redo_available)
958 control.redoAvailable.connect(self.redo_available)
956 control.undoAvailable.connect(self.undo_available)
959 control.undoAvailable.connect(self.undo_available)
957
960
958 # Hijack the document size change signal to prevent Qt from adjusting
961 # Hijack the document size change signal to prevent Qt from adjusting
959 # the viewport's scrollbar. We are relying on an implementation detail
962 # the viewport's scrollbar. We are relying on an implementation detail
960 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
963 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
961 # this functionality we cannot create a nice terminal interface.
964 # this functionality we cannot create a nice terminal interface.
962 layout = control.document().documentLayout()
965 layout = control.document().documentLayout()
963 layout.documentSizeChanged.disconnect()
966 layout.documentSizeChanged.disconnect()
964 layout.documentSizeChanged.connect(self._adjust_scrollbars)
967 layout.documentSizeChanged.connect(self._adjust_scrollbars)
965
968
966 # Configure the control.
969 # Configure the control.
967 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
970 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
968 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
971 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
969 control.setReadOnly(True)
972 control.setReadOnly(True)
970 control.setUndoRedoEnabled(False)
973 control.setUndoRedoEnabled(False)
971 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
974 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
972 return control
975 return control
973
976
974 def _create_page_control(self):
977 def _create_page_control(self):
975 """ Creates and connects the underlying paging widget.
978 """ Creates and connects the underlying paging widget.
976 """
979 """
977 if self.kind == 'plain':
980 if self.kind == 'plain':
978 control = QtGui.QPlainTextEdit()
981 control = QtGui.QPlainTextEdit()
979 elif self.kind == 'rich':
982 elif self.kind == 'rich':
980 control = QtGui.QTextEdit()
983 control = QtGui.QTextEdit()
981 control.installEventFilter(self)
984 control.installEventFilter(self)
982 viewport = control.viewport()
985 viewport = control.viewport()
983 viewport.installEventFilter(self)
986 viewport.installEventFilter(self)
984 control.setReadOnly(True)
987 control.setReadOnly(True)
985 control.setUndoRedoEnabled(False)
988 control.setUndoRedoEnabled(False)
986 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
989 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
987 return control
990 return control
988
991
989 def _event_filter_console_keypress(self, event):
992 def _event_filter_console_keypress(self, event):
990 """ Filter key events for the underlying text widget to create a
993 """ Filter key events for the underlying text widget to create a
991 console-like interface.
994 console-like interface.
992 """
995 """
993 intercepted = False
996 intercepted = False
994 cursor = self._control.textCursor()
997 cursor = self._control.textCursor()
995 position = cursor.position()
998 position = cursor.position()
996 key = event.key()
999 key = event.key()
997 ctrl_down = self._control_key_down(event.modifiers())
1000 ctrl_down = self._control_key_down(event.modifiers())
998 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1001 alt_down = event.modifiers() & QtCore.Qt.AltModifier
999 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1002 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1000
1003
1001 #------ Special sequences ----------------------------------------------
1004 #------ Special sequences ----------------------------------------------
1002
1005
1003 if event.matches(QtGui.QKeySequence.Copy):
1006 if event.matches(QtGui.QKeySequence.Copy):
1004 self.copy()
1007 self.copy()
1005 intercepted = True
1008 intercepted = True
1006
1009
1007 elif event.matches(QtGui.QKeySequence.Cut):
1010 elif event.matches(QtGui.QKeySequence.Cut):
1008 self.cut()
1011 self.cut()
1009 intercepted = True
1012 intercepted = True
1010
1013
1011 elif event.matches(QtGui.QKeySequence.Paste):
1014 elif event.matches(QtGui.QKeySequence.Paste):
1012 self.paste()
1015 self.paste()
1013 intercepted = True
1016 intercepted = True
1014
1017
1015 #------ Special modifier logic -----------------------------------------
1018 #------ Special modifier logic -----------------------------------------
1016
1019
1017 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1020 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1018 intercepted = True
1021 intercepted = True
1019
1022
1020 # Special handling when tab completing in text mode.
1023 # Special handling when tab completing in text mode.
1021 self._cancel_text_completion()
1024 self._cancel_text_completion()
1022
1025
1023 if self._in_buffer(position):
1026 if self._in_buffer(position):
1024 # Special handling when a reading a line of raw input.
1027 # Special handling when a reading a line of raw input.
1025 if self._reading:
1028 if self._reading:
1026 self._append_plain_text('\n')
1029 self._append_plain_text('\n')
1027 self._reading = False
1030 self._reading = False
1028 if self._reading_callback:
1031 if self._reading_callback:
1029 self._reading_callback()
1032 self._reading_callback()
1030
1033
1031 # If the input buffer is a single line or there is only
1034 # If the input buffer is a single line or there is only
1032 # whitespace after the cursor, execute. Otherwise, split the
1035 # whitespace after the cursor, execute. Otherwise, split the
1033 # line with a continuation prompt.
1036 # line with a continuation prompt.
1034 elif not self._executing:
1037 elif not self._executing:
1035 cursor.movePosition(QtGui.QTextCursor.End,
1038 cursor.movePosition(QtGui.QTextCursor.End,
1036 QtGui.QTextCursor.KeepAnchor)
1039 QtGui.QTextCursor.KeepAnchor)
1037 at_end = len(cursor.selectedText().strip()) == 0
1040 at_end = len(cursor.selectedText().strip()) == 0
1038 single_line = (self._get_end_cursor().blockNumber() ==
1041 single_line = (self._get_end_cursor().blockNumber() ==
1039 self._get_prompt_cursor().blockNumber())
1042 self._get_prompt_cursor().blockNumber())
1040 if (at_end or shift_down or single_line) and not ctrl_down:
1043 if (at_end or shift_down or single_line) and not ctrl_down:
1041 self.execute(interactive = not shift_down)
1044 self.execute(interactive = not shift_down)
1042 else:
1045 else:
1043 # Do this inside an edit block for clean undo/redo.
1046 # Do this inside an edit block for clean undo/redo.
1044 cursor.beginEditBlock()
1047 cursor.beginEditBlock()
1045 cursor.setPosition(position)
1048 cursor.setPosition(position)
1046 cursor.insertText('\n')
1049 cursor.insertText('\n')
1047 self._insert_continuation_prompt(cursor)
1050 self._insert_continuation_prompt(cursor)
1048 cursor.endEditBlock()
1051 cursor.endEditBlock()
1049
1052
1050 # Ensure that the whole input buffer is visible.
1053 # Ensure that the whole input buffer is visible.
1051 # FIXME: This will not be usable if the input buffer is
1054 # FIXME: This will not be usable if the input buffer is
1052 # taller than the console widget.
1055 # taller than the console widget.
1053 self._control.moveCursor(QtGui.QTextCursor.End)
1056 self._control.moveCursor(QtGui.QTextCursor.End)
1054 self._control.setTextCursor(cursor)
1057 self._control.setTextCursor(cursor)
1055
1058
1056 #------ Control/Cmd modifier -------------------------------------------
1059 #------ Control/Cmd modifier -------------------------------------------
1057
1060
1058 elif ctrl_down:
1061 elif ctrl_down:
1059 if key == QtCore.Qt.Key_G:
1062 if key == QtCore.Qt.Key_G:
1060 self._keyboard_quit()
1063 self._keyboard_quit()
1061 intercepted = True
1064 intercepted = True
1062
1065
1063 elif key == QtCore.Qt.Key_K:
1066 elif key == QtCore.Qt.Key_K:
1064 if self._in_buffer(position):
1067 if self._in_buffer(position):
1065 cursor.clearSelection()
1068 cursor.clearSelection()
1066 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1069 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1067 QtGui.QTextCursor.KeepAnchor)
1070 QtGui.QTextCursor.KeepAnchor)
1068 if not cursor.hasSelection():
1071 if not cursor.hasSelection():
1069 # Line deletion (remove continuation prompt)
1072 # Line deletion (remove continuation prompt)
1070 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1073 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1071 QtGui.QTextCursor.KeepAnchor)
1074 QtGui.QTextCursor.KeepAnchor)
1072 cursor.movePosition(QtGui.QTextCursor.Right,
1075 cursor.movePosition(QtGui.QTextCursor.Right,
1073 QtGui.QTextCursor.KeepAnchor,
1076 QtGui.QTextCursor.KeepAnchor,
1074 len(self._continuation_prompt))
1077 len(self._continuation_prompt))
1075 self._kill_ring.kill_cursor(cursor)
1078 self._kill_ring.kill_cursor(cursor)
1076 self._set_cursor(cursor)
1079 self._set_cursor(cursor)
1077 intercepted = True
1080 intercepted = True
1078
1081
1079 elif key == QtCore.Qt.Key_L:
1082 elif key == QtCore.Qt.Key_L:
1080 self.prompt_to_top()
1083 self.prompt_to_top()
1081 intercepted = True
1084 intercepted = True
1082
1085
1083 elif key == QtCore.Qt.Key_O:
1086 elif key == QtCore.Qt.Key_O:
1084 if self._page_control and self._page_control.isVisible():
1087 if self._page_control and self._page_control.isVisible():
1085 self._page_control.setFocus()
1088 self._page_control.setFocus()
1086 intercepted = True
1089 intercepted = True
1087
1090
1088 elif key == QtCore.Qt.Key_U:
1091 elif key == QtCore.Qt.Key_U:
1089 if self._in_buffer(position):
1092 if self._in_buffer(position):
1090 cursor.clearSelection()
1093 cursor.clearSelection()
1091 start_line = cursor.blockNumber()
1094 start_line = cursor.blockNumber()
1092 if start_line == self._get_prompt_cursor().blockNumber():
1095 if start_line == self._get_prompt_cursor().blockNumber():
1093 offset = len(self._prompt)
1096 offset = len(self._prompt)
1094 else:
1097 else:
1095 offset = len(self._continuation_prompt)
1098 offset = len(self._continuation_prompt)
1096 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1099 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1097 QtGui.QTextCursor.KeepAnchor)
1100 QtGui.QTextCursor.KeepAnchor)
1098 cursor.movePosition(QtGui.QTextCursor.Right,
1101 cursor.movePosition(QtGui.QTextCursor.Right,
1099 QtGui.QTextCursor.KeepAnchor, offset)
1102 QtGui.QTextCursor.KeepAnchor, offset)
1100 self._kill_ring.kill_cursor(cursor)
1103 self._kill_ring.kill_cursor(cursor)
1101 self._set_cursor(cursor)
1104 self._set_cursor(cursor)
1102 intercepted = True
1105 intercepted = True
1103
1106
1104 elif key == QtCore.Qt.Key_Y:
1107 elif key == QtCore.Qt.Key_Y:
1105 self._keep_cursor_in_buffer()
1108 self._keep_cursor_in_buffer()
1106 self._kill_ring.yank()
1109 self._kill_ring.yank()
1107 intercepted = True
1110 intercepted = True
1108
1111
1109 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1112 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1110 if key == QtCore.Qt.Key_Backspace:
1113 if key == QtCore.Qt.Key_Backspace:
1111 cursor = self._get_word_start_cursor(position)
1114 cursor = self._get_word_start_cursor(position)
1112 else: # key == QtCore.Qt.Key_Delete
1115 else: # key == QtCore.Qt.Key_Delete
1113 cursor = self._get_word_end_cursor(position)
1116 cursor = self._get_word_end_cursor(position)
1114 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1117 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1115 self._kill_ring.kill_cursor(cursor)
1118 self._kill_ring.kill_cursor(cursor)
1116 intercepted = True
1119 intercepted = True
1117
1120
1118 elif key == QtCore.Qt.Key_D:
1121 elif key == QtCore.Qt.Key_D:
1119 if len(self.input_buffer) == 0:
1122 if len(self.input_buffer) == 0:
1120 self.exit_requested.emit(self)
1123 self.exit_requested.emit(self)
1121 else:
1124 else:
1122 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1125 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1123 QtCore.Qt.Key_Delete,
1126 QtCore.Qt.Key_Delete,
1124 QtCore.Qt.NoModifier)
1127 QtCore.Qt.NoModifier)
1125 QtGui.qApp.sendEvent(self._control, new_event)
1128 QtGui.qApp.sendEvent(self._control, new_event)
1126 intercepted = True
1129 intercepted = True
1127
1130
1128 #------ Alt modifier ---------------------------------------------------
1131 #------ Alt modifier ---------------------------------------------------
1129
1132
1130 elif alt_down:
1133 elif alt_down:
1131 if key == QtCore.Qt.Key_B:
1134 if key == QtCore.Qt.Key_B:
1132 self._set_cursor(self._get_word_start_cursor(position))
1135 self._set_cursor(self._get_word_start_cursor(position))
1133 intercepted = True
1136 intercepted = True
1134
1137
1135 elif key == QtCore.Qt.Key_F:
1138 elif key == QtCore.Qt.Key_F:
1136 self._set_cursor(self._get_word_end_cursor(position))
1139 self._set_cursor(self._get_word_end_cursor(position))
1137 intercepted = True
1140 intercepted = True
1138
1141
1139 elif key == QtCore.Qt.Key_Y:
1142 elif key == QtCore.Qt.Key_Y:
1140 self._kill_ring.rotate()
1143 self._kill_ring.rotate()
1141 intercepted = True
1144 intercepted = True
1142
1145
1143 elif key == QtCore.Qt.Key_Backspace:
1146 elif key == QtCore.Qt.Key_Backspace:
1144 cursor = self._get_word_start_cursor(position)
1147 cursor = self._get_word_start_cursor(position)
1145 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1148 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1146 self._kill_ring.kill_cursor(cursor)
1149 self._kill_ring.kill_cursor(cursor)
1147 intercepted = True
1150 intercepted = True
1148
1151
1149 elif key == QtCore.Qt.Key_D:
1152 elif key == QtCore.Qt.Key_D:
1150 cursor = self._get_word_end_cursor(position)
1153 cursor = self._get_word_end_cursor(position)
1151 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1154 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1152 self._kill_ring.kill_cursor(cursor)
1155 self._kill_ring.kill_cursor(cursor)
1153 intercepted = True
1156 intercepted = True
1154
1157
1155 elif key == QtCore.Qt.Key_Delete:
1158 elif key == QtCore.Qt.Key_Delete:
1156 intercepted = True
1159 intercepted = True
1157
1160
1158 elif key == QtCore.Qt.Key_Greater:
1161 elif key == QtCore.Qt.Key_Greater:
1159 self._control.moveCursor(QtGui.QTextCursor.End)
1162 self._control.moveCursor(QtGui.QTextCursor.End)
1160 intercepted = True
1163 intercepted = True
1161
1164
1162 elif key == QtCore.Qt.Key_Less:
1165 elif key == QtCore.Qt.Key_Less:
1163 self._control.setTextCursor(self._get_prompt_cursor())
1166 self._control.setTextCursor(self._get_prompt_cursor())
1164 intercepted = True
1167 intercepted = True
1165
1168
1166 #------ No modifiers ---------------------------------------------------
1169 #------ No modifiers ---------------------------------------------------
1167
1170
1168 else:
1171 else:
1169 if shift_down:
1172 if shift_down:
1170 anchormode = QtGui.QTextCursor.KeepAnchor
1173 anchormode = QtGui.QTextCursor.KeepAnchor
1171 else:
1174 else:
1172 anchormode = QtGui.QTextCursor.MoveAnchor
1175 anchormode = QtGui.QTextCursor.MoveAnchor
1173
1176
1174 if key == QtCore.Qt.Key_Escape:
1177 if key == QtCore.Qt.Key_Escape:
1175 self._keyboard_quit()
1178 self._keyboard_quit()
1176 intercepted = True
1179 intercepted = True
1177
1180
1178 elif key == QtCore.Qt.Key_Up:
1181 elif key == QtCore.Qt.Key_Up:
1179 if self._reading or not self._up_pressed(shift_down):
1182 if self._reading or not self._up_pressed(shift_down):
1180 intercepted = True
1183 intercepted = True
1181 else:
1184 else:
1182 prompt_line = self._get_prompt_cursor().blockNumber()
1185 prompt_line = self._get_prompt_cursor().blockNumber()
1183 intercepted = cursor.blockNumber() <= prompt_line
1186 intercepted = cursor.blockNumber() <= prompt_line
1184
1187
1185 elif key == QtCore.Qt.Key_Down:
1188 elif key == QtCore.Qt.Key_Down:
1186 if self._reading or not self._down_pressed(shift_down):
1189 if self._reading or not self._down_pressed(shift_down):
1187 intercepted = True
1190 intercepted = True
1188 else:
1191 else:
1189 end_line = self._get_end_cursor().blockNumber()
1192 end_line = self._get_end_cursor().blockNumber()
1190 intercepted = cursor.blockNumber() == end_line
1193 intercepted = cursor.blockNumber() == end_line
1191
1194
1192 elif key == QtCore.Qt.Key_Tab:
1195 elif key == QtCore.Qt.Key_Tab:
1193 if not self._reading:
1196 if not self._reading:
1194 if self._tab_pressed():
1197 if self._tab_pressed():
1195 # real tab-key, insert four spaces
1198 # real tab-key, insert four spaces
1196 cursor.insertText(' '*4)
1199 cursor.insertText(' '*4)
1197 intercepted = True
1200 intercepted = True
1198
1201
1199 elif key == QtCore.Qt.Key_Left:
1202 elif key == QtCore.Qt.Key_Left:
1200
1203
1201 # Move to the previous line
1204 # Move to the previous line
1202 line, col = cursor.blockNumber(), cursor.columnNumber()
1205 line, col = cursor.blockNumber(), cursor.columnNumber()
1203 if line > self._get_prompt_cursor().blockNumber() and \
1206 if line > self._get_prompt_cursor().blockNumber() and \
1204 col == len(self._continuation_prompt):
1207 col == len(self._continuation_prompt):
1205 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1208 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1206 mode=anchormode)
1209 mode=anchormode)
1207 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1210 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1208 mode=anchormode)
1211 mode=anchormode)
1209 intercepted = True
1212 intercepted = True
1210
1213
1211 # Regular left movement
1214 # Regular left movement
1212 else:
1215 else:
1213 intercepted = not self._in_buffer(position - 1)
1216 intercepted = not self._in_buffer(position - 1)
1214
1217
1215 elif key == QtCore.Qt.Key_Right:
1218 elif key == QtCore.Qt.Key_Right:
1216 original_block_number = cursor.blockNumber()
1219 original_block_number = cursor.blockNumber()
1217 cursor.movePosition(QtGui.QTextCursor.Right,
1220 cursor.movePosition(QtGui.QTextCursor.Right,
1218 mode=anchormode)
1221 mode=anchormode)
1219 if cursor.blockNumber() != original_block_number:
1222 if cursor.blockNumber() != original_block_number:
1220 cursor.movePosition(QtGui.QTextCursor.Right,
1223 cursor.movePosition(QtGui.QTextCursor.Right,
1221 n=len(self._continuation_prompt),
1224 n=len(self._continuation_prompt),
1222 mode=anchormode)
1225 mode=anchormode)
1223 self._set_cursor(cursor)
1226 self._set_cursor(cursor)
1224 intercepted = True
1227 intercepted = True
1225
1228
1226 elif key == QtCore.Qt.Key_Home:
1229 elif key == QtCore.Qt.Key_Home:
1227 start_line = cursor.blockNumber()
1230 start_line = cursor.blockNumber()
1228 if start_line == self._get_prompt_cursor().blockNumber():
1231 if start_line == self._get_prompt_cursor().blockNumber():
1229 start_pos = self._prompt_pos
1232 start_pos = self._prompt_pos
1230 else:
1233 else:
1231 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1234 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1232 QtGui.QTextCursor.KeepAnchor)
1235 QtGui.QTextCursor.KeepAnchor)
1233 start_pos = cursor.position()
1236 start_pos = cursor.position()
1234 start_pos += len(self._continuation_prompt)
1237 start_pos += len(self._continuation_prompt)
1235 cursor.setPosition(position)
1238 cursor.setPosition(position)
1236 if shift_down and self._in_buffer(position):
1239 if shift_down and self._in_buffer(position):
1237 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1240 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1238 else:
1241 else:
1239 cursor.setPosition(start_pos)
1242 cursor.setPosition(start_pos)
1240 self._set_cursor(cursor)
1243 self._set_cursor(cursor)
1241 intercepted = True
1244 intercepted = True
1242
1245
1243 elif key == QtCore.Qt.Key_Backspace:
1246 elif key == QtCore.Qt.Key_Backspace:
1244
1247
1245 # Line deletion (remove continuation prompt)
1248 # Line deletion (remove continuation prompt)
1246 line, col = cursor.blockNumber(), cursor.columnNumber()
1249 line, col = cursor.blockNumber(), cursor.columnNumber()
1247 if not self._reading and \
1250 if not self._reading and \
1248 col == len(self._continuation_prompt) and \
1251 col == len(self._continuation_prompt) and \
1249 line > self._get_prompt_cursor().blockNumber():
1252 line > self._get_prompt_cursor().blockNumber():
1250 cursor.beginEditBlock()
1253 cursor.beginEditBlock()
1251 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1254 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1252 QtGui.QTextCursor.KeepAnchor)
1255 QtGui.QTextCursor.KeepAnchor)
1253 cursor.removeSelectedText()
1256 cursor.removeSelectedText()
1254 cursor.deletePreviousChar()
1257 cursor.deletePreviousChar()
1255 cursor.endEditBlock()
1258 cursor.endEditBlock()
1256 intercepted = True
1259 intercepted = True
1257
1260
1258 # Regular backwards deletion
1261 # Regular backwards deletion
1259 else:
1262 else:
1260 anchor = cursor.anchor()
1263 anchor = cursor.anchor()
1261 if anchor == position:
1264 if anchor == position:
1262 intercepted = not self._in_buffer(position - 1)
1265 intercepted = not self._in_buffer(position - 1)
1263 else:
1266 else:
1264 intercepted = not self._in_buffer(min(anchor, position))
1267 intercepted = not self._in_buffer(min(anchor, position))
1265
1268
1266 elif key == QtCore.Qt.Key_Delete:
1269 elif key == QtCore.Qt.Key_Delete:
1267
1270
1268 # Line deletion (remove continuation prompt)
1271 # Line deletion (remove continuation prompt)
1269 if not self._reading and self._in_buffer(position) and \
1272 if not self._reading and self._in_buffer(position) and \
1270 cursor.atBlockEnd() and not cursor.hasSelection():
1273 cursor.atBlockEnd() and not cursor.hasSelection():
1271 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1274 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1272 QtGui.QTextCursor.KeepAnchor)
1275 QtGui.QTextCursor.KeepAnchor)
1273 cursor.movePosition(QtGui.QTextCursor.Right,
1276 cursor.movePosition(QtGui.QTextCursor.Right,
1274 QtGui.QTextCursor.KeepAnchor,
1277 QtGui.QTextCursor.KeepAnchor,
1275 len(self._continuation_prompt))
1278 len(self._continuation_prompt))
1276 cursor.removeSelectedText()
1279 cursor.removeSelectedText()
1277 intercepted = True
1280 intercepted = True
1278
1281
1279 # Regular forwards deletion:
1282 # Regular forwards deletion:
1280 else:
1283 else:
1281 anchor = cursor.anchor()
1284 anchor = cursor.anchor()
1282 intercepted = (not self._in_buffer(anchor) or
1285 intercepted = (not self._in_buffer(anchor) or
1283 not self._in_buffer(position))
1286 not self._in_buffer(position))
1284
1287
1285 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1288 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1286 # using the keyboard in any part of the buffer. Also, permit scrolling
1289 # using the keyboard in any part of the buffer. Also, permit scrolling
1287 # with Page Up/Down keys. Finally, if we're executing, don't move the
1290 # with Page Up/Down keys. Finally, if we're executing, don't move the
1288 # cursor (if even this made sense, we can't guarantee that the prompt
1291 # cursor (if even this made sense, we can't guarantee that the prompt
1289 # position is still valid due to text truncation).
1292 # position is still valid due to text truncation).
1290 if not (self._control_key_down(event.modifiers(), include_command=True)
1293 if not (self._control_key_down(event.modifiers(), include_command=True)
1291 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1294 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1292 or (self._executing and not self._reading)):
1295 or (self._executing and not self._reading)):
1293 self._keep_cursor_in_buffer()
1296 self._keep_cursor_in_buffer()
1294
1297
1295 return intercepted
1298 return intercepted
1296
1299
1297 def _event_filter_page_keypress(self, event):
1300 def _event_filter_page_keypress(self, event):
1298 """ Filter key events for the paging widget to create console-like
1301 """ Filter key events for the paging widget to create console-like
1299 interface.
1302 interface.
1300 """
1303 """
1301 key = event.key()
1304 key = event.key()
1302 ctrl_down = self._control_key_down(event.modifiers())
1305 ctrl_down = self._control_key_down(event.modifiers())
1303 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1306 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1304
1307
1305 if ctrl_down:
1308 if ctrl_down:
1306 if key == QtCore.Qt.Key_O:
1309 if key == QtCore.Qt.Key_O:
1307 self._control.setFocus()
1310 self._control.setFocus()
1308 intercept = True
1311 intercept = True
1309
1312
1310 elif alt_down:
1313 elif alt_down:
1311 if key == QtCore.Qt.Key_Greater:
1314 if key == QtCore.Qt.Key_Greater:
1312 self._page_control.moveCursor(QtGui.QTextCursor.End)
1315 self._page_control.moveCursor(QtGui.QTextCursor.End)
1313 intercepted = True
1316 intercepted = True
1314
1317
1315 elif key == QtCore.Qt.Key_Less:
1318 elif key == QtCore.Qt.Key_Less:
1316 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1319 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1317 intercepted = True
1320 intercepted = True
1318
1321
1319 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1322 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1320 if self._splitter:
1323 if self._splitter:
1321 self._page_control.hide()
1324 self._page_control.hide()
1322 self._control.setFocus()
1325 self._control.setFocus()
1323 else:
1326 else:
1324 self.layout().setCurrentWidget(self._control)
1327 self.layout().setCurrentWidget(self._control)
1325 return True
1328 return True
1326
1329
1327 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1330 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1328 QtCore.Qt.Key_Tab):
1331 QtCore.Qt.Key_Tab):
1329 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1332 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1330 QtCore.Qt.Key_PageDown,
1333 QtCore.Qt.Key_PageDown,
1331 QtCore.Qt.NoModifier)
1334 QtCore.Qt.NoModifier)
1332 QtGui.qApp.sendEvent(self._page_control, new_event)
1335 QtGui.qApp.sendEvent(self._page_control, new_event)
1333 return True
1336 return True
1334
1337
1335 elif key == QtCore.Qt.Key_Backspace:
1338 elif key == QtCore.Qt.Key_Backspace:
1336 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1339 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1337 QtCore.Qt.Key_PageUp,
1340 QtCore.Qt.Key_PageUp,
1338 QtCore.Qt.NoModifier)
1341 QtCore.Qt.NoModifier)
1339 QtGui.qApp.sendEvent(self._page_control, new_event)
1342 QtGui.qApp.sendEvent(self._page_control, new_event)
1340 return True
1343 return True
1341
1344
1342 return False
1345 return False
1343
1346
1344 def _format_as_columns(self, items, separator=' '):
1347 def _format_as_columns(self, items, separator=' '):
1345 """ Transform a list of strings into a single string with columns.
1348 """ Transform a list of strings into a single string with columns.
1346
1349
1347 Parameters
1350 Parameters
1348 ----------
1351 ----------
1349 items : sequence of strings
1352 items : sequence of strings
1350 The strings to process.
1353 The strings to process.
1351
1354
1352 separator : str, optional [default is two spaces]
1355 separator : str, optional [default is two spaces]
1353 The string that separates columns.
1356 The string that separates columns.
1354
1357
1355 Returns
1358 Returns
1356 -------
1359 -------
1357 The formatted string.
1360 The formatted string.
1358 """
1361 """
1359 # Calculate the number of characters available.
1362 # Calculate the number of characters available.
1360 width = self._control.viewport().width()
1363 width = self._control.viewport().width()
1361 char_width = QtGui.QFontMetrics(self.font).width(' ')
1364 char_width = QtGui.QFontMetrics(self.font).width(' ')
1362 displaywidth = max(10, (width / char_width) - 1)
1365 displaywidth = max(10, (width / char_width) - 1)
1363
1366
1364 return columnize(items, separator, displaywidth)
1367 return columnize(items, separator, displaywidth)
1365
1368
1366 def _get_block_plain_text(self, block):
1369 def _get_block_plain_text(self, block):
1367 """ Given a QTextBlock, return its unformatted text.
1370 """ Given a QTextBlock, return its unformatted text.
1368 """
1371 """
1369 cursor = QtGui.QTextCursor(block)
1372 cursor = QtGui.QTextCursor(block)
1370 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1373 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1371 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1374 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1372 QtGui.QTextCursor.KeepAnchor)
1375 QtGui.QTextCursor.KeepAnchor)
1373 return cursor.selection().toPlainText()
1376 return cursor.selection().toPlainText()
1374
1377
1375 def _get_cursor(self):
1378 def _get_cursor(self):
1376 """ Convenience method that returns a cursor for the current position.
1379 """ Convenience method that returns a cursor for the current position.
1377 """
1380 """
1378 return self._control.textCursor()
1381 return self._control.textCursor()
1379
1382
1380 def _get_end_cursor(self):
1383 def _get_end_cursor(self):
1381 """ Convenience method that returns a cursor for the last character.
1384 """ Convenience method that returns a cursor for the last character.
1382 """
1385 """
1383 cursor = self._control.textCursor()
1386 cursor = self._control.textCursor()
1384 cursor.movePosition(QtGui.QTextCursor.End)
1387 cursor.movePosition(QtGui.QTextCursor.End)
1385 return cursor
1388 return cursor
1386
1389
1387 def _get_input_buffer_cursor_column(self):
1390 def _get_input_buffer_cursor_column(self):
1388 """ Returns the column of the cursor in the input buffer, excluding the
1391 """ Returns the column of the cursor in the input buffer, excluding the
1389 contribution by the prompt, or -1 if there is no such column.
1392 contribution by the prompt, or -1 if there is no such column.
1390 """
1393 """
1391 prompt = self._get_input_buffer_cursor_prompt()
1394 prompt = self._get_input_buffer_cursor_prompt()
1392 if prompt is None:
1395 if prompt is None:
1393 return -1
1396 return -1
1394 else:
1397 else:
1395 cursor = self._control.textCursor()
1398 cursor = self._control.textCursor()
1396 return cursor.columnNumber() - len(prompt)
1399 return cursor.columnNumber() - len(prompt)
1397
1400
1398 def _get_input_buffer_cursor_line(self):
1401 def _get_input_buffer_cursor_line(self):
1399 """ Returns the text of the line of the input buffer that contains the
1402 """ Returns the text of the line of the input buffer that contains the
1400 cursor, or None if there is no such line.
1403 cursor, or None if there is no such line.
1401 """
1404 """
1402 prompt = self._get_input_buffer_cursor_prompt()
1405 prompt = self._get_input_buffer_cursor_prompt()
1403 if prompt is None:
1406 if prompt is None:
1404 return None
1407 return None
1405 else:
1408 else:
1406 cursor = self._control.textCursor()
1409 cursor = self._control.textCursor()
1407 text = self._get_block_plain_text(cursor.block())
1410 text = self._get_block_plain_text(cursor.block())
1408 return text[len(prompt):]
1411 return text[len(prompt):]
1409
1412
1410 def _get_input_buffer_cursor_prompt(self):
1413 def _get_input_buffer_cursor_prompt(self):
1411 """ Returns the (plain text) prompt for line of the input buffer that
1414 """ Returns the (plain text) prompt for line of the input buffer that
1412 contains the cursor, or None if there is no such line.
1415 contains the cursor, or None if there is no such line.
1413 """
1416 """
1414 if self._executing:
1417 if self._executing:
1415 return None
1418 return None
1416 cursor = self._control.textCursor()
1419 cursor = self._control.textCursor()
1417 if cursor.position() >= self._prompt_pos:
1420 if cursor.position() >= self._prompt_pos:
1418 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1421 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1419 return self._prompt
1422 return self._prompt
1420 else:
1423 else:
1421 return self._continuation_prompt
1424 return self._continuation_prompt
1422 else:
1425 else:
1423 return None
1426 return None
1424
1427
1425 def _get_prompt_cursor(self):
1428 def _get_prompt_cursor(self):
1426 """ Convenience method that returns a cursor for the prompt position.
1429 """ Convenience method that returns a cursor for the prompt position.
1427 """
1430 """
1428 cursor = self._control.textCursor()
1431 cursor = self._control.textCursor()
1429 cursor.setPosition(self._prompt_pos)
1432 cursor.setPosition(self._prompt_pos)
1430 return cursor
1433 return cursor
1431
1434
1432 def _get_selection_cursor(self, start, end):
1435 def _get_selection_cursor(self, start, end):
1433 """ Convenience method that returns a cursor with text selected between
1436 """ Convenience method that returns a cursor with text selected between
1434 the positions 'start' and 'end'.
1437 the positions 'start' and 'end'.
1435 """
1438 """
1436 cursor = self._control.textCursor()
1439 cursor = self._control.textCursor()
1437 cursor.setPosition(start)
1440 cursor.setPosition(start)
1438 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1441 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1439 return cursor
1442 return cursor
1440
1443
1441 def _get_word_start_cursor(self, position):
1444 def _get_word_start_cursor(self, position):
1442 """ Find the start of the word to the left the given position. If a
1445 """ Find the start of the word to the left the given position. If a
1443 sequence of non-word characters precedes the first word, skip over
1446 sequence of non-word characters precedes the first word, skip over
1444 them. (This emulates the behavior of bash, emacs, etc.)
1447 them. (This emulates the behavior of bash, emacs, etc.)
1445 """
1448 """
1446 document = self._control.document()
1449 document = self._control.document()
1447 position -= 1
1450 position -= 1
1448 while position >= self._prompt_pos and \
1451 while position >= self._prompt_pos and \
1449 not is_letter_or_number(document.characterAt(position)):
1452 not is_letter_or_number(document.characterAt(position)):
1450 position -= 1
1453 position -= 1
1451 while position >= self._prompt_pos and \
1454 while position >= self._prompt_pos and \
1452 is_letter_or_number(document.characterAt(position)):
1455 is_letter_or_number(document.characterAt(position)):
1453 position -= 1
1456 position -= 1
1454 cursor = self._control.textCursor()
1457 cursor = self._control.textCursor()
1455 cursor.setPosition(position + 1)
1458 cursor.setPosition(position + 1)
1456 return cursor
1459 return cursor
1457
1460
1458 def _get_word_end_cursor(self, position):
1461 def _get_word_end_cursor(self, position):
1459 """ Find the end of the word to the right the given position. If a
1462 """ Find the end of the word to the right the given position. If a
1460 sequence of non-word characters precedes the first word, skip over
1463 sequence of non-word characters precedes the first word, skip over
1461 them. (This emulates the behavior of bash, emacs, etc.)
1464 them. (This emulates the behavior of bash, emacs, etc.)
1462 """
1465 """
1463 document = self._control.document()
1466 document = self._control.document()
1464 end = self._get_end_cursor().position()
1467 end = self._get_end_cursor().position()
1465 while position < end and \
1468 while position < end and \
1466 not is_letter_or_number(document.characterAt(position)):
1469 not is_letter_or_number(document.characterAt(position)):
1467 position += 1
1470 position += 1
1468 while position < end and \
1471 while position < end and \
1469 is_letter_or_number(document.characterAt(position)):
1472 is_letter_or_number(document.characterAt(position)):
1470 position += 1
1473 position += 1
1471 cursor = self._control.textCursor()
1474 cursor = self._control.textCursor()
1472 cursor.setPosition(position)
1475 cursor.setPosition(position)
1473 return cursor
1476 return cursor
1474
1477
1475 def _insert_continuation_prompt(self, cursor):
1478 def _insert_continuation_prompt(self, cursor):
1476 """ Inserts new continuation prompt using the specified cursor.
1479 """ Inserts new continuation prompt using the specified cursor.
1477 """
1480 """
1478 if self._continuation_prompt_html is None:
1481 if self._continuation_prompt_html is None:
1479 self._insert_plain_text(cursor, self._continuation_prompt)
1482 self._insert_plain_text(cursor, self._continuation_prompt)
1480 else:
1483 else:
1481 self._continuation_prompt = self._insert_html_fetching_plain_text(
1484 self._continuation_prompt = self._insert_html_fetching_plain_text(
1482 cursor, self._continuation_prompt_html)
1485 cursor, self._continuation_prompt_html)
1483
1486
1484 def _insert_html(self, cursor, html):
1487 def _insert_html(self, cursor, html):
1485 """ Inserts HTML using the specified cursor in such a way that future
1488 """ Inserts HTML using the specified cursor in such a way that future
1486 formatting is unaffected.
1489 formatting is unaffected.
1487 """
1490 """
1488 cursor.beginEditBlock()
1491 cursor.beginEditBlock()
1489 cursor.insertHtml(html)
1492 cursor.insertHtml(html)
1490
1493
1491 # After inserting HTML, the text document "remembers" it's in "html
1494 # After inserting HTML, the text document "remembers" it's in "html
1492 # mode", which means that subsequent calls adding plain text will result
1495 # mode", which means that subsequent calls adding plain text will result
1493 # in unwanted formatting, lost tab characters, etc. The following code
1496 # in unwanted formatting, lost tab characters, etc. The following code
1494 # hacks around this behavior, which I consider to be a bug in Qt, by
1497 # hacks around this behavior, which I consider to be a bug in Qt, by
1495 # (crudely) resetting the document's style state.
1498 # (crudely) resetting the document's style state.
1496 cursor.movePosition(QtGui.QTextCursor.Left,
1499 cursor.movePosition(QtGui.QTextCursor.Left,
1497 QtGui.QTextCursor.KeepAnchor)
1500 QtGui.QTextCursor.KeepAnchor)
1498 if cursor.selection().toPlainText() == ' ':
1501 if cursor.selection().toPlainText() == ' ':
1499 cursor.removeSelectedText()
1502 cursor.removeSelectedText()
1500 else:
1503 else:
1501 cursor.movePosition(QtGui.QTextCursor.Right)
1504 cursor.movePosition(QtGui.QTextCursor.Right)
1502 cursor.insertText(' ', QtGui.QTextCharFormat())
1505 cursor.insertText(' ', QtGui.QTextCharFormat())
1503 cursor.endEditBlock()
1506 cursor.endEditBlock()
1504
1507
1505 def _insert_html_fetching_plain_text(self, cursor, html):
1508 def _insert_html_fetching_plain_text(self, cursor, html):
1506 """ Inserts HTML using the specified cursor, then returns its plain text
1509 """ Inserts HTML using the specified cursor, then returns its plain text
1507 version.
1510 version.
1508 """
1511 """
1509 cursor.beginEditBlock()
1512 cursor.beginEditBlock()
1510 cursor.removeSelectedText()
1513 cursor.removeSelectedText()
1511
1514
1512 start = cursor.position()
1515 start = cursor.position()
1513 self._insert_html(cursor, html)
1516 self._insert_html(cursor, html)
1514 end = cursor.position()
1517 end = cursor.position()
1515 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1518 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1516 text = cursor.selection().toPlainText()
1519 text = cursor.selection().toPlainText()
1517
1520
1518 cursor.setPosition(end)
1521 cursor.setPosition(end)
1519 cursor.endEditBlock()
1522 cursor.endEditBlock()
1520 return text
1523 return text
1521
1524
1522 def _insert_plain_text(self, cursor, text):
1525 def _insert_plain_text(self, cursor, text):
1523 """ Inserts plain text using the specified cursor, processing ANSI codes
1526 """ Inserts plain text using the specified cursor, processing ANSI codes
1524 if enabled.
1527 if enabled.
1525 """
1528 """
1526 cursor.beginEditBlock()
1529 cursor.beginEditBlock()
1527 if self.ansi_codes:
1530 if self.ansi_codes:
1528 for substring in self._ansi_processor.split_string(text):
1531 for substring in self._ansi_processor.split_string(text):
1529 for act in self._ansi_processor.actions:
1532 for act in self._ansi_processor.actions:
1530
1533
1531 # Unlike real terminal emulators, we don't distinguish
1534 # Unlike real terminal emulators, we don't distinguish
1532 # between the screen and the scrollback buffer. A screen
1535 # between the screen and the scrollback buffer. A screen
1533 # erase request clears everything.
1536 # erase request clears everything.
1534 if act.action == 'erase' and act.area == 'screen':
1537 if act.action == 'erase' and act.area == 'screen':
1535 cursor.select(QtGui.QTextCursor.Document)
1538 cursor.select(QtGui.QTextCursor.Document)
1536 cursor.removeSelectedText()
1539 cursor.removeSelectedText()
1537
1540
1538 # Simulate a form feed by scrolling just past the last line.
1541 # Simulate a form feed by scrolling just past the last line.
1539 elif act.action == 'scroll' and act.unit == 'page':
1542 elif act.action == 'scroll' and act.unit == 'page':
1540 cursor.insertText('\n')
1543 cursor.insertText('\n')
1541 cursor.endEditBlock()
1544 cursor.endEditBlock()
1542 self._set_top_cursor(cursor)
1545 self._set_top_cursor(cursor)
1543 cursor.joinPreviousEditBlock()
1546 cursor.joinPreviousEditBlock()
1544 cursor.deletePreviousChar()
1547 cursor.deletePreviousChar()
1545
1548
1546 elif act.action == 'carriage-return':
1549 elif act.action == 'carriage-return':
1547 cursor.movePosition(
1550 cursor.movePosition(
1548 cursor.StartOfLine, cursor.KeepAnchor)
1551 cursor.StartOfLine, cursor.KeepAnchor)
1549
1552
1550 elif act.action == 'beep':
1553 elif act.action == 'beep':
1551 QtGui.qApp.beep()
1554 QtGui.qApp.beep()
1552
1555
1553 format = self._ansi_processor.get_format()
1556 format = self._ansi_processor.get_format()
1554 cursor.insertText(substring, format)
1557 cursor.insertText(substring, format)
1555 else:
1558 else:
1556 cursor.insertText(text)
1559 cursor.insertText(text)
1557 cursor.endEditBlock()
1560 cursor.endEditBlock()
1558
1561
1559 def _insert_plain_text_into_buffer(self, cursor, text):
1562 def _insert_plain_text_into_buffer(self, cursor, text):
1560 """ Inserts text into the input buffer using the specified cursor (which
1563 """ Inserts text into the input buffer using the specified cursor (which
1561 must be in the input buffer), ensuring that continuation prompts are
1564 must be in the input buffer), ensuring that continuation prompts are
1562 inserted as necessary.
1565 inserted as necessary.
1563 """
1566 """
1564 lines = text.splitlines(True)
1567 lines = text.splitlines(True)
1565 if lines:
1568 if lines:
1566 cursor.beginEditBlock()
1569 cursor.beginEditBlock()
1567 cursor.insertText(lines[0])
1570 cursor.insertText(lines[0])
1568 for line in lines[1:]:
1571 for line in lines[1:]:
1569 if self._continuation_prompt_html is None:
1572 if self._continuation_prompt_html is None:
1570 cursor.insertText(self._continuation_prompt)
1573 cursor.insertText(self._continuation_prompt)
1571 else:
1574 else:
1572 self._continuation_prompt = \
1575 self._continuation_prompt = \
1573 self._insert_html_fetching_plain_text(
1576 self._insert_html_fetching_plain_text(
1574 cursor, self._continuation_prompt_html)
1577 cursor, self._continuation_prompt_html)
1575 cursor.insertText(line)
1578 cursor.insertText(line)
1576 cursor.endEditBlock()
1579 cursor.endEditBlock()
1577
1580
1578 def _in_buffer(self, position=None):
1581 def _in_buffer(self, position=None):
1579 """ Returns whether the current cursor (or, if specified, a position) is
1582 """ Returns whether the current cursor (or, if specified, a position) is
1580 inside the editing region.
1583 inside the editing region.
1581 """
1584 """
1582 cursor = self._control.textCursor()
1585 cursor = self._control.textCursor()
1583 if position is None:
1586 if position is None:
1584 position = cursor.position()
1587 position = cursor.position()
1585 else:
1588 else:
1586 cursor.setPosition(position)
1589 cursor.setPosition(position)
1587 line = cursor.blockNumber()
1590 line = cursor.blockNumber()
1588 prompt_line = self._get_prompt_cursor().blockNumber()
1591 prompt_line = self._get_prompt_cursor().blockNumber()
1589 if line == prompt_line:
1592 if line == prompt_line:
1590 return position >= self._prompt_pos
1593 return position >= self._prompt_pos
1591 elif line > prompt_line:
1594 elif line > prompt_line:
1592 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1595 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1593 prompt_pos = cursor.position() + len(self._continuation_prompt)
1596 prompt_pos = cursor.position() + len(self._continuation_prompt)
1594 return position >= prompt_pos
1597 return position >= prompt_pos
1595 return False
1598 return False
1596
1599
1597 def _keep_cursor_in_buffer(self):
1600 def _keep_cursor_in_buffer(self):
1598 """ Ensures that the cursor is inside the editing region. Returns
1601 """ Ensures that the cursor is inside the editing region. Returns
1599 whether the cursor was moved.
1602 whether the cursor was moved.
1600 """
1603 """
1601 moved = not self._in_buffer()
1604 moved = not self._in_buffer()
1602 if moved:
1605 if moved:
1603 cursor = self._control.textCursor()
1606 cursor = self._control.textCursor()
1604 cursor.movePosition(QtGui.QTextCursor.End)
1607 cursor.movePosition(QtGui.QTextCursor.End)
1605 self._control.setTextCursor(cursor)
1608 self._control.setTextCursor(cursor)
1606 return moved
1609 return moved
1607
1610
1608 def _keyboard_quit(self):
1611 def _keyboard_quit(self):
1609 """ Cancels the current editing task ala Ctrl-G in Emacs.
1612 """ Cancels the current editing task ala Ctrl-G in Emacs.
1610 """
1613 """
1611 if self._text_completing_pos:
1614 if self._text_completing_pos:
1612 self._cancel_text_completion()
1615 self._cancel_text_completion()
1613 else:
1616 else:
1614 self.input_buffer = ''
1617 self.input_buffer = ''
1615
1618
1616 def _page(self, text, html=False):
1619 def _page(self, text, html=False):
1617 """ Displays text using the pager if it exceeds the height of the
1620 """ Displays text using the pager if it exceeds the height of the
1618 viewport.
1621 viewport.
1619
1622
1620 Parameters:
1623 Parameters:
1621 -----------
1624 -----------
1622 html : bool, optional (default False)
1625 html : bool, optional (default False)
1623 If set, the text will be interpreted as HTML instead of plain text.
1626 If set, the text will be interpreted as HTML instead of plain text.
1624 """
1627 """
1625 line_height = QtGui.QFontMetrics(self.font).height()
1628 line_height = QtGui.QFontMetrics(self.font).height()
1626 minlines = self._control.viewport().height() / line_height
1629 minlines = self._control.viewport().height() / line_height
1627 if self.paging != 'none' and \
1630 if self.paging != 'none' and \
1628 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1631 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1629 if self.paging == 'custom':
1632 if self.paging == 'custom':
1630 self.custom_page_requested.emit(text)
1633 self.custom_page_requested.emit(text)
1631 else:
1634 else:
1632 self._page_control.clear()
1635 self._page_control.clear()
1633 cursor = self._page_control.textCursor()
1636 cursor = self._page_control.textCursor()
1634 if html:
1637 if html:
1635 self._insert_html(cursor, text)
1638 self._insert_html(cursor, text)
1636 else:
1639 else:
1637 self._insert_plain_text(cursor, text)
1640 self._insert_plain_text(cursor, text)
1638 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1641 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1639
1642
1640 self._page_control.viewport().resize(self._control.size())
1643 self._page_control.viewport().resize(self._control.size())
1641 if self._splitter:
1644 if self._splitter:
1642 self._page_control.show()
1645 self._page_control.show()
1643 self._page_control.setFocus()
1646 self._page_control.setFocus()
1644 else:
1647 else:
1645 self.layout().setCurrentWidget(self._page_control)
1648 self.layout().setCurrentWidget(self._page_control)
1646 elif html:
1649 elif html:
1647 self._append_plain_html(text)
1650 self._append_plain_html(text)
1648 else:
1651 else:
1649 self._append_plain_text(text)
1652 self._append_plain_text(text)
1650
1653
1651 def _prompt_finished(self):
1654 def _prompt_finished(self):
1652 """ Called immediately after a prompt is finished, i.e. when some input
1655 """ Called immediately after a prompt is finished, i.e. when some input
1653 will be processed and a new prompt displayed.
1656 will be processed and a new prompt displayed.
1654 """
1657 """
1655 self._control.setReadOnly(True)
1658 self._control.setReadOnly(True)
1656 self._prompt_finished_hook()
1659 self._prompt_finished_hook()
1657
1660
1658 def _prompt_started(self):
1661 def _prompt_started(self):
1659 """ Called immediately after a new prompt is displayed.
1662 """ Called immediately after a new prompt is displayed.
1660 """
1663 """
1661 # Temporarily disable the maximum block count to permit undo/redo and
1664 # Temporarily disable the maximum block count to permit undo/redo and
1662 # to ensure that the prompt position does not change due to truncation.
1665 # to ensure that the prompt position does not change due to truncation.
1663 self._control.document().setMaximumBlockCount(0)
1666 self._control.document().setMaximumBlockCount(0)
1664 self._control.setUndoRedoEnabled(True)
1667 self._control.setUndoRedoEnabled(True)
1665
1668
1666 # Work around bug in QPlainTextEdit: input method is not re-enabled
1669 # Work around bug in QPlainTextEdit: input method is not re-enabled
1667 # when read-only is disabled.
1670 # when read-only is disabled.
1668 self._control.setReadOnly(False)
1671 self._control.setReadOnly(False)
1669 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1672 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1670
1673
1671 if not self._reading:
1674 if not self._reading:
1672 self._executing = False
1675 self._executing = False
1673 self._prompt_started_hook()
1676 self._prompt_started_hook()
1674
1677
1675 # If the input buffer has changed while executing, load it.
1678 # If the input buffer has changed while executing, load it.
1676 if self._input_buffer_pending:
1679 if self._input_buffer_pending:
1677 self.input_buffer = self._input_buffer_pending
1680 self.input_buffer = self._input_buffer_pending
1678 self._input_buffer_pending = ''
1681 self._input_buffer_pending = ''
1679
1682
1680 self._control.moveCursor(QtGui.QTextCursor.End)
1683 self._control.moveCursor(QtGui.QTextCursor.End)
1681
1684
1682 def _readline(self, prompt='', callback=None):
1685 def _readline(self, prompt='', callback=None):
1683 """ Reads one line of input from the user.
1686 """ Reads one line of input from the user.
1684
1687
1685 Parameters
1688 Parameters
1686 ----------
1689 ----------
1687 prompt : str, optional
1690 prompt : str, optional
1688 The prompt to print before reading the line.
1691 The prompt to print before reading the line.
1689
1692
1690 callback : callable, optional
1693 callback : callable, optional
1691 A callback to execute with the read line. If not specified, input is
1694 A callback to execute with the read line. If not specified, input is
1692 read *synchronously* and this method does not return until it has
1695 read *synchronously* and this method does not return until it has
1693 been read.
1696 been read.
1694
1697
1695 Returns
1698 Returns
1696 -------
1699 -------
1697 If a callback is specified, returns nothing. Otherwise, returns the
1700 If a callback is specified, returns nothing. Otherwise, returns the
1698 input string with the trailing newline stripped.
1701 input string with the trailing newline stripped.
1699 """
1702 """
1700 if self._reading:
1703 if self._reading:
1701 raise RuntimeError('Cannot read a line. Widget is already reading.')
1704 raise RuntimeError('Cannot read a line. Widget is already reading.')
1702
1705
1703 if not callback and not self.isVisible():
1706 if not callback and not self.isVisible():
1704 # If the user cannot see the widget, this function cannot return.
1707 # If the user cannot see the widget, this function cannot return.
1705 raise RuntimeError('Cannot synchronously read a line if the widget '
1708 raise RuntimeError('Cannot synchronously read a line if the widget '
1706 'is not visible!')
1709 'is not visible!')
1707
1710
1708 self._reading = True
1711 self._reading = True
1709 self._show_prompt(prompt, newline=False)
1712 self._show_prompt(prompt, newline=False)
1710
1713
1711 if callback is None:
1714 if callback is None:
1712 self._reading_callback = None
1715 self._reading_callback = None
1713 while self._reading:
1716 while self._reading:
1714 QtCore.QCoreApplication.processEvents()
1717 QtCore.QCoreApplication.processEvents()
1715 return self._get_input_buffer(force=True).rstrip('\n')
1718 return self._get_input_buffer(force=True).rstrip('\n')
1716
1719
1717 else:
1720 else:
1718 self._reading_callback = lambda: \
1721 self._reading_callback = lambda: \
1719 callback(self._get_input_buffer(force=True).rstrip('\n'))
1722 callback(self._get_input_buffer(force=True).rstrip('\n'))
1720
1723
1721 def _set_continuation_prompt(self, prompt, html=False):
1724 def _set_continuation_prompt(self, prompt, html=False):
1722 """ Sets the continuation prompt.
1725 """ Sets the continuation prompt.
1723
1726
1724 Parameters
1727 Parameters
1725 ----------
1728 ----------
1726 prompt : str
1729 prompt : str
1727 The prompt to show when more input is needed.
1730 The prompt to show when more input is needed.
1728
1731
1729 html : bool, optional (default False)
1732 html : bool, optional (default False)
1730 If set, the prompt will be inserted as formatted HTML. Otherwise,
1733 If set, the prompt will be inserted as formatted HTML. Otherwise,
1731 the prompt will be treated as plain text, though ANSI color codes
1734 the prompt will be treated as plain text, though ANSI color codes
1732 will be handled.
1735 will be handled.
1733 """
1736 """
1734 if html:
1737 if html:
1735 self._continuation_prompt_html = prompt
1738 self._continuation_prompt_html = prompt
1736 else:
1739 else:
1737 self._continuation_prompt = prompt
1740 self._continuation_prompt = prompt
1738 self._continuation_prompt_html = None
1741 self._continuation_prompt_html = None
1739
1742
1740 def _set_cursor(self, cursor):
1743 def _set_cursor(self, cursor):
1741 """ Convenience method to set the current cursor.
1744 """ Convenience method to set the current cursor.
1742 """
1745 """
1743 self._control.setTextCursor(cursor)
1746 self._control.setTextCursor(cursor)
1744
1747
1745 def _set_top_cursor(self, cursor):
1748 def _set_top_cursor(self, cursor):
1746 """ Scrolls the viewport so that the specified cursor is at the top.
1749 """ Scrolls the viewport so that the specified cursor is at the top.
1747 """
1750 """
1748 scrollbar = self._control.verticalScrollBar()
1751 scrollbar = self._control.verticalScrollBar()
1749 scrollbar.setValue(scrollbar.maximum())
1752 scrollbar.setValue(scrollbar.maximum())
1750 original_cursor = self._control.textCursor()
1753 original_cursor = self._control.textCursor()
1751 self._control.setTextCursor(cursor)
1754 self._control.setTextCursor(cursor)
1752 self._control.ensureCursorVisible()
1755 self._control.ensureCursorVisible()
1753 self._control.setTextCursor(original_cursor)
1756 self._control.setTextCursor(original_cursor)
1754
1757
1755 def _show_prompt(self, prompt=None, html=False, newline=True):
1758 def _show_prompt(self, prompt=None, html=False, newline=True):
1756 """ Writes a new prompt at the end of the buffer.
1759 """ Writes a new prompt at the end of the buffer.
1757
1760
1758 Parameters
1761 Parameters
1759 ----------
1762 ----------
1760 prompt : str, optional
1763 prompt : str, optional
1761 The prompt to show. If not specified, the previous prompt is used.
1764 The prompt to show. If not specified, the previous prompt is used.
1762
1765
1763 html : bool, optional (default False)
1766 html : bool, optional (default False)
1764 Only relevant when a prompt is specified. If set, the prompt will
1767 Only relevant when a prompt is specified. If set, the prompt will
1765 be inserted as formatted HTML. Otherwise, the prompt will be treated
1768 be inserted as formatted HTML. Otherwise, the prompt will be treated
1766 as plain text, though ANSI color codes will be handled.
1769 as plain text, though ANSI color codes will be handled.
1767
1770
1768 newline : bool, optional (default True)
1771 newline : bool, optional (default True)
1769 If set, a new line will be written before showing the prompt if
1772 If set, a new line will be written before showing the prompt if
1770 there is not already a newline at the end of the buffer.
1773 there is not already a newline at the end of the buffer.
1771 """
1774 """
1772 # Save the current end position to support _append*(before_prompt=True).
1775 # Save the current end position to support _append*(before_prompt=True).
1773 cursor = self._get_end_cursor()
1776 cursor = self._get_end_cursor()
1774 self._append_before_prompt_pos = cursor.position()
1777 self._append_before_prompt_pos = cursor.position()
1775
1778
1776 # Insert a preliminary newline, if necessary.
1779 # Insert a preliminary newline, if necessary.
1777 if newline and cursor.position() > 0:
1780 if newline and cursor.position() > 0:
1778 cursor.movePosition(QtGui.QTextCursor.Left,
1781 cursor.movePosition(QtGui.QTextCursor.Left,
1779 QtGui.QTextCursor.KeepAnchor)
1782 QtGui.QTextCursor.KeepAnchor)
1780 if cursor.selection().toPlainText() != '\n':
1783 if cursor.selection().toPlainText() != '\n':
1781 self._append_plain_text('\n')
1784 self._append_plain_text('\n')
1782
1785
1783 # Write the prompt.
1786 # Write the prompt.
1784 self._append_plain_text(self._prompt_sep)
1787 self._append_plain_text(self._prompt_sep)
1785 if prompt is None:
1788 if prompt is None:
1786 if self._prompt_html is None:
1789 if self._prompt_html is None:
1787 self._append_plain_text(self._prompt)
1790 self._append_plain_text(self._prompt)
1788 else:
1791 else:
1789 self._append_html(self._prompt_html)
1792 self._append_html(self._prompt_html)
1790 else:
1793 else:
1791 if html:
1794 if html:
1792 self._prompt = self._append_html_fetching_plain_text(prompt)
1795 self._prompt = self._append_html_fetching_plain_text(prompt)
1793 self._prompt_html = prompt
1796 self._prompt_html = prompt
1794 else:
1797 else:
1795 self._append_plain_text(prompt)
1798 self._append_plain_text(prompt)
1796 self._prompt = prompt
1799 self._prompt = prompt
1797 self._prompt_html = None
1800 self._prompt_html = None
1798
1801
1799 self._prompt_pos = self._get_end_cursor().position()
1802 self._prompt_pos = self._get_end_cursor().position()
1800 self._prompt_started()
1803 self._prompt_started()
1801
1804
1802 #------ Signal handlers ----------------------------------------------------
1805 #------ Signal handlers ----------------------------------------------------
1803
1806
1804 def _adjust_scrollbars(self):
1807 def _adjust_scrollbars(self):
1805 """ Expands the vertical scrollbar beyond the range set by Qt.
1808 """ Expands the vertical scrollbar beyond the range set by Qt.
1806 """
1809 """
1807 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1810 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1808 # and qtextedit.cpp.
1811 # and qtextedit.cpp.
1809 document = self._control.document()
1812 document = self._control.document()
1810 scrollbar = self._control.verticalScrollBar()
1813 scrollbar = self._control.verticalScrollBar()
1811 viewport_height = self._control.viewport().height()
1814 viewport_height = self._control.viewport().height()
1812 if isinstance(self._control, QtGui.QPlainTextEdit):
1815 if isinstance(self._control, QtGui.QPlainTextEdit):
1813 maximum = max(0, document.lineCount() - 1)
1816 maximum = max(0, document.lineCount() - 1)
1814 step = viewport_height / self._control.fontMetrics().lineSpacing()
1817 step = viewport_height / self._control.fontMetrics().lineSpacing()
1815 else:
1818 else:
1816 # QTextEdit does not do line-based layout and blocks will not in
1819 # QTextEdit does not do line-based layout and blocks will not in
1817 # general have the same height. Therefore it does not make sense to
1820 # general have the same height. Therefore it does not make sense to
1818 # attempt to scroll in line height increments.
1821 # attempt to scroll in line height increments.
1819 maximum = document.size().height()
1822 maximum = document.size().height()
1820 step = viewport_height
1823 step = viewport_height
1821 diff = maximum - scrollbar.maximum()
1824 diff = maximum - scrollbar.maximum()
1822 scrollbar.setRange(0, maximum)
1825 scrollbar.setRange(0, maximum)
1823 scrollbar.setPageStep(step)
1826 scrollbar.setPageStep(step)
1824
1827
1825 # Compensate for undesirable scrolling that occurs automatically due to
1828 # Compensate for undesirable scrolling that occurs automatically due to
1826 # maximumBlockCount() text truncation.
1829 # maximumBlockCount() text truncation.
1827 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1830 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1828 scrollbar.setValue(scrollbar.value() + diff)
1831 scrollbar.setValue(scrollbar.value() + diff)
1829
1832
1830 def _cursor_position_changed(self):
1833 def _cursor_position_changed(self):
1831 """ Clears the temporary buffer based on the cursor position.
1834 """ Clears the temporary buffer based on the cursor position.
1832 """
1835 """
1833 if self._text_completing_pos:
1836 if self._text_completing_pos:
1834 document = self._control.document()
1837 document = self._control.document()
1835 if self._text_completing_pos < document.characterCount():
1838 if self._text_completing_pos < document.characterCount():
1836 cursor = self._control.textCursor()
1839 cursor = self._control.textCursor()
1837 pos = cursor.position()
1840 pos = cursor.position()
1838 text_cursor = self._control.textCursor()
1841 text_cursor = self._control.textCursor()
1839 text_cursor.setPosition(self._text_completing_pos)
1842 text_cursor.setPosition(self._text_completing_pos)
1840 if pos < self._text_completing_pos or \
1843 if pos < self._text_completing_pos or \
1841 cursor.blockNumber() > text_cursor.blockNumber():
1844 cursor.blockNumber() > text_cursor.blockNumber():
1842 self._clear_temporary_buffer()
1845 self._clear_temporary_buffer()
1843 self._text_completing_pos = 0
1846 self._text_completing_pos = 0
1844 else:
1847 else:
1845 self._clear_temporary_buffer()
1848 self._clear_temporary_buffer()
1846 self._text_completing_pos = 0
1849 self._text_completing_pos = 0
1847
1850
1848 def _custom_context_menu_requested(self, pos):
1851 def _custom_context_menu_requested(self, pos):
1849 """ Shows a context menu at the given QPoint (in widget coordinates).
1852 """ Shows a context menu at the given QPoint (in widget coordinates).
1850 """
1853 """
1851 menu = self._context_menu_make(pos)
1854 menu = self._context_menu_make(pos)
1852 menu.exec_(self._control.mapToGlobal(pos))
1855 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,559 +1,561 b''
1 """ A FrontendWidget that emulates the interface of the console IPython and
1 """ A FrontendWidget that emulates the interface of the console IPython and
2 supports the additional functionality provided by the IPython kernel.
2 supports the additional functionality provided by the IPython kernel.
3 """
3 """
4
4
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Imports
6 # Imports
7 #-----------------------------------------------------------------------------
7 #-----------------------------------------------------------------------------
8
8
9 # Standard library imports
9 # Standard library imports
10 from collections import namedtuple
10 from collections import namedtuple
11 import os.path
11 import os.path
12 import re
12 import re
13 from subprocess import Popen
13 from subprocess import Popen
14 import sys
14 import sys
15 import time
15 import time
16 from textwrap import dedent
16 from textwrap import dedent
17
17
18 # System library imports
18 # System library imports
19 from IPython.external.qt import QtCore, QtGui
19 from IPython.external.qt import QtCore, QtGui
20
20
21 # Local imports
21 # Local imports
22 from IPython.core.inputsplitter import IPythonInputSplitter, \
22 from IPython.core.inputsplitter import IPythonInputSplitter, \
23 transform_ipy_prompt
23 transform_ipy_prompt
24 from IPython.utils.traitlets import Bool, Unicode
24 from IPython.utils.traitlets import Bool, Unicode
25 from frontend_widget import FrontendWidget
25 from frontend_widget import FrontendWidget
26 import styles
26 import styles
27
27
28 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
29 # Constants
29 # Constants
30 #-----------------------------------------------------------------------------
30 #-----------------------------------------------------------------------------
31
31
32 # Default strings to build and display input and output prompts (and separators
32 # Default strings to build and display input and output prompts (and separators
33 # in between)
33 # in between)
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
34 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
35 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
36 default_input_sep = '\n'
36 default_input_sep = '\n'
37 default_output_sep = ''
37 default_output_sep = ''
38 default_output_sep2 = ''
38 default_output_sep2 = ''
39
39
40 # Base path for most payload sources.
40 # Base path for most payload sources.
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
41 zmq_shell_source = 'IPython.zmq.zmqshell.ZMQInteractiveShell'
42
42
43 if sys.platform.startswith('win'):
43 if sys.platform.startswith('win'):
44 default_editor = 'notepad'
44 default_editor = 'notepad'
45 else:
45 else:
46 default_editor = ''
46 default_editor = ''
47
47
48 #-----------------------------------------------------------------------------
48 #-----------------------------------------------------------------------------
49 # IPythonWidget class
49 # IPythonWidget class
50 #-----------------------------------------------------------------------------
50 #-----------------------------------------------------------------------------
51
51
52 class IPythonWidget(FrontendWidget):
52 class IPythonWidget(FrontendWidget):
53 """ A FrontendWidget for an IPython kernel.
53 """ A FrontendWidget for an IPython kernel.
54 """
54 """
55
55
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
56 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
57 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
58 # settings.
58 # settings.
59 custom_edit = Bool(False)
59 custom_edit = Bool(False)
60 custom_edit_requested = QtCore.Signal(object, object)
60 custom_edit_requested = QtCore.Signal(object, object)
61
61
62 editor = Unicode(default_editor, config=True,
62 editor = Unicode(default_editor, config=True,
63 help="""
63 help="""
64 A command for invoking a system text editor. If the string contains a
64 A command for invoking a system text editor. If the string contains a
65 {filename} format specifier, it will be used. Otherwise, the filename
65 {filename} format specifier, it will be used. Otherwise, the filename
66 will be appended to the end the command.
66 will be appended to the end the command.
67 """)
67 """)
68
68
69 editor_line = Unicode(config=True,
69 editor_line = Unicode(config=True,
70 help="""
70 help="""
71 The editor command to use when a specific line number is requested. The
71 The editor command to use when a specific line number is requested. The
72 string should contain two format specifiers: {line} and {filename}. If
72 string should contain two format specifiers: {line} and {filename}. If
73 this parameter is not specified, the line number option to the %edit
73 this parameter is not specified, the line number option to the %edit
74 magic will be ignored.
74 magic will be ignored.
75 """)
75 """)
76
76
77 style_sheet = Unicode(config=True,
77 style_sheet = Unicode(config=True,
78 help="""
78 help="""
79 A CSS stylesheet. The stylesheet can contain classes for:
79 A CSS stylesheet. The stylesheet can contain classes for:
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
80 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
81 2. Pygments: .c, .k, .o, etc. (see PygmentsHighlighter)
82 3. IPython: .error, .in-prompt, .out-prompt, etc
82 3. IPython: .error, .in-prompt, .out-prompt, etc
83 """)
83 """)
84
84
85 syntax_style = Unicode(config=True,
85 syntax_style = Unicode(config=True,
86 help="""
86 help="""
87 If not empty, use this Pygments style for syntax highlighting.
87 If not empty, use this Pygments style for syntax highlighting.
88 Otherwise, the style sheet is queried for Pygments style
88 Otherwise, the style sheet is queried for Pygments style
89 information.
89 information.
90 """)
90 """)
91
91
92 # Prompts.
92 # Prompts.
93 in_prompt = Unicode(default_in_prompt, config=True)
93 in_prompt = Unicode(default_in_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
94 out_prompt = Unicode(default_out_prompt, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
95 input_sep = Unicode(default_input_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
96 output_sep = Unicode(default_output_sep, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
97 output_sep2 = Unicode(default_output_sep2, config=True)
98
98
99 # FrontendWidget protected class variables.
99 # FrontendWidget protected class variables.
100 _input_splitter_class = IPythonInputSplitter
100 _input_splitter_class = IPythonInputSplitter
101 _transform_prompt = staticmethod(transform_ipy_prompt)
101 _transform_prompt = staticmethod(transform_ipy_prompt)
102
102
103 # IPythonWidget protected class variables.
103 # IPythonWidget protected class variables.
104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
104 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
105 _payload_source_edit = zmq_shell_source + '.edit_magic'
105 _payload_source_edit = zmq_shell_source + '.edit_magic'
106 _payload_source_exit = zmq_shell_source + '.ask_exit'
106 _payload_source_exit = zmq_shell_source + '.ask_exit'
107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
107 _payload_source_next_input = zmq_shell_source + '.set_next_input'
108 _payload_source_page = 'IPython.zmq.page.page'
108 _payload_source_page = 'IPython.zmq.page.page'
109 _retrying_history_request = False
109 _retrying_history_request = False
110
110
111 #---------------------------------------------------------------------------
111 #---------------------------------------------------------------------------
112 # 'object' interface
112 # 'object' interface
113 #---------------------------------------------------------------------------
113 #---------------------------------------------------------------------------
114
114
115 def __init__(self, *args, **kw):
115 def __init__(self, *args, **kw):
116 super(IPythonWidget, self).__init__(*args, **kw)
116 super(IPythonWidget, self).__init__(*args, **kw)
117
117
118 # IPythonWidget protected variables.
118 # IPythonWidget protected variables.
119 self._payload_handlers = {
119 self._payload_handlers = {
120 self._payload_source_edit : self._handle_payload_edit,
120 self._payload_source_edit : self._handle_payload_edit,
121 self._payload_source_exit : self._handle_payload_exit,
121 self._payload_source_exit : self._handle_payload_exit,
122 self._payload_source_page : self._handle_payload_page,
122 self._payload_source_page : self._handle_payload_page,
123 self._payload_source_next_input : self._handle_payload_next_input }
123 self._payload_source_next_input : self._handle_payload_next_input }
124 self._previous_prompt_obj = None
124 self._previous_prompt_obj = None
125 self._keep_kernel_on_exit = None
125 self._keep_kernel_on_exit = None
126
126
127 # Initialize widget styling.
127 # Initialize widget styling.
128 if self.style_sheet:
128 if self.style_sheet:
129 self._style_sheet_changed()
129 self._style_sheet_changed()
130 self._syntax_style_changed()
130 self._syntax_style_changed()
131 else:
131 else:
132 self.set_default_style()
132 self.set_default_style()
133
133
134 #---------------------------------------------------------------------------
134 #---------------------------------------------------------------------------
135 # 'BaseFrontendMixin' abstract interface
135 # 'BaseFrontendMixin' abstract interface
136 #---------------------------------------------------------------------------
136 #---------------------------------------------------------------------------
137
137
138 def _handle_complete_reply(self, rep):
138 def _handle_complete_reply(self, rep):
139 """ Reimplemented to support IPython's improved completion machinery.
139 """ Reimplemented to support IPython's improved completion machinery.
140 """
140 """
141 self.log.debug("complete: %s", rep.get('content', ''))
141 self.log.debug("complete: %s", rep.get('content', ''))
142 cursor = self._get_cursor()
142 cursor = self._get_cursor()
143 info = self._request_info.get('complete')
143 info = self._request_info.get('complete')
144 if info and info.id == rep['parent_header']['msg_id'] and \
144 if info and info.id == rep['parent_header']['msg_id'] and \
145 info.pos == cursor.position():
145 info.pos == cursor.position():
146 matches = rep['content']['matches']
146 matches = rep['content']['matches']
147 text = rep['content']['matched_text']
147 text = rep['content']['matched_text']
148 offset = len(text)
148 offset = len(text)
149
149
150 # Clean up matches with period and path separators if the matched
150 # Clean up matches with period and path separators if the matched
151 # text has not been transformed. This is done by truncating all
151 # text has not been transformed. This is done by truncating all
152 # but the last component and then suitably decreasing the offset
152 # but the last component and then suitably decreasing the offset
153 # between the current cursor position and the start of completion.
153 # between the current cursor position and the start of completion.
154 if len(matches) > 1 and matches[0][:offset] == text:
154 if len(matches) > 1 and matches[0][:offset] == text:
155 parts = re.split(r'[./\\]', text)
155 parts = re.split(r'[./\\]', text)
156 sep_count = len(parts) - 1
156 sep_count = len(parts) - 1
157 if sep_count:
157 if sep_count:
158 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 chop_length = sum(map(len, parts[:sep_count])) + sep_count
159 matches = [ match[chop_length:] for match in matches ]
159 matches = [ match[chop_length:] for match in matches ]
160 offset -= chop_length
160 offset -= chop_length
161
161
162 # Move the cursor to the start of the match and complete.
162 # Move the cursor to the start of the match and complete.
163 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
164 self._complete_with_items(cursor, matches)
164 self._complete_with_items(cursor, matches)
165
165
166 def _handle_execute_reply(self, msg):
166 def _handle_execute_reply(self, msg):
167 """ Reimplemented to support prompt requests.
167 """ Reimplemented to support prompt requests.
168 """
168 """
169 msg_id = msg['parent_header'].get('msg_id')
169 msg_id = msg['parent_header'].get('msg_id')
170 info = self._request_info['execute'].get(msg_id)
170 info = self._request_info['execute'].get(msg_id)
171 if info and info.kind == 'prompt':
171 if info and info.kind == 'prompt':
172 number = msg['content']['execution_count'] + 1
172 number = msg['content']['execution_count'] + 1
173 self._show_interpreter_prompt(number)
173 self._show_interpreter_prompt(number)
174 self._request_info['execute'].pop(msg_id)
174 self._request_info['execute'].pop(msg_id)
175 else:
175 else:
176 super(IPythonWidget, self)._handle_execute_reply(msg)
176 super(IPythonWidget, self)._handle_execute_reply(msg)
177
177
178 def _handle_history_reply(self, msg):
178 def _handle_history_reply(self, msg):
179 """ Implemented to handle history tail replies, which are only supported
179 """ Implemented to handle history tail replies, which are only supported
180 by the IPython kernel.
180 by the IPython kernel.
181 """
181 """
182 content = msg['content']
182 content = msg['content']
183 if 'history' not in content:
183 if 'history' not in content:
184 self.log.error("History request failed: %r"%content)
184 self.log.error("History request failed: %r"%content)
185 if content.get('status', '') == 'aborted' and \
185 if content.get('status', '') == 'aborted' and \
186 not self._retrying_history_request:
186 not self._retrying_history_request:
187 # a *different* action caused this request to be aborted, so
187 # a *different* action caused this request to be aborted, so
188 # we should try again.
188 # we should try again.
189 self.log.error("Retrying aborted history request")
189 self.log.error("Retrying aborted history request")
190 # prevent multiple retries of aborted requests:
190 # prevent multiple retries of aborted requests:
191 self._retrying_history_request = True
191 self._retrying_history_request = True
192 # wait out the kernel's queue flush, which is currently timed at 0.1s
192 # wait out the kernel's queue flush, which is currently timed at 0.1s
193 time.sleep(0.25)
193 time.sleep(0.25)
194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
194 self.kernel_manager.shell_channel.history(hist_access_type='tail',n=1000)
195 else:
195 else:
196 self._retrying_history_request = False
196 self._retrying_history_request = False
197 return
197 return
198 # reset retry flag
198 # reset retry flag
199 self._retrying_history_request = False
199 self._retrying_history_request = False
200 history_items = content['history']
200 history_items = content['history']
201 self.log.debug("Received history reply with %i entries", len(history_items))
201 self.log.debug("Received history reply with %i entries", len(history_items))
202 items = []
202 items = []
203 last_cell = u""
203 last_cell = u""
204 for _, _, cell in history_items:
204 for _, _, cell in history_items:
205 cell = cell.rstrip()
205 cell = cell.rstrip()
206 if cell != last_cell:
206 if cell != last_cell:
207 items.append(cell)
207 items.append(cell)
208 last_cell = cell
208 last_cell = cell
209 self._set_history(items)
209 self._set_history(items)
210
210
211 def _handle_pyout(self, msg):
211 def _handle_pyout(self, msg):
212 """ Reimplemented for IPython-style "display hook".
212 """ Reimplemented for IPython-style "display hook".
213 """
213 """
214 self.log.debug("pyout: %s", msg.get('content', ''))
214 self.log.debug("pyout: %s", msg.get('content', ''))
215 if not self._hidden and self._is_from_this_session(msg):
215 if not self._hidden and self._is_from_this_session(msg):
216 content = msg['content']
216 content = msg['content']
217 prompt_number = content['execution_count']
217 prompt_number = content['execution_count']
218 data = content['data']
218 data = content['data']
219 if data.has_key('text/html'):
219 if data.has_key('text/html'):
220 self._append_plain_text(self.output_sep, True)
220 self._append_plain_text(self.output_sep, True)
221 self._append_html(self._make_out_prompt(prompt_number), True)
221 self._append_html(self._make_out_prompt(prompt_number), True)
222 html = data['text/html']
222 html = data['text/html']
223 self._append_plain_text('\n', True)
223 self._append_plain_text('\n', True)
224 self._append_html(html + self.output_sep2, True)
224 self._append_html(html + self.output_sep2, True)
225 elif data.has_key('text/plain'):
225 elif data.has_key('text/plain'):
226 self._append_plain_text(self.output_sep, True)
226 self._append_plain_text(self.output_sep, True)
227 self._append_html(self._make_out_prompt(prompt_number), True)
227 self._append_html(self._make_out_prompt(prompt_number), True)
228 text = data['text/plain']
228 text = data['text/plain']
229 # If the repr is multiline, make sure we start on a new line,
229 # If the repr is multiline, make sure we start on a new line,
230 # so that its lines are aligned.
230 # so that its lines are aligned.
231 if "\n" in text and not self.output_sep.endswith("\n"):
231 if "\n" in text and not self.output_sep.endswith("\n"):
232 self._append_plain_text('\n', True)
232 self._append_plain_text('\n', True)
233 self._append_plain_text(text + self.output_sep2, True)
233 self._append_plain_text(text + self.output_sep2, True)
234
234
235 def _handle_display_data(self, msg):
235 def _handle_display_data(self, msg):
236 """ The base handler for the ``display_data`` message.
236 """ The base handler for the ``display_data`` message.
237 """
237 """
238 self.log.debug("display: %s", msg.get('content', ''))
238 self.log.debug("display: %s", msg.get('content', ''))
239 # For now, we don't display data from other frontends, but we
239 # For now, we don't display data from other frontends, but we
240 # eventually will as this allows all frontends to monitor the display
240 # eventually will as this allows all frontends to monitor the display
241 # data. But we need to figure out how to handle this in the GUI.
241 # data. But we need to figure out how to handle this in the GUI.
242 if not self._hidden and self._is_from_this_session(msg):
242 if not self._hidden and self._is_from_this_session(msg):
243 source = msg['content']['source']
243 source = msg['content']['source']
244 data = msg['content']['data']
244 data = msg['content']['data']
245 metadata = msg['content']['metadata']
245 metadata = msg['content']['metadata']
246 # In the regular IPythonWidget, we simply print the plain text
246 # In the regular IPythonWidget, we simply print the plain text
247 # representation.
247 # representation.
248 if data.has_key('text/html'):
248 if data.has_key('text/html'):
249 html = data['text/html']
249 html = data['text/html']
250 self._append_html(html, True)
250 self._append_html(html, True)
251 elif data.has_key('text/plain'):
251 elif data.has_key('text/plain'):
252 text = data['text/plain']
252 text = data['text/plain']
253 self._append_plain_text(text, True)
253 self._append_plain_text(text, True)
254 # This newline seems to be needed for text and html output.
254 # This newline seems to be needed for text and html output.
255 self._append_plain_text(u'\n', True)
255 self._append_plain_text(u'\n', True)
256
256
257 def _started_channels(self):
257 def _started_channels(self):
258 """ Reimplemented to make a history request.
258 """ Reimplemented to make a history request.
259 """
259 """
260 super(IPythonWidget, self)._started_channels()
260 super(IPythonWidget, self)._started_channels()
261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
261 self.kernel_manager.shell_channel.history(hist_access_type='tail',
262 n=1000)
262 n=1000)
263 #---------------------------------------------------------------------------
263 #---------------------------------------------------------------------------
264 # 'ConsoleWidget' public interface
264 # 'ConsoleWidget' public interface
265 #---------------------------------------------------------------------------
265 #---------------------------------------------------------------------------
266
266
267 #---------------------------------------------------------------------------
267 #---------------------------------------------------------------------------
268 # 'FrontendWidget' public interface
268 # 'FrontendWidget' public interface
269 #---------------------------------------------------------------------------
269 #---------------------------------------------------------------------------
270
270
271 def execute_file(self, path, hidden=False):
271 def execute_file(self, path, hidden=False):
272 """ Reimplemented to use the 'run' magic.
272 """ Reimplemented to use the 'run' magic.
273 """
273 """
274 # Use forward slashes on Windows to avoid escaping each separator.
274 # Use forward slashes on Windows to avoid escaping each separator.
275 if sys.platform == 'win32':
275 if sys.platform == 'win32':
276 path = os.path.normpath(path).replace('\\', '/')
276 path = os.path.normpath(path).replace('\\', '/')
277
277
278 # Perhaps we should not be using %run directly, but while we
278 # Perhaps we should not be using %run directly, but while we
279 # are, it is necessary to quote or escape filenames containing spaces
279 # are, it is necessary to quote or escape filenames containing spaces
280 # or quotes.
280 # or quotes.
281
281
282 # In earlier code here, to minimize escaping, we sometimes quoted the
282 # In earlier code here, to minimize escaping, we sometimes quoted the
283 # filename with single quotes. But to do this, this code must be
283 # filename with single quotes. But to do this, this code must be
284 # platform-aware, because run uses shlex rather than python string
284 # platform-aware, because run uses shlex rather than python string
285 # parsing, so that:
285 # parsing, so that:
286 # * In Win: single quotes can be used in the filename without quoting,
286 # * In Win: single quotes can be used in the filename without quoting,
287 # and we cannot use single quotes to quote the filename.
287 # and we cannot use single quotes to quote the filename.
288 # * In *nix: we can escape double quotes in a double quoted filename,
288 # * In *nix: we can escape double quotes in a double quoted filename,
289 # but can't escape single quotes in a single quoted filename.
289 # but can't escape single quotes in a single quoted filename.
290
290
291 # So to keep this code non-platform-specific and simple, we now only
291 # So to keep this code non-platform-specific and simple, we now only
292 # use double quotes to quote filenames, and escape when needed:
292 # use double quotes to quote filenames, and escape when needed:
293 if ' ' in path or "'" in path or '"' in path:
293 if ' ' in path or "'" in path or '"' in path:
294 path = '"%s"' % path.replace('"', '\\"')
294 path = '"%s"' % path.replace('"', '\\"')
295 self.execute('%%run %s' % path, hidden=hidden)
295 self.execute('%%run %s' % path, hidden=hidden)
296
296
297 #---------------------------------------------------------------------------
297 #---------------------------------------------------------------------------
298 # 'FrontendWidget' protected interface
298 # 'FrontendWidget' protected interface
299 #---------------------------------------------------------------------------
299 #---------------------------------------------------------------------------
300
300
301 def _complete(self):
301 def _complete(self):
302 """ Reimplemented to support IPython's improved completion machinery.
302 """ Reimplemented to support IPython's improved completion machinery.
303 """
303 """
304 # We let the kernel split the input line, so we *always* send an empty
304 # We let the kernel split the input line, so we *always* send an empty
305 # text field. Readline-based frontends do get a real text field which
305 # text field. Readline-based frontends do get a real text field which
306 # they can use.
306 # they can use.
307 text = ''
307 text = ''
308
308
309 # Send the completion request to the kernel
309 # Send the completion request to the kernel
310 msg_id = self.kernel_manager.shell_channel.complete(
310 msg_id = self.kernel_manager.shell_channel.complete(
311 text, # text
311 text, # text
312 self._get_input_buffer_cursor_line(), # line
312 self._get_input_buffer_cursor_line(), # line
313 self._get_input_buffer_cursor_column(), # cursor_pos
313 self._get_input_buffer_cursor_column(), # cursor_pos
314 self.input_buffer) # block
314 self.input_buffer) # block
315 pos = self._get_cursor().position()
315 pos = self._get_cursor().position()
316 info = self._CompletionRequest(msg_id, pos)
316 info = self._CompletionRequest(msg_id, pos)
317 self._request_info['complete'] = info
317 self._request_info['complete'] = info
318
318
319 def _process_execute_error(self, msg):
319 def _process_execute_error(self, msg):
320 """ Reimplemented for IPython-style traceback formatting.
320 """ Reimplemented for IPython-style traceback formatting.
321 """
321 """
322 content = msg['content']
322 content = msg['content']
323 traceback = '\n'.join(content['traceback']) + '\n'
323 traceback = '\n'.join(content['traceback']) + '\n'
324 if False:
324 if False:
325 # FIXME: For now, tracebacks come as plain text, so we can't use
325 # FIXME: For now, tracebacks come as plain text, so we can't use
326 # the html renderer yet. Once we refactor ultratb to produce
326 # the html renderer yet. Once we refactor ultratb to produce
327 # properly styled tracebacks, this branch should be the default
327 # properly styled tracebacks, this branch should be the default
328 traceback = traceback.replace(' ', '&nbsp;')
328 traceback = traceback.replace(' ', '&nbsp;')
329 traceback = traceback.replace('\n', '<br/>')
329 traceback = traceback.replace('\n', '<br/>')
330
330
331 ename = content['ename']
331 ename = content['ename']
332 ename_styled = '<span class="error">%s</span>' % ename
332 ename_styled = '<span class="error">%s</span>' % ename
333 traceback = traceback.replace(ename, ename_styled)
333 traceback = traceback.replace(ename, ename_styled)
334
334
335 self._append_html(traceback)
335 self._append_html(traceback)
336 else:
336 else:
337 # This is the fallback for now, using plain text with ansi escapes
337 # This is the fallback for now, using plain text with ansi escapes
338 self._append_plain_text(traceback)
338 self._append_plain_text(traceback)
339
339
340 def _process_execute_payload(self, item):
340 def _process_execute_payload(self, item):
341 """ Reimplemented to dispatch payloads to handler methods.
341 """ Reimplemented to dispatch payloads to handler methods.
342 """
342 """
343 handler = self._payload_handlers.get(item['source'])
343 handler = self._payload_handlers.get(item['source'])
344 if handler is None:
344 if handler is None:
345 # We have no handler for this type of payload, simply ignore it
345 # We have no handler for this type of payload, simply ignore it
346 return False
346 return False
347 else:
347 else:
348 handler(item)
348 handler(item)
349 return True
349 return True
350
350
351 def _show_interpreter_prompt(self, number=None):
351 def _show_interpreter_prompt(self, number=None):
352 """ Reimplemented for IPython-style prompts.
352 """ Reimplemented for IPython-style prompts.
353 """
353 """
354 # If a number was not specified, make a prompt number request.
354 # If a number was not specified, make a prompt number request.
355 if number is None:
355 if number is None:
356 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
356 msg_id = self.kernel_manager.shell_channel.execute('', silent=True)
357 info = self._ExecutionRequest(msg_id, 'prompt')
357 info = self._ExecutionRequest(msg_id, 'prompt')
358 self._request_info['execute'][msg_id] = info
358 self._request_info['execute'][msg_id] = info
359 return
359 return
360
360
361 # Show a new prompt and save information about it so that it can be
361 # Show a new prompt and save information about it so that it can be
362 # updated later if the prompt number turns out to be wrong.
362 # updated later if the prompt number turns out to be wrong.
363 self._prompt_sep = self.input_sep
363 self._prompt_sep = self.input_sep
364 self._show_prompt(self._make_in_prompt(number), html=True)
364 self._show_prompt(self._make_in_prompt(number), html=True)
365 block = self._control.document().lastBlock()
365 block = self._control.document().lastBlock()
366 length = len(self._prompt)
366 length = len(self._prompt)
367 self._previous_prompt_obj = self._PromptBlock(block, length, number)
367 self._previous_prompt_obj = self._PromptBlock(block, length, number)
368
368
369 # Update continuation prompt to reflect (possibly) new prompt length.
369 # Update continuation prompt to reflect (possibly) new prompt length.
370 self._set_continuation_prompt(
370 self._set_continuation_prompt(
371 self._make_continuation_prompt(self._prompt), html=True)
371 self._make_continuation_prompt(self._prompt), html=True)
372
372
373 def _show_interpreter_prompt_for_reply(self, msg):
373 def _show_interpreter_prompt_for_reply(self, msg):
374 """ Reimplemented for IPython-style prompts.
374 """ Reimplemented for IPython-style prompts.
375 """
375 """
376 # Update the old prompt number if necessary.
376 # Update the old prompt number if necessary.
377 content = msg['content']
377 content = msg['content']
378 # abort replies do not have any keys:
378 # abort replies do not have any keys:
379 if content['status'] == 'aborted':
379 if content['status'] == 'aborted':
380 if self._previous_prompt_obj:
380 if self._previous_prompt_obj:
381 previous_prompt_number = self._previous_prompt_obj.number
381 previous_prompt_number = self._previous_prompt_obj.number
382 else:
382 else:
383 previous_prompt_number = 0
383 previous_prompt_number = 0
384 else:
384 else:
385 previous_prompt_number = content['execution_count']
385 previous_prompt_number = content['execution_count']
386 if self._previous_prompt_obj and \
386 if self._previous_prompt_obj and \
387 self._previous_prompt_obj.number != previous_prompt_number:
387 self._previous_prompt_obj.number != previous_prompt_number:
388 block = self._previous_prompt_obj.block
388 block = self._previous_prompt_obj.block
389
389
390 # Make sure the prompt block has not been erased.
390 # Make sure the prompt block has not been erased.
391 if block.isValid() and block.text():
391 if block.isValid() and block.text():
392
392
393 # Remove the old prompt and insert a new prompt.
393 # Remove the old prompt and insert a new prompt.
394 cursor = QtGui.QTextCursor(block)
394 cursor = QtGui.QTextCursor(block)
395 cursor.movePosition(QtGui.QTextCursor.Right,
395 cursor.movePosition(QtGui.QTextCursor.Right,
396 QtGui.QTextCursor.KeepAnchor,
396 QtGui.QTextCursor.KeepAnchor,
397 self._previous_prompt_obj.length)
397 self._previous_prompt_obj.length)
398 prompt = self._make_in_prompt(previous_prompt_number)
398 prompt = self._make_in_prompt(previous_prompt_number)
399 self._prompt = self._insert_html_fetching_plain_text(
399 self._prompt = self._insert_html_fetching_plain_text(
400 cursor, prompt)
400 cursor, prompt)
401
401
402 # When the HTML is inserted, Qt blows away the syntax
402 # When the HTML is inserted, Qt blows away the syntax
403 # highlighting for the line, so we need to rehighlight it.
403 # highlighting for the line, so we need to rehighlight it.
404 self._highlighter.rehighlightBlock(cursor.block())
404 self._highlighter.rehighlightBlock(cursor.block())
405
405
406 self._previous_prompt_obj = None
406 self._previous_prompt_obj = None
407
407
408 # Show a new prompt with the kernel's estimated prompt number.
408 # Show a new prompt with the kernel's estimated prompt number.
409 self._show_interpreter_prompt(previous_prompt_number + 1)
409 self._show_interpreter_prompt(previous_prompt_number + 1)
410
410
411 #---------------------------------------------------------------------------
411 #---------------------------------------------------------------------------
412 # 'IPythonWidget' interface
412 # 'IPythonWidget' interface
413 #---------------------------------------------------------------------------
413 #---------------------------------------------------------------------------
414
414
415 def set_default_style(self, colors='lightbg'):
415 def set_default_style(self, colors='lightbg'):
416 """ Sets the widget style to the class defaults.
416 """ Sets the widget style to the class defaults.
417
417
418 Parameters:
418 Parameters:
419 -----------
419 -----------
420 colors : str, optional (default lightbg)
420 colors : str, optional (default lightbg)
421 Whether to use the default IPython light background or dark
421 Whether to use the default IPython light background or dark
422 background or B&W style.
422 background or B&W style.
423 """
423 """
424 colors = colors.lower()
424 colors = colors.lower()
425 if colors=='lightbg':
425 if colors=='lightbg':
426 self.style_sheet = styles.default_light_style_sheet
426 self.style_sheet = styles.default_light_style_sheet
427 self.syntax_style = styles.default_light_syntax_style
427 self.syntax_style = styles.default_light_syntax_style
428 elif colors=='linux':
428 elif colors=='linux':
429 self.style_sheet = styles.default_dark_style_sheet
429 self.style_sheet = styles.default_dark_style_sheet
430 self.syntax_style = styles.default_dark_syntax_style
430 self.syntax_style = styles.default_dark_syntax_style
431 elif colors=='nocolor':
431 elif colors=='nocolor':
432 self.style_sheet = styles.default_bw_style_sheet
432 self.style_sheet = styles.default_bw_style_sheet
433 self.syntax_style = styles.default_bw_syntax_style
433 self.syntax_style = styles.default_bw_syntax_style
434 else:
434 else:
435 raise KeyError("No such color scheme: %s"%colors)
435 raise KeyError("No such color scheme: %s"%colors)
436
436
437 #---------------------------------------------------------------------------
437 #---------------------------------------------------------------------------
438 # 'IPythonWidget' protected interface
438 # 'IPythonWidget' protected interface
439 #---------------------------------------------------------------------------
439 #---------------------------------------------------------------------------
440
440
441 def _edit(self, filename, line=None):
441 def _edit(self, filename, line=None):
442 """ Opens a Python script for editing.
442 """ Opens a Python script for editing.
443
443
444 Parameters:
444 Parameters:
445 -----------
445 -----------
446 filename : str
446 filename : str
447 A path to a local system file.
447 A path to a local system file.
448
448
449 line : int, optional
449 line : int, optional
450 A line of interest in the file.
450 A line of interest in the file.
451 """
451 """
452 if self.custom_edit:
452 if self.custom_edit:
453 self.custom_edit_requested.emit(filename, line)
453 self.custom_edit_requested.emit(filename, line)
454 elif not self.editor:
454 elif not self.editor:
455 self._append_plain_text('No default editor available.\n'
455 self._append_plain_text('No default editor available.\n'
456 'Specify a GUI text editor in the `IPythonWidget.editor` '
456 'Specify a GUI text editor in the `IPythonWidget.editor` '
457 'configurable to enable the %edit magic')
457 'configurable to enable the %edit magic')
458 else:
458 else:
459 try:
459 try:
460 filename = '"%s"' % filename
460 filename = '"%s"' % filename
461 if line and self.editor_line:
461 if line and self.editor_line:
462 command = self.editor_line.format(filename=filename,
462 command = self.editor_line.format(filename=filename,
463 line=line)
463 line=line)
464 else:
464 else:
465 try:
465 try:
466 command = self.editor.format()
466 command = self.editor.format()
467 except KeyError:
467 except KeyError:
468 command = self.editor.format(filename=filename)
468 command = self.editor.format(filename=filename)
469 else:
469 else:
470 command += ' ' + filename
470 command += ' ' + filename
471 except KeyError:
471 except KeyError:
472 self._append_plain_text('Invalid editor command.\n')
472 self._append_plain_text('Invalid editor command.\n')
473 else:
473 else:
474 try:
474 try:
475 Popen(command, shell=True)
475 Popen(command, shell=True)
476 except OSError:
476 except OSError:
477 msg = 'Opening editor with command "%s" failed.\n'
477 msg = 'Opening editor with command "%s" failed.\n'
478 self._append_plain_text(msg % command)
478 self._append_plain_text(msg % command)
479
479
480 def _make_in_prompt(self, number):
480 def _make_in_prompt(self, number):
481 """ Given a prompt number, returns an HTML In prompt.
481 """ Given a prompt number, returns an HTML In prompt.
482 """
482 """
483 try:
483 try:
484 body = self.in_prompt % number
484 body = self.in_prompt % number
485 except TypeError:
485 except TypeError:
486 # allow in_prompt to leave out number, e.g. '>>> '
486 # allow in_prompt to leave out number, e.g. '>>> '
487 body = self.in_prompt
487 body = self.in_prompt
488 return '<span class="in-prompt">%s</span>' % body
488 return '<span class="in-prompt">%s</span>' % body
489
489
490 def _make_continuation_prompt(self, prompt):
490 def _make_continuation_prompt(self, prompt):
491 """ Given a plain text version of an In prompt, returns an HTML
491 """ Given a plain text version of an In prompt, returns an HTML
492 continuation prompt.
492 continuation prompt.
493 """
493 """
494 end_chars = '...: '
494 end_chars = '...: '
495 space_count = len(prompt.lstrip('\n')) - len(end_chars)
495 space_count = len(prompt.lstrip('\n')) - len(end_chars)
496 body = '&nbsp;' * space_count + end_chars
496 body = '&nbsp;' * space_count + end_chars
497 return '<span class="in-prompt">%s</span>' % body
497 return '<span class="in-prompt">%s</span>' % body
498
498
499 def _make_out_prompt(self, number):
499 def _make_out_prompt(self, number):
500 """ Given a prompt number, returns an HTML Out prompt.
500 """ Given a prompt number, returns an HTML Out prompt.
501 """
501 """
502 body = self.out_prompt % number
502 body = self.out_prompt % number
503 return '<span class="out-prompt">%s</span>' % body
503 return '<span class="out-prompt">%s</span>' % body
504
504
505 #------ Payload handlers --------------------------------------------------
505 #------ Payload handlers --------------------------------------------------
506
506
507 # Payload handlers with a generic interface: each takes the opaque payload
507 # Payload handlers with a generic interface: each takes the opaque payload
508 # dict, unpacks it and calls the underlying functions with the necessary
508 # dict, unpacks it and calls the underlying functions with the necessary
509 # arguments.
509 # arguments.
510
510
511 def _handle_payload_edit(self, item):
511 def _handle_payload_edit(self, item):
512 self._edit(item['filename'], item['line_number'])
512 self._edit(item['filename'], item['line_number'])
513
513
514 def _handle_payload_exit(self, item):
514 def _handle_payload_exit(self, item):
515 self._keep_kernel_on_exit = item['keepkernel']
515 self._keep_kernel_on_exit = item['keepkernel']
516 self.exit_requested.emit(self)
516 self.exit_requested.emit(self)
517
517
518 def _handle_payload_next_input(self, item):
518 def _handle_payload_next_input(self, item):
519 self.input_buffer = dedent(item['text'].rstrip())
519 self.input_buffer = dedent(item['text'].rstrip())
520
520
521 def _handle_payload_page(self, item):
521 def _handle_payload_page(self, item):
522 # Since the plain text widget supports only a very small subset of HTML
522 # Since the plain text widget supports only a very small subset of HTML
523 # and we have no control over the HTML source, we only page HTML
523 # and we have no control over the HTML source, we only page HTML
524 # payloads in the rich text widget.
524 # payloads in the rich text widget.
525 if item['html'] and self.kind == 'rich':
525 if item['html'] and self.kind == 'rich':
526 self._page(item['html'], html=True)
526 self._page(item['html'], html=True)
527 else:
527 else:
528 self._page(item['text'], html=False)
528 self._page(item['text'], html=False)
529
529
530 #------ Trait change handlers --------------------------------------------
530 #------ Trait change handlers --------------------------------------------
531
531
532 def _style_sheet_changed(self):
532 def _style_sheet_changed(self):
533 """ Set the style sheets of the underlying widgets.
533 """ Set the style sheets of the underlying widgets.
534 """
534 """
535 self.setStyleSheet(self.style_sheet)
535 self.setStyleSheet(self.style_sheet)
536 self._control.document().setDefaultStyleSheet(self.style_sheet)
536 if self._control is not None:
537 if self._page_control:
537 self._control.document().setDefaultStyleSheet(self.style_sheet)
538 bg_color = self._control.palette().window().color()
539 self._ansi_processor.set_background_color(bg_color)
540
541 if self._page_control is not None:
538 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
542 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
539
543
540 bg_color = self._control.palette().window().color()
541 self._ansi_processor.set_background_color(bg_color)
542
544
543
545
544 def _syntax_style_changed(self):
546 def _syntax_style_changed(self):
545 """ Set the style for the syntax highlighter.
547 """ Set the style for the syntax highlighter.
546 """
548 """
547 if self._highlighter is None:
549 if self._highlighter is None:
548 # ignore premature calls
550 # ignore premature calls
549 return
551 return
550 if self.syntax_style:
552 if self.syntax_style:
551 self._highlighter.set_style(self.syntax_style)
553 self._highlighter.set_style(self.syntax_style)
552 else:
554 else:
553 self._highlighter.set_style_sheet(self.style_sheet)
555 self._highlighter.set_style_sheet(self.style_sheet)
554
556
555 #------ Trait default initializers -----------------------------------------
557 #------ Trait default initializers -----------------------------------------
556
558
557 def _banner_default(self):
559 def _banner_default(self):
558 from IPython.core.usage import default_gui_banner
560 from IPython.core.usage import default_gui_banner
559 return default_gui_banner
561 return default_gui_banner
@@ -1,224 +1,224 b''
1 # System library imports.
1 # System library imports.
2 from IPython.external.qt import QtGui
2 from IPython.external.qt import QtGui
3 from pygments.formatters.html import HtmlFormatter
3 from pygments.formatters.html import HtmlFormatter
4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
4 from pygments.lexer import RegexLexer, _TokenType, Text, Error
5 from pygments.lexers import PythonLexer
5 from pygments.lexers import PythonLexer
6 from pygments.styles import get_style_by_name
6 from pygments.styles import get_style_by_name
7
7
8
8
9 def get_tokens_unprocessed(self, text, stack=('root',)):
9 def get_tokens_unprocessed(self, text, stack=('root',)):
10 """ Split ``text`` into (tokentype, text) pairs.
10 """ Split ``text`` into (tokentype, text) pairs.
11
11
12 Monkeypatched to store the final stack on the object itself.
12 Monkeypatched to store the final stack on the object itself.
13 """
13 """
14 pos = 0
14 pos = 0
15 tokendefs = self._tokens
15 tokendefs = self._tokens
16 if hasattr(self, '_saved_state_stack'):
16 if hasattr(self, '_saved_state_stack'):
17 statestack = list(self._saved_state_stack)
17 statestack = list(self._saved_state_stack)
18 else:
18 else:
19 statestack = list(stack)
19 statestack = list(stack)
20 statetokens = tokendefs[statestack[-1]]
20 statetokens = tokendefs[statestack[-1]]
21 while 1:
21 while 1:
22 for rexmatch, action, new_state in statetokens:
22 for rexmatch, action, new_state in statetokens:
23 m = rexmatch(text, pos)
23 m = rexmatch(text, pos)
24 if m:
24 if m:
25 if type(action) is _TokenType:
25 if type(action) is _TokenType:
26 yield pos, action, m.group()
26 yield pos, action, m.group()
27 else:
27 else:
28 for item in action(self, m):
28 for item in action(self, m):
29 yield item
29 yield item
30 pos = m.end()
30 pos = m.end()
31 if new_state is not None:
31 if new_state is not None:
32 # state transition
32 # state transition
33 if isinstance(new_state, tuple):
33 if isinstance(new_state, tuple):
34 for state in new_state:
34 for state in new_state:
35 if state == '#pop':
35 if state == '#pop':
36 statestack.pop()
36 statestack.pop()
37 elif state == '#push':
37 elif state == '#push':
38 statestack.append(statestack[-1])
38 statestack.append(statestack[-1])
39 else:
39 else:
40 statestack.append(state)
40 statestack.append(state)
41 elif isinstance(new_state, int):
41 elif isinstance(new_state, int):
42 # pop
42 # pop
43 del statestack[new_state:]
43 del statestack[new_state:]
44 elif new_state == '#push':
44 elif new_state == '#push':
45 statestack.append(statestack[-1])
45 statestack.append(statestack[-1])
46 else:
46 else:
47 assert False, "wrong state def: %r" % new_state
47 assert False, "wrong state def: %r" % new_state
48 statetokens = tokendefs[statestack[-1]]
48 statetokens = tokendefs[statestack[-1]]
49 break
49 break
50 else:
50 else:
51 try:
51 try:
52 if text[pos] == '\n':
52 if text[pos] == '\n':
53 # at EOL, reset state to "root"
53 # at EOL, reset state to "root"
54 pos += 1
54 pos += 1
55 statestack = ['root']
55 statestack = ['root']
56 statetokens = tokendefs['root']
56 statetokens = tokendefs['root']
57 yield pos, Text, u'\n'
57 yield pos, Text, u'\n'
58 continue
58 continue
59 yield pos, Error, text[pos]
59 yield pos, Error, text[pos]
60 pos += 1
60 pos += 1
61 except IndexError:
61 except IndexError:
62 break
62 break
63 self._saved_state_stack = list(statestack)
63 self._saved_state_stack = list(statestack)
64
64
65 # Monkeypatch!
65 # Monkeypatch!
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
66 RegexLexer.get_tokens_unprocessed = get_tokens_unprocessed
67
67
68
68
69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
69 class PygmentsBlockUserData(QtGui.QTextBlockUserData):
70 """ Storage for the user data associated with each line.
70 """ Storage for the user data associated with each line.
71 """
71 """
72
72
73 syntax_stack = ('root',)
73 syntax_stack = ('root',)
74
74
75 def __init__(self, **kwds):
75 def __init__(self, **kwds):
76 for key, value in kwds.iteritems():
76 for key, value in kwds.iteritems():
77 setattr(self, key, value)
77 setattr(self, key, value)
78 QtGui.QTextBlockUserData.__init__(self)
78 QtGui.QTextBlockUserData.__init__(self)
79
79
80 def __repr__(self):
80 def __repr__(self):
81 attrs = ['syntax_stack']
81 attrs = ['syntax_stack']
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
82 kwds = ', '.join([ '%s=%r' % (attr, getattr(self, attr))
83 for attr in attrs ])
83 for attr in attrs ])
84 return 'PygmentsBlockUserData(%s)' % kwds
84 return 'PygmentsBlockUserData(%s)' % kwds
85
85
86
86
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
87 class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
88 """ Syntax highlighter that uses Pygments for parsing. """
88 """ Syntax highlighter that uses Pygments for parsing. """
89
89
90 #---------------------------------------------------------------------------
90 #---------------------------------------------------------------------------
91 # 'QSyntaxHighlighter' interface
91 # 'QSyntaxHighlighter' interface
92 #---------------------------------------------------------------------------
92 #---------------------------------------------------------------------------
93
93
94 def __init__(self, parent, lexer=None):
94 def __init__(self, parent, lexer=None):
95 super(PygmentsHighlighter, self).__init__(parent)
95 super(PygmentsHighlighter, self).__init__(parent)
96
96
97 self._document = QtGui.QTextDocument()
97 self._document = QtGui.QTextDocument()
98 self._formatter = HtmlFormatter(nowrap=True)
98 self._formatter = HtmlFormatter(nowrap=True)
99 self._lexer = lexer if lexer else PythonLexer()
99 self._lexer = lexer if lexer else PythonLexer()
100 self.set_style('default')
100 self.set_style('default')
101
101
102 def highlightBlock(self, string):
102 def highlightBlock(self, string):
103 """ Highlight a block of text.
103 """ Highlight a block of text.
104 """
104 """
105 prev_data = self.currentBlock().previous().userData()
105 prev_data = self.currentBlock().previous().userData()
106 if prev_data is not None:
106 if prev_data is not None:
107 self._lexer._saved_state_stack = prev_data.syntax_stack
107 self._lexer._saved_state_stack = prev_data.syntax_stack
108 elif hasattr(self._lexer, '_saved_state_stack'):
108 elif hasattr(self._lexer, '_saved_state_stack'):
109 del self._lexer._saved_state_stack
109 del self._lexer._saved_state_stack
110
110
111 # Lex the text using Pygments
111 # Lex the text using Pygments
112 index = 0
112 index = 0
113 for token, text in self._lexer.get_tokens(string):
113 for token, text in self._lexer.get_tokens(string):
114 length = len(text)
114 length = len(text)
115 self.setFormat(index, length, self._get_format(token))
115 self.setFormat(index, length, self._get_format(token))
116 index += length
116 index += length
117
117
118 if hasattr(self._lexer, '_saved_state_stack'):
118 if hasattr(self._lexer, '_saved_state_stack'):
119 data = PygmentsBlockUserData(
119 data = PygmentsBlockUserData(
120 syntax_stack=self._lexer._saved_state_stack)
120 syntax_stack=self._lexer._saved_state_stack)
121 self.currentBlock().setUserData(data)
121 self.currentBlock().setUserData(data)
122 # Clean up for the next go-round.
122 # Clean up for the next go-round.
123 del self._lexer._saved_state_stack
123 del self._lexer._saved_state_stack
124
124
125 #---------------------------------------------------------------------------
125 #---------------------------------------------------------------------------
126 # 'PygmentsHighlighter' interface
126 # 'PygmentsHighlighter' interface
127 #---------------------------------------------------------------------------
127 #---------------------------------------------------------------------------
128
128
129 def set_style(self, style):
129 def set_style(self, style):
130 """ Sets the style to the specified Pygments style.
130 """ Sets the style to the specified Pygments style.
131 """
131 """
132 if isinstance(style, basestring):
132 if isinstance(style, basestring):
133 style = get_style_by_name(style)
133 style = get_style_by_name(style)
134 self._style = style
134 self._style = style
135 self._clear_caches()
135 self._clear_caches()
136
136
137 def set_style_sheet(self, stylesheet):
137 def set_style_sheet(self, stylesheet):
138 """ Sets a CSS stylesheet. The classes in the stylesheet should
138 """ Sets a CSS stylesheet. The classes in the stylesheet should
139 correspond to those generated by:
139 correspond to those generated by:
140
140
141 pygmentize -S <style> -f html
141 pygmentize -S <style> -f html
142
142
143 Note that 'set_style' and 'set_style_sheet' completely override each
143 Note that 'set_style' and 'set_style_sheet' completely override each
144 other, i.e. they cannot be used in conjunction.
144 other, i.e. they cannot be used in conjunction.
145 """
145 """
146 self._document.setDefaultStyleSheet(stylesheet)
146 self._document.setDefaultStyleSheet(stylesheet)
147 self._style = None
147 self._style = None
148 self._clear_caches()
148 self._clear_caches()
149
149
150 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
151 # Protected interface
151 # Protected interface
152 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
153
153
154 def _clear_caches(self):
154 def _clear_caches(self):
155 """ Clear caches for brushes and formats.
155 """ Clear caches for brushes and formats.
156 """
156 """
157 self._brushes = {}
157 self._brushes = {}
158 self._formats = {}
158 self._formats = {}
159
159
160 def _get_format(self, token):
160 def _get_format(self, token):
161 """ Returns a QTextCharFormat for token or None.
161 """ Returns a QTextCharFormat for token or None.
162 """
162 """
163 if token in self._formats:
163 if token in self._formats:
164 return self._formats[token]
164 return self._formats[token]
165
165
166 if self._style is None:
166 if self._style is None:
167 result = self._get_format_from_document(token, self._document)
167 result = self._get_format_from_document(token, self._document)
168 else:
168 else:
169 result = self._get_format_from_style(token, self._style)
169 result = self._get_format_from_style(token, self._style)
170
170
171 self._formats[token] = result
171 self._formats[token] = result
172 return result
172 return result
173
173
174 def _get_format_from_document(self, token, document):
174 def _get_format_from_document(self, token, document):
175 """ Returns a QTextCharFormat for token by
175 """ Returns a QTextCharFormat for token by
176 """
176 """
177 code, html = self._formatter._format_lines([(token, 'dummy')]).next()
177 code, html = self._formatter._format_lines([(token, u'dummy')]).next()
178 self._document.setHtml(html)
178 self._document.setHtml(html)
179 return QtGui.QTextCursor(self._document).charFormat()
179 return QtGui.QTextCursor(self._document).charFormat()
180
180
181 def _get_format_from_style(self, token, style):
181 def _get_format_from_style(self, token, style):
182 """ Returns a QTextCharFormat for token by reading a Pygments style.
182 """ Returns a QTextCharFormat for token by reading a Pygments style.
183 """
183 """
184 result = QtGui.QTextCharFormat()
184 result = QtGui.QTextCharFormat()
185 for key, value in style.style_for_token(token).items():
185 for key, value in style.style_for_token(token).items():
186 if value:
186 if value:
187 if key == 'color':
187 if key == 'color':
188 result.setForeground(self._get_brush(value))
188 result.setForeground(self._get_brush(value))
189 elif key == 'bgcolor':
189 elif key == 'bgcolor':
190 result.setBackground(self._get_brush(value))
190 result.setBackground(self._get_brush(value))
191 elif key == 'bold':
191 elif key == 'bold':
192 result.setFontWeight(QtGui.QFont.Bold)
192 result.setFontWeight(QtGui.QFont.Bold)
193 elif key == 'italic':
193 elif key == 'italic':
194 result.setFontItalic(True)
194 result.setFontItalic(True)
195 elif key == 'underline':
195 elif key == 'underline':
196 result.setUnderlineStyle(
196 result.setUnderlineStyle(
197 QtGui.QTextCharFormat.SingleUnderline)
197 QtGui.QTextCharFormat.SingleUnderline)
198 elif key == 'sans':
198 elif key == 'sans':
199 result.setFontStyleHint(QtGui.QFont.SansSerif)
199 result.setFontStyleHint(QtGui.QFont.SansSerif)
200 elif key == 'roman':
200 elif key == 'roman':
201 result.setFontStyleHint(QtGui.QFont.Times)
201 result.setFontStyleHint(QtGui.QFont.Times)
202 elif key == 'mono':
202 elif key == 'mono':
203 result.setFontStyleHint(QtGui.QFont.TypeWriter)
203 result.setFontStyleHint(QtGui.QFont.TypeWriter)
204 return result
204 return result
205
205
206 def _get_brush(self, color):
206 def _get_brush(self, color):
207 """ Returns a brush for the color.
207 """ Returns a brush for the color.
208 """
208 """
209 result = self._brushes.get(color)
209 result = self._brushes.get(color)
210 if result is None:
210 if result is None:
211 qcolor = self._get_color(color)
211 qcolor = self._get_color(color)
212 result = QtGui.QBrush(qcolor)
212 result = QtGui.QBrush(qcolor)
213 self._brushes[color] = result
213 self._brushes[color] = result
214 return result
214 return result
215
215
216 def _get_color(self, color):
216 def _get_color(self, color):
217 """ Returns a QColor built from a Pygments color string.
217 """ Returns a QColor built from a Pygments color string.
218 """
218 """
219 qcolor = QtGui.QColor()
219 qcolor = QtGui.QColor()
220 qcolor.setRgb(int(color[:2], base=16),
220 qcolor.setRgb(int(color[:2], base=16),
221 int(color[2:4], base=16),
221 int(color[2:4], base=16),
222 int(color[4:6], base=16))
222 int(color[4:6], base=16))
223 return qcolor
223 return qcolor
224
224
@@ -1,353 +1,356 b''
1 """ A minimal application using the Qt console-style IPython frontend.
1 """ A minimal application using the Qt console-style IPython frontend.
2
2
3 This is not a complete console app, as subprocess will not be able to receive
3 This is not a complete console app, as subprocess will not be able to receive
4 input, there is no real readline support, among other limitations.
4 input, there is no real readline support, among other limitations.
5
5
6 Authors:
6 Authors:
7
7
8 * Evan Patterson
8 * Evan Patterson
9 * Min RK
9 * Min RK
10 * Erik Tollerud
10 * Erik Tollerud
11 * Fernando Perez
11 * Fernando Perez
12 * Bussonnier Matthias
12 * Bussonnier Matthias
13 * Thomas Kluyver
13 * Thomas Kluyver
14 * Paul Ivanov
14 * Paul Ivanov
15
15
16 """
16 """
17
17
18 #-----------------------------------------------------------------------------
18 #-----------------------------------------------------------------------------
19 # Imports
19 # Imports
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21
21
22 # stdlib imports
22 # stdlib imports
23 import json
23 import json
24 import os
24 import os
25 import signal
25 import signal
26 import sys
26 import sys
27 import uuid
27 import uuid
28
28
29 # System library imports
29 # System library imports
30 from IPython.external.qt import QtCore, QtGui
30 from IPython.external.qt import QtCore, QtGui
31
31
32 # Local imports
32 # Local imports
33 from IPython.config.application import boolean_flag, catch_config_error
33 from IPython.config.application import boolean_flag, catch_config_error
34 from IPython.core.application import BaseIPythonApplication
34 from IPython.core.application import BaseIPythonApplication
35 from IPython.core.profiledir import ProfileDir
35 from IPython.core.profiledir import ProfileDir
36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
36 from IPython.lib.kernel import tunnel_to_kernel, find_connection_file
37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
37 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
38 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
39 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
40 from IPython.frontend.qt.console import styles
40 from IPython.frontend.qt.console import styles
41 from IPython.frontend.qt.console.mainwindow import MainWindow
41 from IPython.frontend.qt.console.mainwindow import MainWindow
42 from IPython.frontend.qt.kernelmanager import QtKernelManager
42 from IPython.frontend.qt.kernelmanager import QtKernelManager
43 from IPython.utils.path import filefind
43 from IPython.utils.path import filefind
44 from IPython.utils.py3compat import str_to_bytes
44 from IPython.utils.py3compat import str_to_bytes
45 from IPython.utils.traitlets import (
45 from IPython.utils.traitlets import (
46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
46 Dict, List, Unicode, Integer, CaselessStrEnum, CBool, Any
47 )
47 )
48 from IPython.zmq.ipkernel import IPKernelApp
48 from IPython.zmq.ipkernel import IPKernelApp
49 from IPython.zmq.session import Session, default_secure
49 from IPython.zmq.session import Session, default_secure
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
50 from IPython.zmq.zmqshell import ZMQInteractiveShell
51
51
52 from IPython.frontend.consoleapp import (
52 from IPython.frontend.consoleapp import (
53 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
53 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
54 )
54 )
55
55
56 #-----------------------------------------------------------------------------
56 #-----------------------------------------------------------------------------
57 # Network Constants
57 # Network Constants
58 #-----------------------------------------------------------------------------
58 #-----------------------------------------------------------------------------
59
59
60 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
60 from IPython.utils.localinterfaces import LOCALHOST, LOCAL_IPS
61
61
62 #-----------------------------------------------------------------------------
62 #-----------------------------------------------------------------------------
63 # Globals
63 # Globals
64 #-----------------------------------------------------------------------------
64 #-----------------------------------------------------------------------------
65
65
66 _examples = """
66 _examples = """
67 ipython qtconsole # start the qtconsole
67 ipython qtconsole # start the qtconsole
68 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
68 ipython qtconsole --pylab=inline # start with pylab in inline plotting mode
69 """
69 """
70
70
71 #-----------------------------------------------------------------------------
71 #-----------------------------------------------------------------------------
72 # Aliases and Flags
72 # Aliases and Flags
73 #-----------------------------------------------------------------------------
73 #-----------------------------------------------------------------------------
74
74
75 # start with copy of flags
75 # start with copy of flags
76 flags = dict(flags)
76 flags = dict(flags)
77 qt_flags = {
77 qt_flags = {
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
78 'pure' : ({'IPythonQtConsoleApp' : {'pure' : True}},
79 "Use a pure Python kernel instead of an IPython kernel."),
79 "Use a pure Python kernel instead of an IPython kernel."),
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
80 'plain' : ({'ConsoleWidget' : {'kind' : 'plain'}},
81 "Disable rich text support."),
81 "Disable rich text support."),
82 }
82 }
83 qt_flags.update(boolean_flag(
83 qt_flags.update(boolean_flag(
84 'gui-completion', 'ConsoleWidget.gui_completion',
84 'gui-completion', 'ConsoleWidget.gui_completion',
85 "use a GUI widget for tab completion",
85 "use a GUI widget for tab completion",
86 "use plaintext output for completion"
86 "use plaintext output for completion"
87 ))
87 ))
88 # and app_flags from the Console Mixin
88 # and app_flags from the Console Mixin
89 qt_flags.update(app_flags)
89 qt_flags.update(app_flags)
90 # add frontend flags to the full set
90 # add frontend flags to the full set
91 flags.update(qt_flags)
91 flags.update(qt_flags)
92
92
93 # start with copy of front&backend aliases list
93 # start with copy of front&backend aliases list
94 aliases = dict(aliases)
94 aliases = dict(aliases)
95 qt_aliases = dict(
95 qt_aliases = dict(
96
96
97 style = 'IPythonWidget.syntax_style',
97 style = 'IPythonWidget.syntax_style',
98 stylesheet = 'IPythonQtConsoleApp.stylesheet',
98 stylesheet = 'IPythonQtConsoleApp.stylesheet',
99 colors = 'ZMQInteractiveShell.colors',
99 colors = 'ZMQInteractiveShell.colors',
100
100
101 editor = 'IPythonWidget.editor',
101 editor = 'IPythonWidget.editor',
102 paging = 'ConsoleWidget.paging',
102 paging = 'ConsoleWidget.paging',
103 )
103 )
104 # and app_aliases from the Console Mixin
104 # and app_aliases from the Console Mixin
105 qt_aliases.update(app_aliases)
105 qt_aliases.update(app_aliases)
106 # add frontend aliases to the full set
106 # add frontend aliases to the full set
107 aliases.update(qt_aliases)
107 aliases.update(qt_aliases)
108
108
109 # get flags&aliases into sets, and remove a couple that
109 # get flags&aliases into sets, and remove a couple that
110 # shouldn't be scrubbed from backend flags:
110 # shouldn't be scrubbed from backend flags:
111 qt_aliases = set(qt_aliases.keys())
111 qt_aliases = set(qt_aliases.keys())
112 qt_aliases.remove('colors')
112 qt_aliases.remove('colors')
113 qt_flags = set(qt_flags.keys())
113 qt_flags = set(qt_flags.keys())
114
114
115 #-----------------------------------------------------------------------------
115 #-----------------------------------------------------------------------------
116 # Classes
116 # Classes
117 #-----------------------------------------------------------------------------
117 #-----------------------------------------------------------------------------
118
118
119 #-----------------------------------------------------------------------------
119 #-----------------------------------------------------------------------------
120 # IPythonQtConsole
120 # IPythonQtConsole
121 #-----------------------------------------------------------------------------
121 #-----------------------------------------------------------------------------
122
122
123
123
124 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
124 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
125 name = 'ipython-qtconsole'
125 name = 'ipython-qtconsole'
126
126
127 description = """
127 description = """
128 The IPython QtConsole.
128 The IPython QtConsole.
129
129
130 This launches a Console-style application using Qt. It is not a full
130 This launches a Console-style application using Qt. It is not a full
131 console, in that launched terminal subprocesses will not be able to accept
131 console, in that launched terminal subprocesses will not be able to accept
132 input.
132 input.
133
133
134 The QtConsole supports various extra features beyond the Terminal IPython
134 The QtConsole supports various extra features beyond the Terminal IPython
135 shell, such as inline plotting with matplotlib, via:
135 shell, such as inline plotting with matplotlib, via:
136
136
137 ipython qtconsole --pylab=inline
137 ipython qtconsole --pylab=inline
138
138
139 as well as saving your session as HTML, and printing the output.
139 as well as saving your session as HTML, and printing the output.
140
140
141 """
141 """
142 examples = _examples
142 examples = _examples
143
143
144 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
144 classes = [IPKernelApp, IPythonWidget, ZMQInteractiveShell, ProfileDir, Session]
145 flags = Dict(flags)
145 flags = Dict(flags)
146 aliases = Dict(aliases)
146 aliases = Dict(aliases)
147 frontend_flags = Any(qt_flags)
147 frontend_flags = Any(qt_flags)
148 frontend_aliases = Any(qt_aliases)
148 frontend_aliases = Any(qt_aliases)
149 kernel_manager_class = QtKernelManager
149 kernel_manager_class = QtKernelManager
150
150
151 stylesheet = Unicode('', config=True,
151 stylesheet = Unicode('', config=True,
152 help="path to a custom CSS stylesheet")
152 help="path to a custom CSS stylesheet")
153
153
154 plain = CBool(False, config=True,
154 plain = CBool(False, config=True,
155 help="Use a plaintext widget instead of rich text (plain can't print/save).")
155 help="Use a plaintext widget instead of rich text (plain can't print/save).")
156
156
157 def _pure_changed(self, name, old, new):
157 def _pure_changed(self, name, old, new):
158 kind = 'plain' if self.plain else 'rich'
158 kind = 'plain' if self.plain else 'rich'
159 self.config.ConsoleWidget.kind = kind
159 self.config.ConsoleWidget.kind = kind
160 if self.pure:
160 if self.pure:
161 self.widget_factory = FrontendWidget
161 self.widget_factory = FrontendWidget
162 elif self.plain:
162 elif self.plain:
163 self.widget_factory = IPythonWidget
163 self.widget_factory = IPythonWidget
164 else:
164 else:
165 self.widget_factory = RichIPythonWidget
165 self.widget_factory = RichIPythonWidget
166
166
167 _plain_changed = _pure_changed
167 _plain_changed = _pure_changed
168
168
169 # the factory for creating a widget
169 # the factory for creating a widget
170 widget_factory = Any(RichIPythonWidget)
170 widget_factory = Any(RichIPythonWidget)
171
171
172 def parse_command_line(self, argv=None):
172 def parse_command_line(self, argv=None):
173 super(IPythonQtConsoleApp, self).parse_command_line(argv)
173 super(IPythonQtConsoleApp, self).parse_command_line(argv)
174 self.build_kernel_argv(argv)
174 self.build_kernel_argv(argv)
175
175
176
176
177 def new_frontend_master(self):
177 def new_frontend_master(self):
178 """ Create and return new frontend attached to new kernel, launched on localhost.
178 """ Create and return new frontend attached to new kernel, launched on localhost.
179 """
179 """
180 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
180 ip = self.ip if self.ip in LOCAL_IPS else LOCALHOST
181 kernel_manager = self.kernel_manager_class(
181 kernel_manager = self.kernel_manager_class(
182 ip=ip,
182 ip=ip,
183 connection_file=self._new_connection_file(),
183 connection_file=self._new_connection_file(),
184 config=self.config,
184 config=self.config,
185 )
185 )
186 # start the kernel
186 # start the kernel
187 kwargs = dict(ipython=not self.pure)
187 kwargs = dict(ipython=not self.pure)
188 kwargs['extra_arguments'] = self.kernel_argv
188 kwargs['extra_arguments'] = self.kernel_argv
189 kernel_manager.start_kernel(**kwargs)
189 kernel_manager.start_kernel(**kwargs)
190 kernel_manager.start_channels()
190 kernel_manager.start_channels()
191 widget = self.widget_factory(config=self.config,
191 widget = self.widget_factory(config=self.config,
192 local_kernel=True)
192 local_kernel=True)
193 self.init_colors(widget)
193 widget.kernel_manager = kernel_manager
194 widget.kernel_manager = kernel_manager
194 widget._existing = False
195 widget._existing = False
195 widget._may_close = True
196 widget._may_close = True
196 widget._confirm_exit = self.confirm_exit
197 widget._confirm_exit = self.confirm_exit
197 return widget
198 return widget
198
199
199 def new_frontend_slave(self, current_widget):
200 def new_frontend_slave(self, current_widget):
200 """Create and return a new frontend attached to an existing kernel.
201 """Create and return a new frontend attached to an existing kernel.
201
202
202 Parameters
203 Parameters
203 ----------
204 ----------
204 current_widget : IPythonWidget
205 current_widget : IPythonWidget
205 The IPythonWidget whose kernel this frontend is to share
206 The IPythonWidget whose kernel this frontend is to share
206 """
207 """
207 kernel_manager = self.kernel_manager_class(
208 kernel_manager = self.kernel_manager_class(
208 connection_file=current_widget.kernel_manager.connection_file,
209 connection_file=current_widget.kernel_manager.connection_file,
209 config = self.config,
210 config = self.config,
210 )
211 )
211 kernel_manager.load_connection_file()
212 kernel_manager.load_connection_file()
212 kernel_manager.start_channels()
213 kernel_manager.start_channels()
213 widget = self.widget_factory(config=self.config,
214 widget = self.widget_factory(config=self.config,
214 local_kernel=False)
215 local_kernel=False)
216 self.init_colors(widget)
215 widget._existing = True
217 widget._existing = True
216 widget._may_close = False
218 widget._may_close = False
217 widget._confirm_exit = False
219 widget._confirm_exit = False
218 widget.kernel_manager = kernel_manager
220 widget.kernel_manager = kernel_manager
219 return widget
221 return widget
220
222
221 def init_qt_elements(self):
223 def init_qt_elements(self):
222 # Create the widget.
224 # Create the widget.
223 self.app = QtGui.QApplication([])
225 self.app = QtGui.QApplication([])
224
226
225 base_path = os.path.abspath(os.path.dirname(__file__))
227 base_path = os.path.abspath(os.path.dirname(__file__))
226 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
228 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
227 self.app.icon = QtGui.QIcon(icon_path)
229 self.app.icon = QtGui.QIcon(icon_path)
228 QtGui.QApplication.setWindowIcon(self.app.icon)
230 QtGui.QApplication.setWindowIcon(self.app.icon)
229
231
230 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
232 local_kernel = (not self.existing) or self.ip in LOCAL_IPS
231 self.widget = self.widget_factory(config=self.config,
233 self.widget = self.widget_factory(config=self.config,
232 local_kernel=local_kernel)
234 local_kernel=local_kernel)
235 self.init_colors(self.widget)
233 self.widget._existing = self.existing
236 self.widget._existing = self.existing
234 self.widget._may_close = not self.existing
237 self.widget._may_close = not self.existing
235 self.widget._confirm_exit = self.confirm_exit
238 self.widget._confirm_exit = self.confirm_exit
236
239
237 self.widget.kernel_manager = self.kernel_manager
240 self.widget.kernel_manager = self.kernel_manager
238 self.window = MainWindow(self.app,
241 self.window = MainWindow(self.app,
239 confirm_exit=self.confirm_exit,
242 confirm_exit=self.confirm_exit,
240 new_frontend_factory=self.new_frontend_master,
243 new_frontend_factory=self.new_frontend_master,
241 slave_frontend_factory=self.new_frontend_slave,
244 slave_frontend_factory=self.new_frontend_slave,
242 )
245 )
243 self.window.log = self.log
246 self.window.log = self.log
244 self.window.add_tab_with_frontend(self.widget)
247 self.window.add_tab_with_frontend(self.widget)
245 self.window.init_menu_bar()
248 self.window.init_menu_bar()
246
249
247 self.window.setWindowTitle('Python' if self.pure else 'IPython')
250 self.window.setWindowTitle('Python' if self.pure else 'IPython')
248
251
249 def init_colors(self):
252 def init_colors(self, widget):
250 """Configure the coloring of the widget"""
253 """Configure the coloring of the widget"""
251 # Note: This will be dramatically simplified when colors
254 # Note: This will be dramatically simplified when colors
252 # are removed from the backend.
255 # are removed from the backend.
253
256
254 if self.pure:
257 if self.pure:
255 # only IPythonWidget supports styling
258 # only IPythonWidget supports styling
256 return
259 return
257
260
258 # parse the colors arg down to current known labels
261 # parse the colors arg down to current known labels
259 try:
262 try:
260 colors = self.config.ZMQInteractiveShell.colors
263 colors = self.config.ZMQInteractiveShell.colors
261 except AttributeError:
264 except AttributeError:
262 colors = None
265 colors = None
263 try:
266 try:
264 style = self.config.IPythonWidget.syntax_style
267 style = self.config.IPythonWidget.syntax_style
265 except AttributeError:
268 except AttributeError:
266 style = None
269 style = None
270 try:
271 sheet = self.config.IPythonWidget.style_sheet
272 except AttributeError:
273 sheet = None
267
274
268 # find the value for colors:
275 # find the value for colors:
269 if colors:
276 if colors:
270 colors=colors.lower()
277 colors=colors.lower()
271 if colors in ('lightbg', 'light'):
278 if colors in ('lightbg', 'light'):
272 colors='lightbg'
279 colors='lightbg'
273 elif colors in ('dark', 'linux'):
280 elif colors in ('dark', 'linux'):
274 colors='linux'
281 colors='linux'
275 else:
282 else:
276 colors='nocolor'
283 colors='nocolor'
277 elif style:
284 elif style:
278 if style=='bw':
285 if style=='bw':
279 colors='nocolor'
286 colors='nocolor'
280 elif styles.dark_style(style):
287 elif styles.dark_style(style):
281 colors='linux'
288 colors='linux'
282 else:
289 else:
283 colors='lightbg'
290 colors='lightbg'
284 else:
291 else:
285 colors=None
292 colors=None
286
293
287 # Configure the style.
294 # Configure the style
288 widget = self.widget
289 if style:
295 if style:
290 widget.style_sheet = styles.sheet_from_template(style, colors)
296 widget.style_sheet = styles.sheet_from_template(style, colors)
291 widget.syntax_style = style
297 widget.syntax_style = style
292 widget._syntax_style_changed()
298 widget._syntax_style_changed()
293 widget._style_sheet_changed()
299 widget._style_sheet_changed()
294 elif colors:
300 elif colors:
295 # use a default style
301 # use a default dark/light/bw style
296 widget.set_default_style(colors=colors)
302 widget.set_default_style(colors=colors)
297 else:
298 # this is redundant for now, but allows the widget's
299 # defaults to change
300 widget.set_default_style()
301
303
302 if self.stylesheet:
304 if self.stylesheet:
303 # we got an expicit stylesheet
305 # we got an explicit stylesheet
304 if os.path.isfile(self.stylesheet):
306 if os.path.isfile(self.stylesheet):
305 with open(self.stylesheet) as f:
307 with open(self.stylesheet) as f:
306 sheet = f.read()
308 sheet = f.read()
307 widget.style_sheet = sheet
308 widget._style_sheet_changed()
309 else:
309 else:
310 raise IOError("Stylesheet %r not found."%self.stylesheet)
310 raise IOError("Stylesheet %r not found." % self.stylesheet)
311 if sheet:
312 widget.style_sheet = sheet
313 widget._style_sheet_changed()
314
311
315
312 def init_signal(self):
316 def init_signal(self):
313 """allow clean shutdown on sigint"""
317 """allow clean shutdown on sigint"""
314 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
318 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
315 # need a timer, so that QApplication doesn't block until a real
319 # need a timer, so that QApplication doesn't block until a real
316 # Qt event fires (can require mouse movement)
320 # Qt event fires (can require mouse movement)
317 # timer trick from http://stackoverflow.com/q/4938723/938949
321 # timer trick from http://stackoverflow.com/q/4938723/938949
318 timer = QtCore.QTimer()
322 timer = QtCore.QTimer()
319 # Let the interpreter run each 200 ms:
323 # Let the interpreter run each 200 ms:
320 timer.timeout.connect(lambda: None)
324 timer.timeout.connect(lambda: None)
321 timer.start(200)
325 timer.start(200)
322 # hold onto ref, so the timer doesn't get cleaned up
326 # hold onto ref, so the timer doesn't get cleaned up
323 self._sigint_timer = timer
327 self._sigint_timer = timer
324
328
325 @catch_config_error
329 @catch_config_error
326 def initialize(self, argv=None):
330 def initialize(self, argv=None):
327 super(IPythonQtConsoleApp, self).initialize(argv)
331 super(IPythonQtConsoleApp, self).initialize(argv)
328 IPythonConsoleApp.initialize(self,argv)
332 IPythonConsoleApp.initialize(self,argv)
329 self.init_qt_elements()
333 self.init_qt_elements()
330 self.init_colors()
331 self.init_signal()
334 self.init_signal()
332
335
333 def start(self):
336 def start(self):
334
337
335 # draw the window
338 # draw the window
336 self.window.show()
339 self.window.show()
337 self.window.raise_()
340 self.window.raise_()
338
341
339 # Start the application main loop.
342 # Start the application main loop.
340 self.app.exec_()
343 self.app.exec_()
341
344
342 #-----------------------------------------------------------------------------
345 #-----------------------------------------------------------------------------
343 # Main entry point
346 # Main entry point
344 #-----------------------------------------------------------------------------
347 #-----------------------------------------------------------------------------
345
348
346 def main():
349 def main():
347 app = IPythonQtConsoleApp()
350 app = IPythonQtConsoleApp()
348 app.initialize()
351 app.initialize()
349 app.start()
352 app.start()
350
353
351
354
352 if __name__ == '__main__':
355 if __name__ == '__main__':
353 main()
356 main()
@@ -1,119 +1,119 b''
1 """ Style utilities, templates, and defaults for syntax highlighting widgets.
1 """ Style utilities, templates, and defaults for syntax highlighting widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 from colorsys import rgb_to_hls
7 from colorsys import rgb_to_hls
8 from pygments.styles import get_style_by_name
8 from pygments.styles import get_style_by_name
9 from pygments.token import Token
9 from pygments.token import Token
10
10
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12 # Constants
12 # Constants
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14
14
15 # The default light style sheet: black text on a white background.
15 # The default light style sheet: black text on a white background.
16 default_light_style_template = '''
16 default_light_style_template = '''
17 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
17 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
18 color: %(fgcolor)s ;
18 color: %(fgcolor)s ;
19 selection-background-color: %(select)s}
19 selection-background-color: %(select)s}
20 .error { color: red; }
20 .error { color: red; }
21 .in-prompt { color: navy; }
21 .in-prompt { color: navy; }
22 .in-prompt-number { font-weight: bold; }
22 .in-prompt-number { font-weight: bold; }
23 .out-prompt { color: darkred; }
23 .out-prompt { color: darkred; }
24 .out-prompt-number { font-weight: bold; }
24 .out-prompt-number { font-weight: bold; }
25 '''
25 '''
26 default_light_style_sheet = default_light_style_template%dict(
26 default_light_style_sheet = default_light_style_template%dict(
27 bgcolor='white', fgcolor='black', select="#ccc")
27 bgcolor='white', fgcolor='black', select="#ccc")
28 default_light_syntax_style = 'default'
28 default_light_syntax_style = 'default'
29
29
30 # The default dark style sheet: white text on a black background.
30 # The default dark style sheet: white text on a black background.
31 default_dark_style_template = '''
31 default_dark_style_template = '''
32 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
32 QPlainTextEdit, QTextEdit { background-color: %(bgcolor)s;
33 color: %(fgcolor)s ;
33 color: %(fgcolor)s ;
34 selection-background-color: %(select)s}
34 selection-background-color: %(select)s}
35 QFrame { border: 1px solid grey; }
35 QFrame { border: 1px solid grey; }
36 .error { color: red; }
36 .error { color: red; }
37 .in-prompt { color: lime; }
37 .in-prompt { color: lime; }
38 .in-prompt-number { color: lime; font-weight: bold; }
38 .in-prompt-number { color: lime; font-weight: bold; }
39 .out-prompt { color: red; }
39 .out-prompt { color: red; }
40 .out-prompt-number { color: red; font-weight: bold; }
40 .out-prompt-number { color: red; font-weight: bold; }
41 '''
41 '''
42 default_dark_style_sheet = default_dark_style_template%dict(
42 default_dark_style_sheet = default_dark_style_template%dict(
43 bgcolor='black', fgcolor='white', select="#555")
43 bgcolor='black', fgcolor='white', select="#555")
44 default_dark_syntax_style = 'monokai'
44 default_dark_syntax_style = 'monokai'
45
45
46 # The default monochrome
46 # The default monochrome
47 default_bw_style_sheet = '''
47 default_bw_style_sheet = '''
48 QPlainTextEdit, QTextEdit { background-color: white;
48 QPlainTextEdit, QTextEdit { background-color: white;
49 color: black ;
49 color: black ;
50 selection-background-color: #cccccc}
50 selection-background-color: #cccccc}
51 .in-prompt-number { font-weight: bold; }
51 .in-prompt-number { font-weight: bold; }
52 .out-prompt-number { font-weight: bold; }
52 .out-prompt-number { font-weight: bold; }
53 '''
53 '''
54 default_bw_syntax_style = 'bw'
54 default_bw_syntax_style = 'bw'
55
55
56
56
57 def hex_to_rgb(color):
57 def hex_to_rgb(color):
58 """Convert a hex color to rgb integer tuple."""
58 """Convert a hex color to rgb integer tuple."""
59 if color.startswith('#'):
59 if color.startswith('#'):
60 color = color[1:]
60 color = color[1:]
61 if len(color) == 3:
61 if len(color) == 3:
62 color = ''.join([c*2 for c in color])
62 color = ''.join([c*2 for c in color])
63 if len(color) != 6:
63 if len(color) != 6:
64 return False
64 return False
65 try:
65 try:
66 r = int(color[:2],16)
66 r = int(color[:2],16)
67 g = int(color[:2],16)
67 g = int(color[2:4],16)
68 b = int(color[:2],16)
68 b = int(color[4:],16)
69 except ValueError:
69 except ValueError:
70 return False
70 return False
71 else:
71 else:
72 return r,g,b
72 return r,g,b
73
73
74 def dark_color(color):
74 def dark_color(color):
75 """Check whether a color is 'dark'.
75 """Check whether a color is 'dark'.
76
76
77 Currently, this is simply whether the luminance is <50%"""
77 Currently, this is simply whether the luminance is <50%"""
78 rgb = hex_to_rgb(color)
78 rgb = hex_to_rgb(color)
79 if rgb:
79 if rgb:
80 return rgb_to_hls(*rgb)[1] < 128
80 return rgb_to_hls(*rgb)[1] < 128
81 else: # default to False
81 else: # default to False
82 return False
82 return False
83
83
84 def dark_style(stylename):
84 def dark_style(stylename):
85 """Guess whether the background of the style with name 'stylename'
85 """Guess whether the background of the style with name 'stylename'
86 counts as 'dark'."""
86 counts as 'dark'."""
87 return dark_color(get_style_by_name(stylename).background_color)
87 return dark_color(get_style_by_name(stylename).background_color)
88
88
89 def get_colors(stylename):
89 def get_colors(stylename):
90 """Construct the keys to be used building the base stylesheet
90 """Construct the keys to be used building the base stylesheet
91 from a templatee."""
91 from a templatee."""
92 style = get_style_by_name(stylename)
92 style = get_style_by_name(stylename)
93 fgcolor = style.style_for_token(Token.Text)['color'] or ''
93 fgcolor = style.style_for_token(Token.Text)['color'] or ''
94 if len(fgcolor) in (3,6):
94 if len(fgcolor) in (3,6):
95 # could be 'abcdef' or 'ace' hex, which needs '#' prefix
95 # could be 'abcdef' or 'ace' hex, which needs '#' prefix
96 try:
96 try:
97 int(fgcolor, 16)
97 int(fgcolor, 16)
98 except TypeError:
98 except TypeError:
99 pass
99 pass
100 else:
100 else:
101 fgcolor = "#"+fgcolor
101 fgcolor = "#"+fgcolor
102
102
103 return dict(
103 return dict(
104 bgcolor = style.background_color,
104 bgcolor = style.background_color,
105 select = style.highlight_color,
105 select = style.highlight_color,
106 fgcolor = fgcolor
106 fgcolor = fgcolor
107 )
107 )
108
108
109 def sheet_from_template(name, colors='lightbg'):
109 def sheet_from_template(name, colors='lightbg'):
110 """Use one of the base templates, and set bg/fg/select colors."""
110 """Use one of the base templates, and set bg/fg/select colors."""
111 colors = colors.lower()
111 colors = colors.lower()
112 if colors=='lightbg':
112 if colors=='lightbg':
113 return default_light_style_template%get_colors(name)
113 return default_light_style_template%get_colors(name)
114 elif colors=='linux':
114 elif colors=='linux':
115 return default_dark_style_template%get_colors(name)
115 return default_dark_style_template%get_colors(name)
116 elif colors=='nocolor':
116 elif colors=='nocolor':
117 return default_bw_style_sheet
117 return default_bw_style_sheet
118 else:
118 else:
119 raise KeyError("No such color scheme: %s"%colors)
119 raise KeyError("No such color scheme: %s"%colors)
General Comments 0
You need to be logged in to leave comments. Login now