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