##// END OF EJS Templates
Refactored ConsoleWidget's HTML exportaton code + other minor code cleanup.
epatters -
Show More
@@ -0,0 +1,233 b''
1 """ Defines classes and functions for working with Qt's rich text system.
2 """
3 #-----------------------------------------------------------------------------
4 # Imports
5 #-----------------------------------------------------------------------------
6
7 from __future__ import with_statement
8
9 # Standard library imports.
10 import os
11 import re
12
13 # System library imports.
14 from IPython.external.qt import QtGui
15
16 #-----------------------------------------------------------------------------
17 # Constants
18 #-----------------------------------------------------------------------------
19
20 # A regular expression for matching images in rich text HTML.
21 # Note that this is overly restrictive, but Qt's output is predictable...
22 IMG_RE = re.compile(r'<img src="(?P<name>[\d]+)" />')
23
24 #-----------------------------------------------------------------------------
25 # Classes
26 #-----------------------------------------------------------------------------
27
28 class HtmlExporter(object):
29 """ A stateful HTML exporter for a Q(Plain)TextEdit.
30
31 This class is designed for convenient user interaction.
32 """
33
34 def __init__(self, control):
35 """ Creates an HtmlExporter for the given Q(Plain)TextEdit.
36 """
37 assert isinstance(control, (QtGui.QPlainTextEdit, QtGui.QTextEdit))
38 self.control = control
39 self.filename = 'ipython.html'
40 self.image_tag = None
41 self.inline_png = None
42
43 def export(self):
44 """ Displays a dialog for exporting HTML generated by Qt's rich text
45 system.
46
47 Returns
48 -------
49 The name of the file that was saved, or None if no file was saved.
50 """
51 parent = self.control.window()
52 dialog = QtGui.QFileDialog(parent, 'Save as...')
53 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
54 filters = [
55 'HTML with PNG figures (*.html *.htm)',
56 'XHTML with inline SVG figures (*.xhtml *.xml)'
57 ]
58 dialog.setNameFilters(filters)
59 if self.filename:
60 dialog.selectFile(self.filename)
61 root,ext = os.path.splitext(self.filename)
62 if ext.lower() in ('.xml', '.xhtml'):
63 dialog.selectNameFilter(filters[-1])
64
65 if dialog.exec_():
66 self.filename = dialog.selectedFiles()[0]
67 choice = dialog.selectedNameFilter()
68 html = self.control.document().toHtml().encode('utf-8')
69
70 # Configure the exporter.
71 if choice.startswith('XHTML'):
72 exporter = export_xhtml
73 else:
74 # If there are PNGs, decide how to export them.
75 inline = self.inline_png
76 if inline is None and IMG_RE.search(html):
77 dialog = QtGui.QDialog(parent)
78 dialog.setWindowTitle('Save as...')
79 layout = QtGui.QVBoxLayout(dialog)
80 msg = "Exporting HTML with PNGs"
81 info = "Would you like inline PNGs (single large html " \
82 "file) or external image files?"
83 checkbox = QtGui.QCheckBox("&Don't ask again")
84 checkbox.setShortcut('D')
85 ib = QtGui.QPushButton("&Inline")
86 ib.setShortcut('I')
87 eb = QtGui.QPushButton("&External")
88 eb.setShortcut('E')
89 box = QtGui.QMessageBox(QtGui.QMessageBox.Question,
90 dialog.windowTitle(), msg)
91 box.setInformativeText(info)
92 box.addButton(ib, QtGui.QMessageBox.NoRole)
93 box.addButton(eb, QtGui.QMessageBox.YesRole)
94 box.setDefaultButton(ib)
95 layout.setSpacing(0)
96 layout.addWidget(box)
97 layout.addWidget(checkbox)
98 dialog.setLayout(layout)
99 dialog.show()
100 reply = box.exec_()
101 dialog.hide()
102 inline = (reply == 0)
103 if checkbox.checkState():
104 # Don't ask anymore; always use this choice.
105 self.inline_png = inline
106 exporter = lambda h, f, i: export_html(h, f, i, inline)
107
108 # Perform the export!
109 try:
110 return exporter(html, self.filename, self.image_tag)
111 except Exception, e:
112 title = self.window().windowTitle()
113 msg = "Error while saving to: %s\n" % filename + str(e)
114 reply = QtGui.QMessageBox.warning(parent, title, msg,
115 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
116
117 return None
118
119 #-----------------------------------------------------------------------------
120 # Functions
121 #-----------------------------------------------------------------------------
122
123 def export_html(html, filename, image_tag = None, inline = True):
124 """ Export the contents of the ConsoleWidget as HTML.
125
126 Parameters:
127 -----------
128 html : str,
129 A utf-8 encoded Python string containing the Qt HTML to export.
130
131 filename : str
132 The file to be saved.
133
134 image_tag : callable, optional (default None)
135 Used to convert images. See ``default_image_tag()`` for information.
136
137 inline : bool, optional [default True]
138 If True, include images as inline PNGs. Otherwise, include them as
139 links to external PNG files, mimicking web browsers' "Web Page,
140 Complete" behavior.
141 """
142 if image_tag is None:
143 image_tag = default_image_tag
144
145 if inline:
146 path = None
147 else:
148 root,ext = os.path.splitext(filename)
149 path = root + "_files"
150 if os.path.isfile(path):
151 raise OSError("%s exists, but is not a directory." % path)
152
153 with open(filename, 'w') as f:
154 html = fix_html_encoding(html)
155 f.write(IMG_RE.sub(lambda x: image_tag(x, path = path, format = "png"),
156 html))
157
158
159 def export_xhtml(html, filename, image_tag=None):
160 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
161
162 Parameters:
163 -----------
164 html : str,
165 A utf-8 encoded Python string containing the Qt HTML to export.
166
167 filename : str
168 The file to be saved.
169
170 image_tag : callable, optional (default None)
171 Used to convert images. See ``default_image_tag()`` for information.
172 """
173 if image_tag is None:
174 image_tag = default_image_tag
175
176 with open(filename, 'w') as f:
177 # Hack to make xhtml header -- note that we are not doing any check for
178 # valid XML.
179 offset = html.find("<html>")
180 assert(offset > -1)
181 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
182 html[offset+6:])
183
184 html = fix_html_encoding(html)
185 f.write(IMG_RE.sub(lambda x: image_tag(x, path = None, format = "svg"),
186 html))
187
188
189 def default_image_tag(match, path = None, format = "png"):
190 """ Return (X)HTML mark-up for the image-tag given by match.
191
192 This default implementation merely removes the image, and exists mostly
193 for documentation purposes. More information than is present in the Qt
194 HTML is required to supply the images.
195
196 Parameters
197 ----------
198 match : re.SRE_Match
199 A match to an HTML image tag as exported by Qt, with match.group("Name")
200 containing the matched image ID.
201
202 path : string|None, optional [default None]
203 If not None, specifies a path to which supporting files may be written
204 (e.g., for linked images). If None, all images are to be included
205 inline.
206
207 format : "png"|"svg", optional [default "png"]
208 Format for returned or referenced images.
209 """
210 return ''
211
212
213 def fix_html_encoding(html):
214 """ Return html string, with a UTF-8 declaration added to <HEAD>.
215
216 Assumes that html is Qt generated and has already been UTF-8 encoded
217 and coerced to a python string. If the expected head element is
218 not found, the given object is returned unmodified.
219
220 This patching is needed for proper rendering of some characters
221 (e.g., indented commands) when viewing exported HTML on a local
222 system (i.e., without seeing an encoding declaration in an HTTP
223 header).
224
225 C.f. http://www.w3.org/International/O-charset for details.
226 """
227 offset = html.find('<head>')
228 if offset > -1:
229 html = (html[:offset+6]+
230 '\n<meta http-equiv="Content-Type" '+
231 'content="text/html; charset=utf-8" />\n'+
232 html[offset+6:])
233 return html
@@ -1,1912 +1,1713 b''
1 """ An abstract base class for console-type widgets.
1 """ An abstract base class for console-type widgets.
2 """
2 """
3 #-----------------------------------------------------------------------------
3 #-----------------------------------------------------------------------------
4 # Imports
4 # Imports
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6
6
7 # Standard library imports
7 # Standard library imports
8 import os
8 import os
9 from os.path import commonprefix
9 from os.path import commonprefix
10 import re
10 import re
11 import sys
11 import sys
12 from textwrap import dedent
12 from textwrap import dedent
13 from unicodedata import category
13 from unicodedata import category
14
14
15 # System library imports
15 # System library imports
16 from IPython.external.qt import QtCore, QtGui
16 from IPython.external.qt import QtCore, QtGui
17
17
18 # Local imports
18 # Local imports
19 from IPython.config.configurable import Configurable
19 from IPython.config.configurable import Configurable
20 from IPython.frontend.qt.rich_text import HtmlExporter
20 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
21 from IPython.utils.traitlets import Bool, Enum, Int
22 from IPython.utils.traitlets import Bool, Enum, Int
22 from ansi_code_processor import QtAnsiCodeProcessor
23 from ansi_code_processor import QtAnsiCodeProcessor
23 from completion_widget import CompletionWidget
24 from completion_widget import CompletionWidget
24
25
25 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
26 # Functions
27 # Functions
27 #-----------------------------------------------------------------------------
28 #-----------------------------------------------------------------------------
28
29
29 def is_letter_or_number(char):
30 def is_letter_or_number(char):
30 """ Returns whether the specified unicode character is a letter or a number.
31 """ Returns whether the specified unicode character is a letter or a number.
31 """
32 """
32 cat = category(char)
33 cat = category(char)
33 return cat.startswith('L') or cat.startswith('N')
34 return cat.startswith('L') or cat.startswith('N')
34
35
35 #-----------------------------------------------------------------------------
36 #-----------------------------------------------------------------------------
36 # Classes
37 # Classes
37 #-----------------------------------------------------------------------------
38 #-----------------------------------------------------------------------------
38
39
39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 class ConsoleWidget(Configurable, QtGui.QWidget):
40 """ An abstract base class for console-type widgets. This class has
41 """ An abstract base class for console-type widgets. This class has
41 functionality for:
42 functionality for:
42
43
43 * Maintaining a prompt and editing region
44 * Maintaining a prompt and editing region
44 * Providing the traditional Unix-style console keyboard shortcuts
45 * Providing the traditional Unix-style console keyboard shortcuts
45 * Performing tab completion
46 * Performing tab completion
46 * Paging text
47 * Paging text
47 * Handling ANSI escape codes
48 * Handling ANSI escape codes
48
49
49 ConsoleWidget also provides a number of utility methods that will be
50 ConsoleWidget also provides a number of utility methods that will be
50 convenient to implementors of a console-style widget.
51 convenient to implementors of a console-style widget.
51 """
52 """
52 __metaclass__ = MetaQObjectHasTraits
53 __metaclass__ = MetaQObjectHasTraits
53
54
54 #------ Configuration ------------------------------------------------------
55 #------ Configuration ------------------------------------------------------
55
56
56 # Whether to process ANSI escape codes.
57 # Whether to process ANSI escape codes.
57 ansi_codes = Bool(True, config=True)
58 ansi_codes = Bool(True, config=True)
58
59
59 # The maximum number of lines of text before truncation. Specifying a
60 # The maximum number of lines of text before truncation. Specifying a
60 # non-positive number disables text truncation (not recommended).
61 # non-positive number disables text truncation (not recommended).
61 buffer_size = Int(500, config=True)
62 buffer_size = Int(500, config=True)
62
63
63 # Whether to use a list widget or plain text output for tab completion.
64 # Whether to use a list widget or plain text output for tab completion.
64 gui_completion = Bool(False, config=True)
65 gui_completion = Bool(False, config=True)
65
66
66 # The type of underlying text widget to use. Valid values are 'plain', which
67 # The type of underlying text widget to use. Valid values are 'plain', which
67 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
68 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
68 # NOTE: this value can only be specified during initialization.
69 # NOTE: this value can only be specified during initialization.
69 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
70 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
70
71
71 # The type of paging to use. Valid values are:
72 # The type of paging to use. Valid values are:
72 # 'inside' : The widget pages like a traditional terminal.
73 # 'inside' : The widget pages like a traditional terminal.
73 # 'hsplit' : When paging is requested, the widget is split
74 # 'hsplit' : When paging is requested, the widget is split
74 # horizontally. The top pane contains the console, and the
75 # horizontally. The top pane contains the console, and the
75 # bottom pane contains the paged text.
76 # bottom pane contains the paged text.
76 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
77 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
77 # 'custom' : No action is taken by the widget beyond emitting a
78 # 'custom' : No action is taken by the widget beyond emitting a
78 # 'custom_page_requested(str)' signal.
79 # 'custom_page_requested(str)' signal.
79 # 'none' : The text is written directly to the console.
80 # 'none' : The text is written directly to the console.
80 # NOTE: this value can only be specified during initialization.
81 # NOTE: this value can only be specified during initialization.
81 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
82 default_value='inside', config=True)
83 default_value='inside', config=True)
83
84
84 # Whether to override ShortcutEvents for the keybindings defined by this
85 # Whether to override ShortcutEvents for the keybindings defined by this
85 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
86 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
86 # priority (when it has focus) over, e.g., window-level menu shortcuts.
87 # priority (when it has focus) over, e.g., window-level menu shortcuts.
87 override_shortcuts = Bool(False)
88 override_shortcuts = Bool(False)
88
89
89 #------ Signals ------------------------------------------------------------
90 #------ Signals ------------------------------------------------------------
90
91
91 # Signals that indicate ConsoleWidget state.
92 # Signals that indicate ConsoleWidget state.
92 copy_available = QtCore.Signal(bool)
93 copy_available = QtCore.Signal(bool)
93 redo_available = QtCore.Signal(bool)
94 redo_available = QtCore.Signal(bool)
94 undo_available = QtCore.Signal(bool)
95 undo_available = QtCore.Signal(bool)
95
96
96 # Signal emitted when paging is needed and the paging style has been
97 # Signal emitted when paging is needed and the paging style has been
97 # specified as 'custom'.
98 # specified as 'custom'.
98 custom_page_requested = QtCore.Signal(object)
99 custom_page_requested = QtCore.Signal(object)
99
100
100 # Signal emitted when the font is changed.
101 # Signal emitted when the font is changed.
101 font_changed = QtCore.Signal(QtGui.QFont)
102 font_changed = QtCore.Signal(QtGui.QFont)
102
103
103 #------ Protected class variables ------------------------------------------
104 #------ Protected class variables ------------------------------------------
104
105
105 # When the control key is down, these keys are mapped.
106 # When the control key is down, these keys are mapped.
106 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
107 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
107 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
108 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
108 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
109 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
109 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
110 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
110 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
111 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
111 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
112 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
112 if not sys.platform == 'darwin':
113 if not sys.platform == 'darwin':
113 # On OS X, Ctrl-E already does the right thing, whereas End moves the
114 # On OS X, Ctrl-E already does the right thing, whereas End moves the
114 # cursor to the bottom of the buffer.
115 # cursor to the bottom of the buffer.
115 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
116 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
116
117
117 # The shortcuts defined by this widget. We need to keep track of these to
118 # The shortcuts defined by this widget. We need to keep track of these to
118 # support 'override_shortcuts' above.
119 # support 'override_shortcuts' above.
119 _shortcuts = set(_ctrl_down_remap.keys() +
120 _shortcuts = set(_ctrl_down_remap.keys() +
120 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
121 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
121 QtCore.Qt.Key_V ])
122 QtCore.Qt.Key_V ])
122
123
123 #---------------------------------------------------------------------------
124 #---------------------------------------------------------------------------
124 # 'QObject' interface
125 # 'QObject' interface
125 #---------------------------------------------------------------------------
126 #---------------------------------------------------------------------------
126
127
127 def __init__(self, parent=None, **kw):
128 def __init__(self, parent=None, **kw):
128 """ Create a ConsoleWidget.
129 """ Create a ConsoleWidget.
129
130
130 Parameters:
131 Parameters:
131 -----------
132 -----------
132 parent : QWidget, optional [default None]
133 parent : QWidget, optional [default None]
133 The parent for this widget.
134 The parent for this widget.
134 """
135 """
135 QtGui.QWidget.__init__(self, parent)
136 QtGui.QWidget.__init__(self, parent)
136 Configurable.__init__(self, **kw)
137 Configurable.__init__(self, **kw)
137
138
138 # Create the layout and underlying text widget.
139 # Create the layout and underlying text widget.
139 layout = QtGui.QStackedLayout(self)
140 layout = QtGui.QStackedLayout(self)
140 layout.setContentsMargins(0, 0, 0, 0)
141 layout.setContentsMargins(0, 0, 0, 0)
141 self._control = self._create_control()
142 self._control = self._create_control()
142 self._page_control = None
143 self._page_control = None
143 self._splitter = None
144 self._splitter = None
144 if self.paging in ('hsplit', 'vsplit'):
145 if self.paging in ('hsplit', 'vsplit'):
145 self._splitter = QtGui.QSplitter()
146 self._splitter = QtGui.QSplitter()
146 if self.paging == 'hsplit':
147 if self.paging == 'hsplit':
147 self._splitter.setOrientation(QtCore.Qt.Horizontal)
148 self._splitter.setOrientation(QtCore.Qt.Horizontal)
148 else:
149 else:
149 self._splitter.setOrientation(QtCore.Qt.Vertical)
150 self._splitter.setOrientation(QtCore.Qt.Vertical)
150 self._splitter.addWidget(self._control)
151 self._splitter.addWidget(self._control)
151 layout.addWidget(self._splitter)
152 layout.addWidget(self._splitter)
152 else:
153 else:
153 layout.addWidget(self._control)
154 layout.addWidget(self._control)
154
155
155 # Create the paging widget, if necessary.
156 # Create the paging widget, if necessary.
156 if self.paging in ('inside', 'hsplit', 'vsplit'):
157 if self.paging in ('inside', 'hsplit', 'vsplit'):
157 self._page_control = self._create_page_control()
158 self._page_control = self._create_page_control()
158 if self._splitter:
159 if self._splitter:
159 self._page_control.hide()
160 self._page_control.hide()
160 self._splitter.addWidget(self._page_control)
161 self._splitter.addWidget(self._page_control)
161 else:
162 else:
162 layout.addWidget(self._page_control)
163 layout.addWidget(self._page_control)
163
164
164 # Initialize protected variables. Some variables contain useful state
165 # Initialize protected variables. Some variables contain useful state
165 # information for subclasses; they should be considered read-only.
166 # information for subclasses; they should be considered read-only.
166 self._ansi_processor = QtAnsiCodeProcessor()
167 self._ansi_processor = QtAnsiCodeProcessor()
167 self._completion_widget = CompletionWidget(self._control)
168 self._completion_widget = CompletionWidget(self._control)
168 self._continuation_prompt = '> '
169 self._continuation_prompt = '> '
169 self._continuation_prompt_html = None
170 self._continuation_prompt_html = None
170 self._executing = False
171 self._executing = False
171 self._filter_drag = False
172 self._filter_drag = False
172 self._filter_resize = False
173 self._filter_resize = False
174 self._html_exporter = HtmlExporter(self._control)
173 self._prompt = ''
175 self._prompt = ''
174 self._prompt_html = None
176 self._prompt_html = None
175 self._prompt_pos = 0
177 self._prompt_pos = 0
176 self._prompt_sep = ''
178 self._prompt_sep = ''
177 self._reading = False
179 self._reading = False
178 self._reading_callback = None
180 self._reading_callback = None
179 self._tab_width = 8
181 self._tab_width = 8
180 self._text_completing_pos = 0
182 self._text_completing_pos = 0
181 self._filename = 'ipython.html'
182 self._png_mode=None
183
183
184 # Set a monospaced font.
184 # Set a monospaced font.
185 self.reset_font()
185 self.reset_font()
186
186
187 # Configure actions.
187 # Configure actions.
188 action = QtGui.QAction('Print', None)
188 action = QtGui.QAction('Print', None)
189 action.setEnabled(True)
189 action.setEnabled(True)
190 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
190 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
191 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
191 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
192 # Only override the default if there is a collision.
192 # Only override the default if there is a collision.
193 # Qt ctrl = cmd on OSX, so the match gets a false positive on darwin.
193 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
194 printkey = "Ctrl+Shift+P"
194 printkey = "Ctrl+Shift+P"
195 action.setShortcut(printkey)
195 action.setShortcut(printkey)
196 action.triggered.connect(self.print_)
196 action.triggered.connect(self.print_)
197 self.addAction(action)
197 self.addAction(action)
198 self._print_action = action
198 self._print_action = action
199
199
200 action = QtGui.QAction('Save as HTML/XML', None)
200 action = QtGui.QAction('Save as HTML/XML', None)
201 action.setEnabled(self.can_export())
202 action.setShortcut(QtGui.QKeySequence.Save)
201 action.setShortcut(QtGui.QKeySequence.Save)
203 action.triggered.connect(self.export)
202 action.triggered.connect(self._html_exporter.export)
204 self.addAction(action)
203 self.addAction(action)
205 self._export_action = action
204 self._export_action = action
206
205
207 action = QtGui.QAction('Select All', None)
206 action = QtGui.QAction('Select All', None)
208 action.setEnabled(True)
207 action.setEnabled(True)
209 action.setShortcut(QtGui.QKeySequence.SelectAll)
208 action.setShortcut(QtGui.QKeySequence.SelectAll)
210 action.triggered.connect(self.select_all)
209 action.triggered.connect(self.select_all)
211 self.addAction(action)
210 self.addAction(action)
212 self._select_all_action = action
211 self._select_all_action = action
213
214
212
215 def eventFilter(self, obj, event):
213 def eventFilter(self, obj, event):
216 """ Reimplemented to ensure a console-like behavior in the underlying
214 """ Reimplemented to ensure a console-like behavior in the underlying
217 text widgets.
215 text widgets.
218 """
216 """
219 etype = event.type()
217 etype = event.type()
220 if etype == QtCore.QEvent.KeyPress:
218 if etype == QtCore.QEvent.KeyPress:
221
219
222 # Re-map keys for all filtered widgets.
220 # Re-map keys for all filtered widgets.
223 key = event.key()
221 key = event.key()
224 if self._control_key_down(event.modifiers()) and \
222 if self._control_key_down(event.modifiers()) and \
225 key in self._ctrl_down_remap:
223 key in self._ctrl_down_remap:
226 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
224 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
227 self._ctrl_down_remap[key],
225 self._ctrl_down_remap[key],
228 QtCore.Qt.NoModifier)
226 QtCore.Qt.NoModifier)
229 QtGui.qApp.sendEvent(obj, new_event)
227 QtGui.qApp.sendEvent(obj, new_event)
230 return True
228 return True
231
229
232 elif obj == self._control:
230 elif obj == self._control:
233 return self._event_filter_console_keypress(event)
231 return self._event_filter_console_keypress(event)
234
232
235 elif obj == self._page_control:
233 elif obj == self._page_control:
236 return self._event_filter_page_keypress(event)
234 return self._event_filter_page_keypress(event)
237
235
238 # Make middle-click paste safe.
236 # Make middle-click paste safe.
239 elif etype == QtCore.QEvent.MouseButtonRelease and \
237 elif etype == QtCore.QEvent.MouseButtonRelease and \
240 event.button() == QtCore.Qt.MidButton and \
238 event.button() == QtCore.Qt.MidButton and \
241 obj == self._control.viewport():
239 obj == self._control.viewport():
242 cursor = self._control.cursorForPosition(event.pos())
240 cursor = self._control.cursorForPosition(event.pos())
243 self._control.setTextCursor(cursor)
241 self._control.setTextCursor(cursor)
244 self.paste(QtGui.QClipboard.Selection)
242 self.paste(QtGui.QClipboard.Selection)
245 return True
243 return True
246
244
247 # Manually adjust the scrollbars *after* a resize event is dispatched.
245 # Manually adjust the scrollbars *after* a resize event is dispatched.
248 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
246 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
249 self._filter_resize = True
247 self._filter_resize = True
250 QtGui.qApp.sendEvent(obj, event)
248 QtGui.qApp.sendEvent(obj, event)
251 self._adjust_scrollbars()
249 self._adjust_scrollbars()
252 self._filter_resize = False
250 self._filter_resize = False
253 return True
251 return True
254
252
255 # Override shortcuts for all filtered widgets.
253 # Override shortcuts for all filtered widgets.
256 elif etype == QtCore.QEvent.ShortcutOverride and \
254 elif etype == QtCore.QEvent.ShortcutOverride and \
257 self.override_shortcuts and \
255 self.override_shortcuts and \
258 self._control_key_down(event.modifiers()) and \
256 self._control_key_down(event.modifiers()) and \
259 event.key() in self._shortcuts:
257 event.key() in self._shortcuts:
260 event.accept()
258 event.accept()
261
259
262 # Ensure that drags are safe. The problem is that the drag starting
260 # Ensure that drags are safe. The problem is that the drag starting
263 # logic, which determines whether the drag is a Copy or Move, is locked
261 # logic, which determines whether the drag is a Copy or Move, is locked
264 # down in QTextControl. If the widget is editable, which it must be if
262 # down in QTextControl. If the widget is editable, which it must be if
265 # we're not executing, the drag will be a Move. The following hack
263 # we're not executing, the drag will be a Move. The following hack
266 # prevents QTextControl from deleting the text by clearing the selection
264 # prevents QTextControl from deleting the text by clearing the selection
267 # when a drag leave event originating from this widget is dispatched.
265 # when a drag leave event originating from this widget is dispatched.
268 # The fact that we have to clear the user's selection is unfortunate,
266 # The fact that we have to clear the user's selection is unfortunate,
269 # but the alternative--trying to prevent Qt from using its hardwired
267 # but the alternative--trying to prevent Qt from using its hardwired
270 # drag logic and writing our own--is worse.
268 # drag logic and writing our own--is worse.
271 elif etype == QtCore.QEvent.DragEnter and \
269 elif etype == QtCore.QEvent.DragEnter and \
272 obj == self._control.viewport() and \
270 obj == self._control.viewport() and \
273 event.source() == self._control.viewport():
271 event.source() == self._control.viewport():
274 self._filter_drag = True
272 self._filter_drag = True
275 elif etype == QtCore.QEvent.DragLeave and \
273 elif etype == QtCore.QEvent.DragLeave and \
276 obj == self._control.viewport() and \
274 obj == self._control.viewport() and \
277 self._filter_drag:
275 self._filter_drag:
278 cursor = self._control.textCursor()
276 cursor = self._control.textCursor()
279 cursor.clearSelection()
277 cursor.clearSelection()
280 self._control.setTextCursor(cursor)
278 self._control.setTextCursor(cursor)
281 self._filter_drag = False
279 self._filter_drag = False
282
280
283 # Ensure that drops are safe.
281 # Ensure that drops are safe.
284 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
282 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
285 cursor = self._control.cursorForPosition(event.pos())
283 cursor = self._control.cursorForPosition(event.pos())
286 if self._in_buffer(cursor.position()):
284 if self._in_buffer(cursor.position()):
287 text = event.mimeData().text()
285 text = event.mimeData().text()
288 self._insert_plain_text_into_buffer(cursor, text)
286 self._insert_plain_text_into_buffer(cursor, text)
289
287
290 # Qt is expecting to get something here--drag and drop occurs in its
288 # Qt is expecting to get something here--drag and drop occurs in its
291 # own event loop. Send a DragLeave event to end it.
289 # own event loop. Send a DragLeave event to end it.
292 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
290 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
293 return True
291 return True
294
292
295 return super(ConsoleWidget, self).eventFilter(obj, event)
293 return super(ConsoleWidget, self).eventFilter(obj, event)
296
294
297 #---------------------------------------------------------------------------
295 #---------------------------------------------------------------------------
298 # 'QWidget' interface
296 # 'QWidget' interface
299 #---------------------------------------------------------------------------
297 #---------------------------------------------------------------------------
300
298
301 def sizeHint(self):
299 def sizeHint(self):
302 """ Reimplemented to suggest a size that is 80 characters wide and
300 """ Reimplemented to suggest a size that is 80 characters wide and
303 25 lines high.
301 25 lines high.
304 """
302 """
305 font_metrics = QtGui.QFontMetrics(self.font)
303 font_metrics = QtGui.QFontMetrics(self.font)
306 margin = (self._control.frameWidth() +
304 margin = (self._control.frameWidth() +
307 self._control.document().documentMargin()) * 2
305 self._control.document().documentMargin()) * 2
308 style = self.style()
306 style = self.style()
309 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
307 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
310
308
311 # Note 1: Despite my best efforts to take the various margins into
309 # Note 1: Despite my best efforts to take the various margins into
312 # account, the width is still coming out a bit too small, so we include
310 # account, the width is still coming out a bit too small, so we include
313 # a fudge factor of one character here.
311 # a fudge factor of one character here.
314 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
312 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
315 # to a Qt bug on certain Mac OS systems where it returns 0.
313 # to a Qt bug on certain Mac OS systems where it returns 0.
316 width = font_metrics.width(' ') * 81 + margin
314 width = font_metrics.width(' ') * 81 + margin
317 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
315 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
318 if self.paging == 'hsplit':
316 if self.paging == 'hsplit':
319 width = width * 2 + splitwidth
317 width = width * 2 + splitwidth
320
318
321 height = font_metrics.height() * 25 + margin
319 height = font_metrics.height() * 25 + margin
322 if self.paging == 'vsplit':
320 if self.paging == 'vsplit':
323 height = height * 2 + splitwidth
321 height = height * 2 + splitwidth
324
322
325 return QtCore.QSize(width, height)
323 return QtCore.QSize(width, height)
326
324
327 #---------------------------------------------------------------------------
325 #---------------------------------------------------------------------------
328 # 'ConsoleWidget' public interface
326 # 'ConsoleWidget' public interface
329 #---------------------------------------------------------------------------
327 #---------------------------------------------------------------------------
330
328
331 def can_copy(self):
329 def can_copy(self):
332 """ Returns whether text can be copied to the clipboard.
330 """ Returns whether text can be copied to the clipboard.
333 """
331 """
334 return self._control.textCursor().hasSelection()
332 return self._control.textCursor().hasSelection()
335
333
336 def can_cut(self):
334 def can_cut(self):
337 """ Returns whether text can be cut to the clipboard.
335 """ Returns whether text can be cut to the clipboard.
338 """
336 """
339 cursor = self._control.textCursor()
337 cursor = self._control.textCursor()
340 return (cursor.hasSelection() and
338 return (cursor.hasSelection() and
341 self._in_buffer(cursor.anchor()) and
339 self._in_buffer(cursor.anchor()) and
342 self._in_buffer(cursor.position()))
340 self._in_buffer(cursor.position()))
343
341
344 def can_paste(self):
342 def can_paste(self):
345 """ Returns whether text can be pasted from the clipboard.
343 """ Returns whether text can be pasted from the clipboard.
346 """
344 """
347 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
345 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
348 return bool(QtGui.QApplication.clipboard().text())
346 return bool(QtGui.QApplication.clipboard().text())
349 return False
347 return False
350
348
351 def can_export(self):
352 """Returns whether we can export. Currently only rich widgets
353 can export html.
354 """
355 return self.kind == "rich"
356
357 def clear(self, keep_input=True):
349 def clear(self, keep_input=True):
358 """ Clear the console.
350 """ Clear the console.
359
351
360 Parameters:
352 Parameters:
361 -----------
353 -----------
362 keep_input : bool, optional (default True)
354 keep_input : bool, optional (default True)
363 If set, restores the old input buffer if a new prompt is written.
355 If set, restores the old input buffer if a new prompt is written.
364 """
356 """
365 if self._executing:
357 if self._executing:
366 self._control.clear()
358 self._control.clear()
367 else:
359 else:
368 if keep_input:
360 if keep_input:
369 input_buffer = self.input_buffer
361 input_buffer = self.input_buffer
370 self._control.clear()
362 self._control.clear()
371 self._show_prompt()
363 self._show_prompt()
372 if keep_input:
364 if keep_input:
373 self.input_buffer = input_buffer
365 self.input_buffer = input_buffer
374
366
375 def copy(self):
367 def copy(self):
376 """ Copy the currently selected text to the clipboard.
368 """ Copy the currently selected text to the clipboard.
377 """
369 """
378 self._control.copy()
370 self._control.copy()
379
371
380 def cut(self):
372 def cut(self):
381 """ Copy the currently selected text to the clipboard and delete it
373 """ Copy the currently selected text to the clipboard and delete it
382 if it's inside the input buffer.
374 if it's inside the input buffer.
383 """
375 """
384 self.copy()
376 self.copy()
385 if self.can_cut():
377 if self.can_cut():
386 self._control.textCursor().removeSelectedText()
378 self._control.textCursor().removeSelectedText()
387
379
388 def execute(self, source=None, hidden=False, interactive=False):
380 def execute(self, source=None, hidden=False, interactive=False):
389 """ Executes source or the input buffer, possibly prompting for more
381 """ Executes source or the input buffer, possibly prompting for more
390 input.
382 input.
391
383
392 Parameters:
384 Parameters:
393 -----------
385 -----------
394 source : str, optional
386 source : str, optional
395
387
396 The source to execute. If not specified, the input buffer will be
388 The source to execute. If not specified, the input buffer will be
397 used. If specified and 'hidden' is False, the input buffer will be
389 used. If specified and 'hidden' is False, the input buffer will be
398 replaced with the source before execution.
390 replaced with the source before execution.
399
391
400 hidden : bool, optional (default False)
392 hidden : bool, optional (default False)
401
393
402 If set, no output will be shown and the prompt will not be modified.
394 If set, no output will be shown and the prompt will not be modified.
403 In other words, it will be completely invisible to the user that
395 In other words, it will be completely invisible to the user that
404 an execution has occurred.
396 an execution has occurred.
405
397
406 interactive : bool, optional (default False)
398 interactive : bool, optional (default False)
407
399
408 Whether the console is to treat the source as having been manually
400 Whether the console is to treat the source as having been manually
409 entered by the user. The effect of this parameter depends on the
401 entered by the user. The effect of this parameter depends on the
410 subclass implementation.
402 subclass implementation.
411
403
412 Raises:
404 Raises:
413 -------
405 -------
414 RuntimeError
406 RuntimeError
415 If incomplete input is given and 'hidden' is True. In this case,
407 If incomplete input is given and 'hidden' is True. In this case,
416 it is not possible to prompt for more input.
408 it is not possible to prompt for more input.
417
409
418 Returns:
410 Returns:
419 --------
411 --------
420 A boolean indicating whether the source was executed.
412 A boolean indicating whether the source was executed.
421 """
413 """
422 # WARNING: The order in which things happen here is very particular, in
414 # WARNING: The order in which things happen here is very particular, in
423 # large part because our syntax highlighting is fragile. If you change
415 # large part because our syntax highlighting is fragile. If you change
424 # something, test carefully!
416 # something, test carefully!
425
417
426 # Decide what to execute.
418 # Decide what to execute.
427 if source is None:
419 if source is None:
428 source = self.input_buffer
420 source = self.input_buffer
429 if not hidden:
421 if not hidden:
430 # A newline is appended later, but it should be considered part
422 # A newline is appended later, but it should be considered part
431 # of the input buffer.
423 # of the input buffer.
432 source += '\n'
424 source += '\n'
433 elif not hidden:
425 elif not hidden:
434 self.input_buffer = source
426 self.input_buffer = source
435
427
436 # Execute the source or show a continuation prompt if it is incomplete.
428 # Execute the source or show a continuation prompt if it is incomplete.
437 complete = self._is_complete(source, interactive)
429 complete = self._is_complete(source, interactive)
438 if hidden:
430 if hidden:
439 if complete:
431 if complete:
440 self._execute(source, hidden)
432 self._execute(source, hidden)
441 else:
433 else:
442 error = 'Incomplete noninteractive input: "%s"'
434 error = 'Incomplete noninteractive input: "%s"'
443 raise RuntimeError(error % source)
435 raise RuntimeError(error % source)
444 else:
436 else:
445 if complete:
437 if complete:
446 self._append_plain_text('\n')
438 self._append_plain_text('\n')
447 self._executing_input_buffer = self.input_buffer
439 self._executing_input_buffer = self.input_buffer
448 self._executing = True
440 self._executing = True
449 self._prompt_finished()
441 self._prompt_finished()
450
442
451 # The maximum block count is only in effect during execution.
443 # The maximum block count is only in effect during execution.
452 # This ensures that _prompt_pos does not become invalid due to
444 # This ensures that _prompt_pos does not become invalid due to
453 # text truncation.
445 # text truncation.
454 self._control.document().setMaximumBlockCount(self.buffer_size)
446 self._control.document().setMaximumBlockCount(self.buffer_size)
455
447
456 # Setting a positive maximum block count will automatically
448 # Setting a positive maximum block count will automatically
457 # disable the undo/redo history, but just to be safe:
449 # disable the undo/redo history, but just to be safe:
458 self._control.setUndoRedoEnabled(False)
450 self._control.setUndoRedoEnabled(False)
459
451
460 # Perform actual execution.
452 # Perform actual execution.
461 self._execute(source, hidden)
453 self._execute(source, hidden)
462
454
463 else:
455 else:
464 # Do this inside an edit block so continuation prompts are
456 # Do this inside an edit block so continuation prompts are
465 # removed seamlessly via undo/redo.
457 # removed seamlessly via undo/redo.
466 cursor = self._get_end_cursor()
458 cursor = self._get_end_cursor()
467 cursor.beginEditBlock()
459 cursor.beginEditBlock()
468 cursor.insertText('\n')
460 cursor.insertText('\n')
469 self._insert_continuation_prompt(cursor)
461 self._insert_continuation_prompt(cursor)
470 cursor.endEditBlock()
462 cursor.endEditBlock()
471
463
472 # Do not do this inside the edit block. It works as expected
464 # Do not do this inside the edit block. It works as expected
473 # when using a QPlainTextEdit control, but does not have an
465 # when using a QPlainTextEdit control, but does not have an
474 # effect when using a QTextEdit. I believe this is a Qt bug.
466 # effect when using a QTextEdit. I believe this is a Qt bug.
475 self._control.moveCursor(QtGui.QTextCursor.End)
467 self._control.moveCursor(QtGui.QTextCursor.End)
476
468
477 return complete
469 return complete
478
470
479 def _get_input_buffer(self):
471 def _get_input_buffer(self):
480 """ The text that the user has entered entered at the current prompt.
472 """ The text that the user has entered entered at the current prompt.
481 """
473 """
482 # If we're executing, the input buffer may not even exist anymore due to
474 # If we're executing, the input buffer may not even exist anymore due to
483 # the limit imposed by 'buffer_size'. Therefore, we store it.
475 # the limit imposed by 'buffer_size'. Therefore, we store it.
484 if self._executing:
476 if self._executing:
485 return self._executing_input_buffer
477 return self._executing_input_buffer
486
478
487 cursor = self._get_end_cursor()
479 cursor = self._get_end_cursor()
488 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
480 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
489 input_buffer = cursor.selection().toPlainText()
481 input_buffer = cursor.selection().toPlainText()
490
482
491 # Strip out continuation prompts.
483 # Strip out continuation prompts.
492 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
484 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
493
485
494 def _set_input_buffer(self, string):
486 def _set_input_buffer(self, string):
495 """ Replaces the text in the input buffer with 'string'.
487 """ Replaces the text in the input buffer with 'string'.
496 """
488 """
497 # For now, it is an error to modify the input buffer during execution.
489 # For now, it is an error to modify the input buffer during execution.
498 if self._executing:
490 if self._executing:
499 raise RuntimeError("Cannot change input buffer during execution.")
491 raise RuntimeError("Cannot change input buffer during execution.")
500
492
501 # Remove old text.
493 # Remove old text.
502 cursor = self._get_end_cursor()
494 cursor = self._get_end_cursor()
503 cursor.beginEditBlock()
495 cursor.beginEditBlock()
504 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
496 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
505 cursor.removeSelectedText()
497 cursor.removeSelectedText()
506
498
507 # Insert new text with continuation prompts.
499 # Insert new text with continuation prompts.
508 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
500 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
509 cursor.endEditBlock()
501 cursor.endEditBlock()
510 self._control.moveCursor(QtGui.QTextCursor.End)
502 self._control.moveCursor(QtGui.QTextCursor.End)
511
503
512 input_buffer = property(_get_input_buffer, _set_input_buffer)
504 input_buffer = property(_get_input_buffer, _set_input_buffer)
513
505
514 def _get_font(self):
506 def _get_font(self):
515 """ The base font being used by the ConsoleWidget.
507 """ The base font being used by the ConsoleWidget.
516 """
508 """
517 return self._control.document().defaultFont()
509 return self._control.document().defaultFont()
518
510
519 def _set_font(self, font):
511 def _set_font(self, font):
520 """ Sets the base font for the ConsoleWidget to the specified QFont.
512 """ Sets the base font for the ConsoleWidget to the specified QFont.
521 """
513 """
522 font_metrics = QtGui.QFontMetrics(font)
514 font_metrics = QtGui.QFontMetrics(font)
523 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
515 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
524
516
525 self._completion_widget.setFont(font)
517 self._completion_widget.setFont(font)
526 self._control.document().setDefaultFont(font)
518 self._control.document().setDefaultFont(font)
527 if self._page_control:
519 if self._page_control:
528 self._page_control.document().setDefaultFont(font)
520 self._page_control.document().setDefaultFont(font)
529
521
530 self.font_changed.emit(font)
522 self.font_changed.emit(font)
531
523
532 font = property(_get_font, _set_font)
524 font = property(_get_font, _set_font)
533
525
534 def paste(self, mode=QtGui.QClipboard.Clipboard):
526 def paste(self, mode=QtGui.QClipboard.Clipboard):
535 """ Paste the contents of the clipboard into the input region.
527 """ Paste the contents of the clipboard into the input region.
536
528
537 Parameters:
529 Parameters:
538 -----------
530 -----------
539 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
531 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
540
532
541 Controls which part of the system clipboard is used. This can be
533 Controls which part of the system clipboard is used. This can be
542 used to access the selection clipboard in X11 and the Find buffer
534 used to access the selection clipboard in X11 and the Find buffer
543 in Mac OS. By default, the regular clipboard is used.
535 in Mac OS. By default, the regular clipboard is used.
544 """
536 """
545 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
537 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
546 # Make sure the paste is safe.
538 # Make sure the paste is safe.
547 self._keep_cursor_in_buffer()
539 self._keep_cursor_in_buffer()
548 cursor = self._control.textCursor()
540 cursor = self._control.textCursor()
549
541
550 # Remove any trailing newline, which confuses the GUI and forces the
542 # Remove any trailing newline, which confuses the GUI and forces the
551 # user to backspace.
543 # user to backspace.
552 text = QtGui.QApplication.clipboard().text(mode).rstrip()
544 text = QtGui.QApplication.clipboard().text(mode).rstrip()
553 self._insert_plain_text_into_buffer(cursor, dedent(text))
545 self._insert_plain_text_into_buffer(cursor, dedent(text))
554
546
555 def print_(self, printer = None):
547 def print_(self, printer = None):
556 """ Print the contents of the ConsoleWidget to the specified QPrinter.
548 """ Print the contents of the ConsoleWidget to the specified QPrinter.
557 """
549 """
558 if (not printer):
550 if (not printer):
559 printer = QtGui.QPrinter()
551 printer = QtGui.QPrinter()
560 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
552 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
561 return
553 return
562 self._control.print_(printer)
554 self._control.print_(printer)
563
555
564 def export(self, parent = None):
565 """Export HTML/XML in various modes from one Dialog."""
566 parent = parent or None # sometimes parent is False
567 dialog = QtGui.QFileDialog(parent, 'Save Console as...')
568 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
569 filters = [
570 'HTML with PNG figures (*.html *.htm)',
571 'XHTML with inline SVG figures (*.xhtml *.xml)'
572 ]
573 dialog.setNameFilters(filters)
574 if self._filename:
575 dialog.selectFile(self._filename)
576 root,ext = os.path.splitext(self._filename)
577 if ext.lower() in ('.xml', '.xhtml'):
578 dialog.selectNameFilter(filters[-1])
579 if dialog.exec_():
580 filename = str(dialog.selectedFiles()[0])
581 self._filename = filename
582 choice = str(dialog.selectedNameFilter())
583
584 if choice.startswith('XHTML'):
585 exporter = self.export_xhtml
586 else:
587 exporter = self.export_html
588
589 try:
590 return exporter(filename)
591 except Exception, e:
592 title = self.window().windowTitle()
593 msg = "Error while saving to: %s\n"%filename+str(e)
594 reply = QtGui.QMessageBox.warning(self, title, msg,
595 QtGui.QMessageBox.Ok, QtGui.QMessageBox.Ok)
596 return None
597
598 def export_html(self, filename):
599 """ Export the contents of the ConsoleWidget as HTML.
600
601 Parameters:
602 -----------
603 filename : str
604 The file to be saved.
605 inline : bool, optional [default True]
606 If True, include images as inline PNGs. Otherwise,
607 include them as links to external PNG files, mimicking
608 web browsers' "Web Page, Complete" behavior.
609 """
610 # N.B. this is overly restrictive, but Qt's output is
611 # predictable...
612 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
613 html = self.fix_html_encoding(
614 str(self._control.toHtml().toUtf8()))
615 if self._png_mode:
616 # preference saved, don't ask again
617 if img_re.search(html):
618 inline = (self._png_mode == 'inline')
619 else:
620 inline = True
621 elif img_re.search(html):
622 # there are images
623 widget = QtGui.QWidget()
624 layout = QtGui.QVBoxLayout(widget)
625 title = self.window().windowTitle()
626 msg = "Exporting HTML with PNGs"
627 info = "Would you like inline PNGs (single large html file) or "+\
628 "external image files?"
629 checkbox = QtGui.QCheckBox("&Don't ask again")
630 checkbox.setShortcut('D')
631 ib = QtGui.QPushButton("&Inline", self)
632 ib.setShortcut('I')
633 eb = QtGui.QPushButton("&External", self)
634 eb.setShortcut('E')
635 box = QtGui.QMessageBox(QtGui.QMessageBox.Question, title, msg)
636 box.setInformativeText(info)
637 box.addButton(ib,QtGui.QMessageBox.NoRole)
638 box.addButton(eb,QtGui.QMessageBox.YesRole)
639 box.setDefaultButton(ib)
640 layout.setSpacing(0)
641 layout.addWidget(box)
642 layout.addWidget(checkbox)
643 widget.setLayout(layout)
644 widget.show()
645 reply = box.exec_()
646 inline = (reply == 0)
647 if checkbox.checkState():
648 # don't ask anymore, always use this choice
649 if inline:
650 self._png_mode='inline'
651 else:
652 self._png_mode='external'
653 else:
654 # no images
655 inline = True
656
657 if inline:
658 path = None
659 else:
660 root,ext = os.path.splitext(filename)
661 path = root+"_files"
662 if os.path.isfile(path):
663 raise OSError("%s exists, but is not a directory."%path)
664
665 f = open(filename, 'w')
666 try:
667 f.write(img_re.sub(
668 lambda x: self.image_tag(x, path = path, format = "png"),
669 html))
670 except Exception, e:
671 f.close()
672 raise e
673 else:
674 f.close()
675 return filename
676
677
678 def export_xhtml(self, filename):
679 """ Export the contents of the ConsoleWidget as XHTML with inline SVGs.
680 """
681 f = open(filename, 'w')
682 try:
683 # N.B. this is overly restrictive, but Qt's output is
684 # predictable...
685 img_re = re.compile(r'<img src="(?P<name>[\d]+)" />')
686 html = str(self._control.toHtml().toUtf8())
687 # Hack to make xhtml header -- note that we are not doing
688 # any check for valid xml
689 offset = html.find("<html>")
690 assert(offset > -1)
691 html = ('<html xmlns="http://www.w3.org/1999/xhtml">\n'+
692 html[offset+6:])
693 # And now declare UTF-8 encoding
694 html = self.fix_html_encoding(html)
695 f.write(img_re.sub(
696 lambda x: self.image_tag(x, path = None, format = "svg"),
697 html))
698 except Exception, e:
699 f.close()
700 raise e
701 else:
702 f.close()
703 return filename
704
705 def fix_html_encoding(self, html):
706 """ Return html string, with a UTF-8 declaration added to <HEAD>.
707
708 Assumes that html is Qt generated and has already been UTF-8 encoded
709 and coerced to a python string. If the expected head element is
710 not found, the given object is returned unmodified.
711
712 This patching is needed for proper rendering of some characters
713 (e.g., indented commands) when viewing exported HTML on a local
714 system (i.e., without seeing an encoding declaration in an HTTP
715 header).
716
717 C.f. http://www.w3.org/International/O-charset for details.
718 """
719 offset = html.find("<head>")
720 if(offset > -1):
721 html = (html[:offset+6]+
722 '\n<meta http-equiv="Content-Type" '+
723 'content="text/html; charset=utf-8" />\n'+
724 html[offset+6:])
725
726 return html
727
728 def image_tag(self, match, path = None, format = "png"):
729 """ Return (X)HTML mark-up for the image-tag given by match.
730
731 Parameters
732 ----------
733 match : re.SRE_Match
734 A match to an HTML image tag as exported by Qt, with
735 match.group("Name") containing the matched image ID.
736
737 path : string|None, optional [default None]
738 If not None, specifies a path to which supporting files
739 may be written (e.g., for linked images).
740 If None, all images are to be included inline.
741
742 format : "png"|"svg", optional [default "png"]
743 Format for returned or referenced images.
744
745 Subclasses supporting image display should override this
746 method.
747 """
748
749 # Default case -- not enough information to generate tag
750 return ""
751
752 def prompt_to_top(self):
556 def prompt_to_top(self):
753 """ Moves the prompt to the top of the viewport.
557 """ Moves the prompt to the top of the viewport.
754 """
558 """
755 if not self._executing:
559 if not self._executing:
756 prompt_cursor = self._get_prompt_cursor()
560 prompt_cursor = self._get_prompt_cursor()
757 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
561 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
758 self._set_cursor(prompt_cursor)
562 self._set_cursor(prompt_cursor)
759 self._set_top_cursor(prompt_cursor)
563 self._set_top_cursor(prompt_cursor)
760
564
761 def redo(self):
565 def redo(self):
762 """ Redo the last operation. If there is no operation to redo, nothing
566 """ Redo the last operation. If there is no operation to redo, nothing
763 happens.
567 happens.
764 """
568 """
765 self._control.redo()
569 self._control.redo()
766
570
767 def reset_font(self):
571 def reset_font(self):
768 """ Sets the font to the default fixed-width font for this platform.
572 """ Sets the font to the default fixed-width font for this platform.
769 """
573 """
770 if sys.platform == 'win32':
574 if sys.platform == 'win32':
771 # Consolas ships with Vista/Win7, fallback to Courier if needed
575 # Consolas ships with Vista/Win7, fallback to Courier if needed
772 family, fallback = 'Consolas', 'Courier'
576 family, fallback = 'Consolas', 'Courier'
773 elif sys.platform == 'darwin':
577 elif sys.platform == 'darwin':
774 # OSX always has Monaco, no need for a fallback
578 # OSX always has Monaco, no need for a fallback
775 family, fallback = 'Monaco', None
579 family, fallback = 'Monaco', None
776 else:
580 else:
777 # FIXME: remove Consolas as a default on Linux once our font
581 # FIXME: remove Consolas as a default on Linux once our font
778 # selections are configurable by the user.
582 # selections are configurable by the user.
779 family, fallback = 'Consolas', 'Monospace'
583 family, fallback = 'Consolas', 'Monospace'
780 font = get_font(family, fallback)
584 font = get_font(family, fallback)
781 font.setPointSize(QtGui.qApp.font().pointSize())
585 font.setPointSize(QtGui.qApp.font().pointSize())
782 font.setStyleHint(QtGui.QFont.TypeWriter)
586 font.setStyleHint(QtGui.QFont.TypeWriter)
783 self._set_font(font)
587 self._set_font(font)
784
588
785 def change_font_size(self, delta):
589 def change_font_size(self, delta):
786 """Change the font size by the specified amount (in points).
590 """Change the font size by the specified amount (in points).
787 """
591 """
788 font = self.font
592 font = self.font
789 font.setPointSize(font.pointSize() + delta)
593 font.setPointSize(font.pointSize() + delta)
790 self._set_font(font)
594 self._set_font(font)
791
595
792 def select_all(self):
596 def select_all(self):
793 """ Selects all the text in the buffer.
597 """ Selects all the text in the buffer.
794 """
598 """
795 self._control.selectAll()
599 self._control.selectAll()
796
600
797 def _get_tab_width(self):
601 def _get_tab_width(self):
798 """ The width (in terms of space characters) for tab characters.
602 """ The width (in terms of space characters) for tab characters.
799 """
603 """
800 return self._tab_width
604 return self._tab_width
801
605
802 def _set_tab_width(self, tab_width):
606 def _set_tab_width(self, tab_width):
803 """ Sets the width (in terms of space characters) for tab characters.
607 """ Sets the width (in terms of space characters) for tab characters.
804 """
608 """
805 font_metrics = QtGui.QFontMetrics(self.font)
609 font_metrics = QtGui.QFontMetrics(self.font)
806 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
610 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
807
611
808 self._tab_width = tab_width
612 self._tab_width = tab_width
809
613
810 tab_width = property(_get_tab_width, _set_tab_width)
614 tab_width = property(_get_tab_width, _set_tab_width)
811
615
812 def undo(self):
616 def undo(self):
813 """ Undo the last operation. If there is no operation to undo, nothing
617 """ Undo the last operation. If there is no operation to undo, nothing
814 happens.
618 happens.
815 """
619 """
816 self._control.undo()
620 self._control.undo()
817
621
818 #---------------------------------------------------------------------------
622 #---------------------------------------------------------------------------
819 # 'ConsoleWidget' abstract interface
623 # 'ConsoleWidget' abstract interface
820 #---------------------------------------------------------------------------
624 #---------------------------------------------------------------------------
821
625
822 def _is_complete(self, source, interactive):
626 def _is_complete(self, source, interactive):
823 """ Returns whether 'source' can be executed. When triggered by an
627 """ Returns whether 'source' can be executed. When triggered by an
824 Enter/Return key press, 'interactive' is True; otherwise, it is
628 Enter/Return key press, 'interactive' is True; otherwise, it is
825 False.
629 False.
826 """
630 """
827 raise NotImplementedError
631 raise NotImplementedError
828
632
829 def _execute(self, source, hidden):
633 def _execute(self, source, hidden):
830 """ Execute 'source'. If 'hidden', do not show any output.
634 """ Execute 'source'. If 'hidden', do not show any output.
831 """
635 """
832 raise NotImplementedError
636 raise NotImplementedError
833
637
834 def _prompt_started_hook(self):
638 def _prompt_started_hook(self):
835 """ Called immediately after a new prompt is displayed.
639 """ Called immediately after a new prompt is displayed.
836 """
640 """
837 pass
641 pass
838
642
839 def _prompt_finished_hook(self):
643 def _prompt_finished_hook(self):
840 """ Called immediately after a prompt is finished, i.e. when some input
644 """ Called immediately after a prompt is finished, i.e. when some input
841 will be processed and a new prompt displayed.
645 will be processed and a new prompt displayed.
842 """
646 """
843 pass
647 pass
844
648
845 def _up_pressed(self):
649 def _up_pressed(self):
846 """ Called when the up key is pressed. Returns whether to continue
650 """ Called when the up key is pressed. Returns whether to continue
847 processing the event.
651 processing the event.
848 """
652 """
849 return True
653 return True
850
654
851 def _down_pressed(self):
655 def _down_pressed(self):
852 """ Called when the down key is pressed. Returns whether to continue
656 """ Called when the down key is pressed. Returns whether to continue
853 processing the event.
657 processing the event.
854 """
658 """
855 return True
659 return True
856
660
857 def _tab_pressed(self):
661 def _tab_pressed(self):
858 """ Called when the tab key is pressed. Returns whether to continue
662 """ Called when the tab key is pressed. Returns whether to continue
859 processing the event.
663 processing the event.
860 """
664 """
861 return False
665 return False
862
666
863 #--------------------------------------------------------------------------
667 #--------------------------------------------------------------------------
864 # 'ConsoleWidget' protected interface
668 # 'ConsoleWidget' protected interface
865 #--------------------------------------------------------------------------
669 #--------------------------------------------------------------------------
866
670
867 def _append_html(self, html):
671 def _append_html(self, html):
868 """ Appends html at the end of the console buffer.
672 """ Appends html at the end of the console buffer.
869 """
673 """
870 cursor = self._get_end_cursor()
674 cursor = self._get_end_cursor()
871 self._insert_html(cursor, html)
675 self._insert_html(cursor, html)
872
676
873 def _append_html_fetching_plain_text(self, html):
677 def _append_html_fetching_plain_text(self, html):
874 """ Appends 'html', then returns the plain text version of it.
678 """ Appends 'html', then returns the plain text version of it.
875 """
679 """
876 cursor = self._get_end_cursor()
680 cursor = self._get_end_cursor()
877 return self._insert_html_fetching_plain_text(cursor, html)
681 return self._insert_html_fetching_plain_text(cursor, html)
878
682
879 def _append_plain_text(self, text):
683 def _append_plain_text(self, text):
880 """ Appends plain text at the end of the console buffer, processing
684 """ Appends plain text at the end of the console buffer, processing
881 ANSI codes if enabled.
685 ANSI codes if enabled.
882 """
686 """
883 cursor = self._get_end_cursor()
687 cursor = self._get_end_cursor()
884 self._insert_plain_text(cursor, text)
688 self._insert_plain_text(cursor, text)
885
689
886 def _append_plain_text_keeping_prompt(self, text):
690 def _append_plain_text_keeping_prompt(self, text):
887 """ Writes 'text' after the current prompt, then restores the old prompt
691 """ Writes 'text' after the current prompt, then restores the old prompt
888 with its old input buffer.
692 with its old input buffer.
889 """
693 """
890 input_buffer = self.input_buffer
694 input_buffer = self.input_buffer
891 self._append_plain_text('\n')
695 self._append_plain_text('\n')
892 self._prompt_finished()
696 self._prompt_finished()
893
697
894 self._append_plain_text(text)
698 self._append_plain_text(text)
895 self._show_prompt()
699 self._show_prompt()
896 self.input_buffer = input_buffer
700 self.input_buffer = input_buffer
897
701
898 def _cancel_text_completion(self):
702 def _cancel_text_completion(self):
899 """ If text completion is progress, cancel it.
703 """ If text completion is progress, cancel it.
900 """
704 """
901 if self._text_completing_pos:
705 if self._text_completing_pos:
902 self._clear_temporary_buffer()
706 self._clear_temporary_buffer()
903 self._text_completing_pos = 0
707 self._text_completing_pos = 0
904
708
905 def _clear_temporary_buffer(self):
709 def _clear_temporary_buffer(self):
906 """ Clears the "temporary text" buffer, i.e. all the text following
710 """ Clears the "temporary text" buffer, i.e. all the text following
907 the prompt region.
711 the prompt region.
908 """
712 """
909 # Select and remove all text below the input buffer.
713 # Select and remove all text below the input buffer.
910 cursor = self._get_prompt_cursor()
714 cursor = self._get_prompt_cursor()
911 prompt = self._continuation_prompt.lstrip()
715 prompt = self._continuation_prompt.lstrip()
912 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
716 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
913 temp_cursor = QtGui.QTextCursor(cursor)
717 temp_cursor = QtGui.QTextCursor(cursor)
914 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
718 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
915 text = temp_cursor.selection().toPlainText().lstrip()
719 text = temp_cursor.selection().toPlainText().lstrip()
916 if not text.startswith(prompt):
720 if not text.startswith(prompt):
917 break
721 break
918 else:
722 else:
919 # We've reached the end of the input buffer and no text follows.
723 # We've reached the end of the input buffer and no text follows.
920 return
724 return
921 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
725 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
922 cursor.movePosition(QtGui.QTextCursor.End,
726 cursor.movePosition(QtGui.QTextCursor.End,
923 QtGui.QTextCursor.KeepAnchor)
727 QtGui.QTextCursor.KeepAnchor)
924 cursor.removeSelectedText()
728 cursor.removeSelectedText()
925
729
926 # After doing this, we have no choice but to clear the undo/redo
730 # After doing this, we have no choice but to clear the undo/redo
927 # history. Otherwise, the text is not "temporary" at all, because it
731 # history. Otherwise, the text is not "temporary" at all, because it
928 # can be recalled with undo/redo. Unfortunately, Qt does not expose
732 # can be recalled with undo/redo. Unfortunately, Qt does not expose
929 # fine-grained control to the undo/redo system.
733 # fine-grained control to the undo/redo system.
930 if self._control.isUndoRedoEnabled():
734 if self._control.isUndoRedoEnabled():
931 self._control.setUndoRedoEnabled(False)
735 self._control.setUndoRedoEnabled(False)
932 self._control.setUndoRedoEnabled(True)
736 self._control.setUndoRedoEnabled(True)
933
737
934 def _complete_with_items(self, cursor, items):
738 def _complete_with_items(self, cursor, items):
935 """ Performs completion with 'items' at the specified cursor location.
739 """ Performs completion with 'items' at the specified cursor location.
936 """
740 """
937 self._cancel_text_completion()
741 self._cancel_text_completion()
938
742
939 if len(items) == 1:
743 if len(items) == 1:
940 cursor.setPosition(self._control.textCursor().position(),
744 cursor.setPosition(self._control.textCursor().position(),
941 QtGui.QTextCursor.KeepAnchor)
745 QtGui.QTextCursor.KeepAnchor)
942 cursor.insertText(items[0])
746 cursor.insertText(items[0])
943
747
944 elif len(items) > 1:
748 elif len(items) > 1:
945 current_pos = self._control.textCursor().position()
749 current_pos = self._control.textCursor().position()
946 prefix = commonprefix(items)
750 prefix = commonprefix(items)
947 if prefix:
751 if prefix:
948 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
752 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
949 cursor.insertText(prefix)
753 cursor.insertText(prefix)
950 current_pos = cursor.position()
754 current_pos = cursor.position()
951
755
952 if self.gui_completion:
756 if self.gui_completion:
953 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
757 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
954 self._completion_widget.show_items(cursor, items)
758 self._completion_widget.show_items(cursor, items)
955 else:
759 else:
956 cursor.beginEditBlock()
760 cursor.beginEditBlock()
957 self._append_plain_text('\n')
761 self._append_plain_text('\n')
958 self._page(self._format_as_columns(items))
762 self._page(self._format_as_columns(items))
959 cursor.endEditBlock()
763 cursor.endEditBlock()
960
764
961 cursor.setPosition(current_pos)
765 cursor.setPosition(current_pos)
962 self._control.moveCursor(QtGui.QTextCursor.End)
766 self._control.moveCursor(QtGui.QTextCursor.End)
963 self._control.setTextCursor(cursor)
767 self._control.setTextCursor(cursor)
964 self._text_completing_pos = current_pos
768 self._text_completing_pos = current_pos
965
769
966 def _context_menu_make(self, pos):
770 def _context_menu_make(self, pos):
967 """ Creates a context menu for the given QPoint (in widget coordinates).
771 """ Creates a context menu for the given QPoint (in widget coordinates).
968 """
772 """
969 menu = QtGui.QMenu(self)
773 menu = QtGui.QMenu(self)
970
774
971 cut_action = menu.addAction('Cut', self.cut)
775 cut_action = menu.addAction('Cut', self.cut)
972 cut_action.setEnabled(self.can_cut())
776 cut_action.setEnabled(self.can_cut())
973 cut_action.setShortcut(QtGui.QKeySequence.Cut)
777 cut_action.setShortcut(QtGui.QKeySequence.Cut)
974
778
975 copy_action = menu.addAction('Copy', self.copy)
779 copy_action = menu.addAction('Copy', self.copy)
976 copy_action.setEnabled(self.can_copy())
780 copy_action.setEnabled(self.can_copy())
977 copy_action.setShortcut(QtGui.QKeySequence.Copy)
781 copy_action.setShortcut(QtGui.QKeySequence.Copy)
978
782
979 paste_action = menu.addAction('Paste', self.paste)
783 paste_action = menu.addAction('Paste', self.paste)
980 paste_action.setEnabled(self.can_paste())
784 paste_action.setEnabled(self.can_paste())
981 paste_action.setShortcut(QtGui.QKeySequence.Paste)
785 paste_action.setShortcut(QtGui.QKeySequence.Paste)
982
786
983 menu.addSeparator()
787 menu.addSeparator()
984 menu.addAction(self._select_all_action)
788 menu.addAction(self._select_all_action)
985
789
986 menu.addSeparator()
790 menu.addSeparator()
987 menu.addAction(self._export_action)
791 menu.addAction(self._export_action)
988 menu.addAction(self._print_action)
792 menu.addAction(self._print_action)
989
793
990 return menu
794 return menu
991
795
992 def _control_key_down(self, modifiers, include_command=False):
796 def _control_key_down(self, modifiers, include_command=False):
993 """ Given a KeyboardModifiers flags object, return whether the Control
797 """ Given a KeyboardModifiers flags object, return whether the Control
994 key is down.
798 key is down.
995
799
996 Parameters:
800 Parameters:
997 -----------
801 -----------
998 include_command : bool, optional (default True)
802 include_command : bool, optional (default True)
999 Whether to treat the Command key as a (mutually exclusive) synonym
803 Whether to treat the Command key as a (mutually exclusive) synonym
1000 for Control when in Mac OS.
804 for Control when in Mac OS.
1001 """
805 """
1002 # Note that on Mac OS, ControlModifier corresponds to the Command key
806 # Note that on Mac OS, ControlModifier corresponds to the Command key
1003 # while MetaModifier corresponds to the Control key.
807 # while MetaModifier corresponds to the Control key.
1004 if sys.platform == 'darwin':
808 if sys.platform == 'darwin':
1005 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
809 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
1006 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
810 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
1007 else:
811 else:
1008 return bool(modifiers & QtCore.Qt.ControlModifier)
812 return bool(modifiers & QtCore.Qt.ControlModifier)
1009
813
1010 def _create_control(self):
814 def _create_control(self):
1011 """ Creates and connects the underlying text widget.
815 """ Creates and connects the underlying text widget.
1012 """
816 """
1013 # Create the underlying control.
817 # Create the underlying control.
1014 if self.kind == 'plain':
818 if self.kind == 'plain':
1015 control = QtGui.QPlainTextEdit()
819 control = QtGui.QPlainTextEdit()
1016 elif self.kind == 'rich':
820 elif self.kind == 'rich':
1017 control = QtGui.QTextEdit()
821 control = QtGui.QTextEdit()
1018 control.setAcceptRichText(False)
822 control.setAcceptRichText(False)
1019
823
1020 # Install event filters. The filter on the viewport is needed for
824 # Install event filters. The filter on the viewport is needed for
1021 # mouse events and drag events.
825 # mouse events and drag events.
1022 control.installEventFilter(self)
826 control.installEventFilter(self)
1023 control.viewport().installEventFilter(self)
827 control.viewport().installEventFilter(self)
1024
828
1025 # Connect signals.
829 # Connect signals.
1026 control.cursorPositionChanged.connect(self._cursor_position_changed)
830 control.cursorPositionChanged.connect(self._cursor_position_changed)
1027 control.customContextMenuRequested.connect(
831 control.customContextMenuRequested.connect(
1028 self._custom_context_menu_requested)
832 self._custom_context_menu_requested)
1029 control.copyAvailable.connect(self.copy_available)
833 control.copyAvailable.connect(self.copy_available)
1030 control.redoAvailable.connect(self.redo_available)
834 control.redoAvailable.connect(self.redo_available)
1031 control.undoAvailable.connect(self.undo_available)
835 control.undoAvailable.connect(self.undo_available)
1032
836
1033 # Hijack the document size change signal to prevent Qt from adjusting
837 # Hijack the document size change signal to prevent Qt from adjusting
1034 # the viewport's scrollbar. We are relying on an implementation detail
838 # the viewport's scrollbar. We are relying on an implementation detail
1035 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
839 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1036 # this functionality we cannot create a nice terminal interface.
840 # this functionality we cannot create a nice terminal interface.
1037 layout = control.document().documentLayout()
841 layout = control.document().documentLayout()
1038 layout.documentSizeChanged.disconnect()
842 layout.documentSizeChanged.disconnect()
1039 layout.documentSizeChanged.connect(self._adjust_scrollbars)
843 layout.documentSizeChanged.connect(self._adjust_scrollbars)
1040
844
1041 # Configure the control.
845 # Configure the control.
1042 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
846 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
1043 control.setReadOnly(True)
847 control.setReadOnly(True)
1044 control.setUndoRedoEnabled(False)
848 control.setUndoRedoEnabled(False)
1045 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
849 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1046 return control
850 return control
1047
851
1048 def _create_page_control(self):
852 def _create_page_control(self):
1049 """ Creates and connects the underlying paging widget.
853 """ Creates and connects the underlying paging widget.
1050 """
854 """
1051 if self.kind == 'plain':
855 if self.kind == 'plain':
1052 control = QtGui.QPlainTextEdit()
856 control = QtGui.QPlainTextEdit()
1053 elif self.kind == 'rich':
857 elif self.kind == 'rich':
1054 control = QtGui.QTextEdit()
858 control = QtGui.QTextEdit()
1055 control.installEventFilter(self)
859 control.installEventFilter(self)
1056 control.setReadOnly(True)
860 control.setReadOnly(True)
1057 control.setUndoRedoEnabled(False)
861 control.setUndoRedoEnabled(False)
1058 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
862 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
1059 return control
863 return control
1060
864
1061 def _event_filter_console_keypress(self, event):
865 def _event_filter_console_keypress(self, event):
1062 """ Filter key events for the underlying text widget to create a
866 """ Filter key events for the underlying text widget to create a
1063 console-like interface.
867 console-like interface.
1064 """
868 """
1065 intercepted = False
869 intercepted = False
1066 cursor = self._control.textCursor()
870 cursor = self._control.textCursor()
1067 position = cursor.position()
871 position = cursor.position()
1068 key = event.key()
872 key = event.key()
1069 ctrl_down = self._control_key_down(event.modifiers())
873 ctrl_down = self._control_key_down(event.modifiers())
1070 alt_down = event.modifiers() & QtCore.Qt.AltModifier
874 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1071 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
875 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
1072
876
1073 #------ Special sequences ----------------------------------------------
877 #------ Special sequences ----------------------------------------------
1074
878
1075 if event.matches(QtGui.QKeySequence.Copy):
879 if event.matches(QtGui.QKeySequence.Copy):
1076 self.copy()
880 self.copy()
1077 intercepted = True
881 intercepted = True
1078
882
1079 elif event.matches(QtGui.QKeySequence.Cut):
883 elif event.matches(QtGui.QKeySequence.Cut):
1080 self.cut()
884 self.cut()
1081 intercepted = True
885 intercepted = True
1082
886
1083 elif event.matches(QtGui.QKeySequence.Paste):
887 elif event.matches(QtGui.QKeySequence.Paste):
1084 self.paste()
888 self.paste()
1085 intercepted = True
889 intercepted = True
1086
890
1087 #------ Special modifier logic -----------------------------------------
891 #------ Special modifier logic -----------------------------------------
1088
892
1089 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
893 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1090 intercepted = True
894 intercepted = True
1091
895
1092 # Special handling when tab completing in text mode.
896 # Special handling when tab completing in text mode.
1093 self._cancel_text_completion()
897 self._cancel_text_completion()
1094
898
1095 if self._in_buffer(position):
899 if self._in_buffer(position):
1096 if self._reading:
900 if self._reading:
1097 self._append_plain_text('\n')
901 self._append_plain_text('\n')
1098 self._reading = False
902 self._reading = False
1099 if self._reading_callback:
903 if self._reading_callback:
1100 self._reading_callback()
904 self._reading_callback()
1101
905
1102 # If the input buffer is a single line or there is only
906 # If the input buffer is a single line or there is only
1103 # whitespace after the cursor, execute. Otherwise, split the
907 # whitespace after the cursor, execute. Otherwise, split the
1104 # line with a continuation prompt.
908 # line with a continuation prompt.
1105 elif not self._executing:
909 elif not self._executing:
1106 cursor.movePosition(QtGui.QTextCursor.End,
910 cursor.movePosition(QtGui.QTextCursor.End,
1107 QtGui.QTextCursor.KeepAnchor)
911 QtGui.QTextCursor.KeepAnchor)
1108 at_end = len(cursor.selectedText().strip()) == 0
912 at_end = len(cursor.selectedText().strip()) == 0
1109 single_line = (self._get_end_cursor().blockNumber() ==
913 single_line = (self._get_end_cursor().blockNumber() ==
1110 self._get_prompt_cursor().blockNumber())
914 self._get_prompt_cursor().blockNumber())
1111 if (at_end or shift_down or single_line) and not ctrl_down:
915 if (at_end or shift_down or single_line) and not ctrl_down:
1112 self.execute(interactive = not shift_down)
916 self.execute(interactive = not shift_down)
1113 else:
917 else:
1114 # Do this inside an edit block for clean undo/redo.
918 # Do this inside an edit block for clean undo/redo.
1115 cursor.beginEditBlock()
919 cursor.beginEditBlock()
1116 cursor.setPosition(position)
920 cursor.setPosition(position)
1117 cursor.insertText('\n')
921 cursor.insertText('\n')
1118 self._insert_continuation_prompt(cursor)
922 self._insert_continuation_prompt(cursor)
1119 cursor.endEditBlock()
923 cursor.endEditBlock()
1120
924
1121 # Ensure that the whole input buffer is visible.
925 # Ensure that the whole input buffer is visible.
1122 # FIXME: This will not be usable if the input buffer is
926 # FIXME: This will not be usable if the input buffer is
1123 # taller than the console widget.
927 # taller than the console widget.
1124 self._control.moveCursor(QtGui.QTextCursor.End)
928 self._control.moveCursor(QtGui.QTextCursor.End)
1125 self._control.setTextCursor(cursor)
929 self._control.setTextCursor(cursor)
1126
930
1127 #------ Control/Cmd modifier -------------------------------------------
931 #------ Control/Cmd modifier -------------------------------------------
1128
932
1129 elif ctrl_down:
933 elif ctrl_down:
1130 if key == QtCore.Qt.Key_G:
934 if key == QtCore.Qt.Key_G:
1131 self._keyboard_quit()
935 self._keyboard_quit()
1132 intercepted = True
936 intercepted = True
1133
937
1134 elif key == QtCore.Qt.Key_K:
938 elif key == QtCore.Qt.Key_K:
1135 if self._in_buffer(position):
939 if self._in_buffer(position):
1136 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
940 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1137 QtGui.QTextCursor.KeepAnchor)
941 QtGui.QTextCursor.KeepAnchor)
1138 if not cursor.hasSelection():
942 if not cursor.hasSelection():
1139 # Line deletion (remove continuation prompt)
943 # Line deletion (remove continuation prompt)
1140 cursor.movePosition(QtGui.QTextCursor.NextBlock,
944 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1141 QtGui.QTextCursor.KeepAnchor)
945 QtGui.QTextCursor.KeepAnchor)
1142 cursor.movePosition(QtGui.QTextCursor.Right,
946 cursor.movePosition(QtGui.QTextCursor.Right,
1143 QtGui.QTextCursor.KeepAnchor,
947 QtGui.QTextCursor.KeepAnchor,
1144 len(self._continuation_prompt))
948 len(self._continuation_prompt))
1145 cursor.removeSelectedText()
949 cursor.removeSelectedText()
1146 intercepted = True
950 intercepted = True
1147
951
1148 elif key == QtCore.Qt.Key_L:
952 elif key == QtCore.Qt.Key_L:
1149 self.prompt_to_top()
953 self.prompt_to_top()
1150 intercepted = True
954 intercepted = True
1151
955
1152 elif key == QtCore.Qt.Key_O:
956 elif key == QtCore.Qt.Key_O:
1153 if self._page_control and self._page_control.isVisible():
957 if self._page_control and self._page_control.isVisible():
1154 self._page_control.setFocus()
958 self._page_control.setFocus()
1155 intercepted = True
959 intercepted = True
1156
960
1157 elif key == QtCore.Qt.Key_Y:
961 elif key == QtCore.Qt.Key_Y:
1158 self.paste()
962 self.paste()
1159 intercepted = True
963 intercepted = True
1160
964
1161 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
965 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1162 intercepted = True
966 intercepted = True
1163
967
1164 elif key == QtCore.Qt.Key_Plus:
968 elif key == QtCore.Qt.Key_Plus:
1165 self.change_font_size(1)
969 self.change_font_size(1)
1166 intercepted = True
970 intercepted = True
1167
971
1168 elif key == QtCore.Qt.Key_Minus:
972 elif key == QtCore.Qt.Key_Minus:
1169 self.change_font_size(-1)
973 self.change_font_size(-1)
1170 intercepted = True
974 intercepted = True
1171
975
1172 #------ Alt modifier ---------------------------------------------------
976 #------ Alt modifier ---------------------------------------------------
1173
977
1174 elif alt_down:
978 elif alt_down:
1175 if key == QtCore.Qt.Key_B:
979 if key == QtCore.Qt.Key_B:
1176 self._set_cursor(self._get_word_start_cursor(position))
980 self._set_cursor(self._get_word_start_cursor(position))
1177 intercepted = True
981 intercepted = True
1178
982
1179 elif key == QtCore.Qt.Key_F:
983 elif key == QtCore.Qt.Key_F:
1180 self._set_cursor(self._get_word_end_cursor(position))
984 self._set_cursor(self._get_word_end_cursor(position))
1181 intercepted = True
985 intercepted = True
1182
986
1183 elif key == QtCore.Qt.Key_Backspace:
987 elif key == QtCore.Qt.Key_Backspace:
1184 cursor = self._get_word_start_cursor(position)
988 cursor = self._get_word_start_cursor(position)
1185 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
989 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1186 cursor.removeSelectedText()
990 cursor.removeSelectedText()
1187 intercepted = True
991 intercepted = True
1188
992
1189 elif key == QtCore.Qt.Key_D:
993 elif key == QtCore.Qt.Key_D:
1190 cursor = self._get_word_end_cursor(position)
994 cursor = self._get_word_end_cursor(position)
1191 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
995 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1192 cursor.removeSelectedText()
996 cursor.removeSelectedText()
1193 intercepted = True
997 intercepted = True
1194
998
1195 elif key == QtCore.Qt.Key_Delete:
999 elif key == QtCore.Qt.Key_Delete:
1196 intercepted = True
1000 intercepted = True
1197
1001
1198 elif key == QtCore.Qt.Key_Greater:
1002 elif key == QtCore.Qt.Key_Greater:
1199 self._control.moveCursor(QtGui.QTextCursor.End)
1003 self._control.moveCursor(QtGui.QTextCursor.End)
1200 intercepted = True
1004 intercepted = True
1201
1005
1202 elif key == QtCore.Qt.Key_Less:
1006 elif key == QtCore.Qt.Key_Less:
1203 self._control.setTextCursor(self._get_prompt_cursor())
1007 self._control.setTextCursor(self._get_prompt_cursor())
1204 intercepted = True
1008 intercepted = True
1205
1009
1206 #------ No modifiers ---------------------------------------------------
1010 #------ No modifiers ---------------------------------------------------
1207
1011
1208 else:
1012 else:
1209 if shift_down:
1013 if shift_down:
1210 anchormode=QtGui.QTextCursor.KeepAnchor
1014 anchormode=QtGui.QTextCursor.KeepAnchor
1211 else:
1015 else:
1212 anchormode=QtGui.QTextCursor.MoveAnchor
1016 anchormode=QtGui.QTextCursor.MoveAnchor
1213
1017
1214 if key == QtCore.Qt.Key_Escape:
1018 if key == QtCore.Qt.Key_Escape:
1215 self._keyboard_quit()
1019 self._keyboard_quit()
1216 intercepted = True
1020 intercepted = True
1217
1021
1218 elif key == QtCore.Qt.Key_Up:
1022 elif key == QtCore.Qt.Key_Up:
1219 if self._reading or not self._up_pressed():
1023 if self._reading or not self._up_pressed():
1220 intercepted = True
1024 intercepted = True
1221 else:
1025 else:
1222 prompt_line = self._get_prompt_cursor().blockNumber()
1026 prompt_line = self._get_prompt_cursor().blockNumber()
1223 intercepted = cursor.blockNumber() <= prompt_line
1027 intercepted = cursor.blockNumber() <= prompt_line
1224
1028
1225 elif key == QtCore.Qt.Key_Down:
1029 elif key == QtCore.Qt.Key_Down:
1226 if self._reading or not self._down_pressed():
1030 if self._reading or not self._down_pressed():
1227 intercepted = True
1031 intercepted = True
1228 else:
1032 else:
1229 end_line = self._get_end_cursor().blockNumber()
1033 end_line = self._get_end_cursor().blockNumber()
1230 intercepted = cursor.blockNumber() == end_line
1034 intercepted = cursor.blockNumber() == end_line
1231
1035
1232 elif key == QtCore.Qt.Key_Tab:
1036 elif key == QtCore.Qt.Key_Tab:
1233 if not self._reading:
1037 if not self._reading:
1234 intercepted = not self._tab_pressed()
1038 intercepted = not self._tab_pressed()
1235
1039
1236 elif key == QtCore.Qt.Key_Left:
1040 elif key == QtCore.Qt.Key_Left:
1237
1041
1238 # Move to the previous line
1042 # Move to the previous line
1239 line, col = cursor.blockNumber(), cursor.columnNumber()
1043 line, col = cursor.blockNumber(), cursor.columnNumber()
1240 if line > self._get_prompt_cursor().blockNumber() and \
1044 if line > self._get_prompt_cursor().blockNumber() and \
1241 col == len(self._continuation_prompt):
1045 col == len(self._continuation_prompt):
1242 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1046 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1243 mode=anchormode)
1047 mode=anchormode)
1244 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1048 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1245 mode=anchormode)
1049 mode=anchormode)
1246 intercepted = True
1050 intercepted = True
1247
1051
1248 # Regular left movement
1052 # Regular left movement
1249 else:
1053 else:
1250 intercepted = not self._in_buffer(position - 1)
1054 intercepted = not self._in_buffer(position - 1)
1251
1055
1252 elif key == QtCore.Qt.Key_Right:
1056 elif key == QtCore.Qt.Key_Right:
1253 original_block_number = cursor.blockNumber()
1057 original_block_number = cursor.blockNumber()
1254 cursor.movePosition(QtGui.QTextCursor.Right,
1058 cursor.movePosition(QtGui.QTextCursor.Right,
1255 mode=anchormode)
1059 mode=anchormode)
1256 if cursor.blockNumber() != original_block_number:
1060 if cursor.blockNumber() != original_block_number:
1257 cursor.movePosition(QtGui.QTextCursor.Right,
1061 cursor.movePosition(QtGui.QTextCursor.Right,
1258 n=len(self._continuation_prompt),
1062 n=len(self._continuation_prompt),
1259 mode=anchormode)
1063 mode=anchormode)
1260 self._set_cursor(cursor)
1064 self._set_cursor(cursor)
1261 intercepted = True
1065 intercepted = True
1262
1066
1263 elif key == QtCore.Qt.Key_Home:
1067 elif key == QtCore.Qt.Key_Home:
1264 start_line = cursor.blockNumber()
1068 start_line = cursor.blockNumber()
1265 if start_line == self._get_prompt_cursor().blockNumber():
1069 if start_line == self._get_prompt_cursor().blockNumber():
1266 start_pos = self._prompt_pos
1070 start_pos = self._prompt_pos
1267 else:
1071 else:
1268 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1072 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1269 QtGui.QTextCursor.KeepAnchor)
1073 QtGui.QTextCursor.KeepAnchor)
1270 start_pos = cursor.position()
1074 start_pos = cursor.position()
1271 start_pos += len(self._continuation_prompt)
1075 start_pos += len(self._continuation_prompt)
1272 cursor.setPosition(position)
1076 cursor.setPosition(position)
1273 if shift_down and self._in_buffer(position):
1077 if shift_down and self._in_buffer(position):
1274 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1078 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1275 else:
1079 else:
1276 cursor.setPosition(start_pos)
1080 cursor.setPosition(start_pos)
1277 self._set_cursor(cursor)
1081 self._set_cursor(cursor)
1278 intercepted = True
1082 intercepted = True
1279
1083
1280 elif key == QtCore.Qt.Key_Backspace:
1084 elif key == QtCore.Qt.Key_Backspace:
1281
1085
1282 # Line deletion (remove continuation prompt)
1086 # Line deletion (remove continuation prompt)
1283 line, col = cursor.blockNumber(), cursor.columnNumber()
1087 line, col = cursor.blockNumber(), cursor.columnNumber()
1284 if not self._reading and \
1088 if not self._reading and \
1285 col == len(self._continuation_prompt) and \
1089 col == len(self._continuation_prompt) and \
1286 line > self._get_prompt_cursor().blockNumber():
1090 line > self._get_prompt_cursor().blockNumber():
1287 cursor.beginEditBlock()
1091 cursor.beginEditBlock()
1288 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1092 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1289 QtGui.QTextCursor.KeepAnchor)
1093 QtGui.QTextCursor.KeepAnchor)
1290 cursor.removeSelectedText()
1094 cursor.removeSelectedText()
1291 cursor.deletePreviousChar()
1095 cursor.deletePreviousChar()
1292 cursor.endEditBlock()
1096 cursor.endEditBlock()
1293 intercepted = True
1097 intercepted = True
1294
1098
1295 # Regular backwards deletion
1099 # Regular backwards deletion
1296 else:
1100 else:
1297 anchor = cursor.anchor()
1101 anchor = cursor.anchor()
1298 if anchor == position:
1102 if anchor == position:
1299 intercepted = not self._in_buffer(position - 1)
1103 intercepted = not self._in_buffer(position - 1)
1300 else:
1104 else:
1301 intercepted = not self._in_buffer(min(anchor, position))
1105 intercepted = not self._in_buffer(min(anchor, position))
1302
1106
1303 elif key == QtCore.Qt.Key_Delete:
1107 elif key == QtCore.Qt.Key_Delete:
1304
1108
1305 # Line deletion (remove continuation prompt)
1109 # Line deletion (remove continuation prompt)
1306 if not self._reading and self._in_buffer(position) and \
1110 if not self._reading and self._in_buffer(position) and \
1307 cursor.atBlockEnd() and not cursor.hasSelection():
1111 cursor.atBlockEnd() and not cursor.hasSelection():
1308 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1112 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1309 QtGui.QTextCursor.KeepAnchor)
1113 QtGui.QTextCursor.KeepAnchor)
1310 cursor.movePosition(QtGui.QTextCursor.Right,
1114 cursor.movePosition(QtGui.QTextCursor.Right,
1311 QtGui.QTextCursor.KeepAnchor,
1115 QtGui.QTextCursor.KeepAnchor,
1312 len(self._continuation_prompt))
1116 len(self._continuation_prompt))
1313 cursor.removeSelectedText()
1117 cursor.removeSelectedText()
1314 intercepted = True
1118 intercepted = True
1315
1119
1316 # Regular forwards deletion:
1120 # Regular forwards deletion:
1317 else:
1121 else:
1318 anchor = cursor.anchor()
1122 anchor = cursor.anchor()
1319 intercepted = (not self._in_buffer(anchor) or
1123 intercepted = (not self._in_buffer(anchor) or
1320 not self._in_buffer(position))
1124 not self._in_buffer(position))
1321
1125
1322 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1126 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1323 # using the keyboard in any part of the buffer.
1127 # using the keyboard in any part of the buffer.
1324 if not self._control_key_down(event.modifiers(), include_command=True):
1128 if not self._control_key_down(event.modifiers(), include_command=True):
1325 self._keep_cursor_in_buffer()
1129 self._keep_cursor_in_buffer()
1326
1130
1327 return intercepted
1131 return intercepted
1328
1132
1329 def _event_filter_page_keypress(self, event):
1133 def _event_filter_page_keypress(self, event):
1330 """ Filter key events for the paging widget to create console-like
1134 """ Filter key events for the paging widget to create console-like
1331 interface.
1135 interface.
1332 """
1136 """
1333 key = event.key()
1137 key = event.key()
1334 ctrl_down = self._control_key_down(event.modifiers())
1138 ctrl_down = self._control_key_down(event.modifiers())
1335 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1139 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1336
1140
1337 if ctrl_down:
1141 if ctrl_down:
1338 if key == QtCore.Qt.Key_O:
1142 if key == QtCore.Qt.Key_O:
1339 self._control.setFocus()
1143 self._control.setFocus()
1340 intercept = True
1144 intercept = True
1341
1145
1342 elif alt_down:
1146 elif alt_down:
1343 if key == QtCore.Qt.Key_Greater:
1147 if key == QtCore.Qt.Key_Greater:
1344 self._page_control.moveCursor(QtGui.QTextCursor.End)
1148 self._page_control.moveCursor(QtGui.QTextCursor.End)
1345 intercepted = True
1149 intercepted = True
1346
1150
1347 elif key == QtCore.Qt.Key_Less:
1151 elif key == QtCore.Qt.Key_Less:
1348 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1152 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1349 intercepted = True
1153 intercepted = True
1350
1154
1351 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1155 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1352 if self._splitter:
1156 if self._splitter:
1353 self._page_control.hide()
1157 self._page_control.hide()
1354 else:
1158 else:
1355 self.layout().setCurrentWidget(self._control)
1159 self.layout().setCurrentWidget(self._control)
1356 return True
1160 return True
1357
1161
1358 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1162 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
1359 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1163 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1360 QtCore.Qt.Key_PageDown,
1164 QtCore.Qt.Key_PageDown,
1361 QtCore.Qt.NoModifier)
1165 QtCore.Qt.NoModifier)
1362 QtGui.qApp.sendEvent(self._page_control, new_event)
1166 QtGui.qApp.sendEvent(self._page_control, new_event)
1363 return True
1167 return True
1364
1168
1365 elif key == QtCore.Qt.Key_Backspace:
1169 elif key == QtCore.Qt.Key_Backspace:
1366 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1170 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1367 QtCore.Qt.Key_PageUp,
1171 QtCore.Qt.Key_PageUp,
1368 QtCore.Qt.NoModifier)
1172 QtCore.Qt.NoModifier)
1369 QtGui.qApp.sendEvent(self._page_control, new_event)
1173 QtGui.qApp.sendEvent(self._page_control, new_event)
1370 return True
1174 return True
1371
1175
1372 return False
1176 return False
1373
1177
1374 def _format_as_columns(self, items, separator=' '):
1178 def _format_as_columns(self, items, separator=' '):
1375 """ Transform a list of strings into a single string with columns.
1179 """ Transform a list of strings into a single string with columns.
1376
1180
1377 Parameters
1181 Parameters
1378 ----------
1182 ----------
1379 items : sequence of strings
1183 items : sequence of strings
1380 The strings to process.
1184 The strings to process.
1381
1185
1382 separator : str, optional [default is two spaces]
1186 separator : str, optional [default is two spaces]
1383 The string that separates columns.
1187 The string that separates columns.
1384
1188
1385 Returns
1189 Returns
1386 -------
1190 -------
1387 The formatted string.
1191 The formatted string.
1388 """
1192 """
1389 # Note: this code is adapted from columnize 0.3.2.
1193 # Note: this code is adapted from columnize 0.3.2.
1390 # See http://code.google.com/p/pycolumnize/
1194 # See http://code.google.com/p/pycolumnize/
1391
1195
1392 # Calculate the number of characters available.
1196 # Calculate the number of characters available.
1393 width = self._control.viewport().width()
1197 width = self._control.viewport().width()
1394 char_width = QtGui.QFontMetrics(self.font).width(' ')
1198 char_width = QtGui.QFontMetrics(self.font).width(' ')
1395 displaywidth = max(10, (width / char_width) - 1)
1199 displaywidth = max(10, (width / char_width) - 1)
1396
1200
1397 # Some degenerate cases.
1201 # Some degenerate cases.
1398 size = len(items)
1202 size = len(items)
1399 if size == 0:
1203 if size == 0:
1400 return '\n'
1204 return '\n'
1401 elif size == 1:
1205 elif size == 1:
1402 return '%s\n' % items[0]
1206 return '%s\n' % items[0]
1403
1207
1404 # Try every row count from 1 upwards
1208 # Try every row count from 1 upwards
1405 array_index = lambda nrows, row, col: nrows*col + row
1209 array_index = lambda nrows, row, col: nrows*col + row
1406 for nrows in range(1, size):
1210 for nrows in range(1, size):
1407 ncols = (size + nrows - 1) // nrows
1211 ncols = (size + nrows - 1) // nrows
1408 colwidths = []
1212 colwidths = []
1409 totwidth = -len(separator)
1213 totwidth = -len(separator)
1410 for col in range(ncols):
1214 for col in range(ncols):
1411 # Get max column width for this column
1215 # Get max column width for this column
1412 colwidth = 0
1216 colwidth = 0
1413 for row in range(nrows):
1217 for row in range(nrows):
1414 i = array_index(nrows, row, col)
1218 i = array_index(nrows, row, col)
1415 if i >= size: break
1219 if i >= size: break
1416 x = items[i]
1220 x = items[i]
1417 colwidth = max(colwidth, len(x))
1221 colwidth = max(colwidth, len(x))
1418 colwidths.append(colwidth)
1222 colwidths.append(colwidth)
1419 totwidth += colwidth + len(separator)
1223 totwidth += colwidth + len(separator)
1420 if totwidth > displaywidth:
1224 if totwidth > displaywidth:
1421 break
1225 break
1422 if totwidth <= displaywidth:
1226 if totwidth <= displaywidth:
1423 break
1227 break
1424
1228
1425 # The smallest number of rows computed and the max widths for each
1229 # The smallest number of rows computed and the max widths for each
1426 # column has been obtained. Now we just have to format each of the rows.
1230 # column has been obtained. Now we just have to format each of the rows.
1427 string = ''
1231 string = ''
1428 for row in range(nrows):
1232 for row in range(nrows):
1429 texts = []
1233 texts = []
1430 for col in range(ncols):
1234 for col in range(ncols):
1431 i = row + nrows*col
1235 i = row + nrows*col
1432 if i >= size:
1236 if i >= size:
1433 texts.append('')
1237 texts.append('')
1434 else:
1238 else:
1435 texts.append(items[i])
1239 texts.append(items[i])
1436 while texts and not texts[-1]:
1240 while texts and not texts[-1]:
1437 del texts[-1]
1241 del texts[-1]
1438 for col in range(len(texts)):
1242 for col in range(len(texts)):
1439 texts[col] = texts[col].ljust(colwidths[col])
1243 texts[col] = texts[col].ljust(colwidths[col])
1440 string += '%s\n' % separator.join(texts)
1244 string += '%s\n' % separator.join(texts)
1441 return string
1245 return string
1442
1246
1443 def _get_block_plain_text(self, block):
1247 def _get_block_plain_text(self, block):
1444 """ Given a QTextBlock, return its unformatted text.
1248 """ Given a QTextBlock, return its unformatted text.
1445 """
1249 """
1446 cursor = QtGui.QTextCursor(block)
1250 cursor = QtGui.QTextCursor(block)
1447 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1251 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1448 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1252 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1449 QtGui.QTextCursor.KeepAnchor)
1253 QtGui.QTextCursor.KeepAnchor)
1450 return cursor.selection().toPlainText()
1254 return cursor.selection().toPlainText()
1451
1255
1452 def _get_cursor(self):
1256 def _get_cursor(self):
1453 """ Convenience method that returns a cursor for the current position.
1257 """ Convenience method that returns a cursor for the current position.
1454 """
1258 """
1455 return self._control.textCursor()
1259 return self._control.textCursor()
1456
1260
1457 def _get_end_cursor(self):
1261 def _get_end_cursor(self):
1458 """ Convenience method that returns a cursor for the last character.
1262 """ Convenience method that returns a cursor for the last character.
1459 """
1263 """
1460 cursor = self._control.textCursor()
1264 cursor = self._control.textCursor()
1461 cursor.movePosition(QtGui.QTextCursor.End)
1265 cursor.movePosition(QtGui.QTextCursor.End)
1462 return cursor
1266 return cursor
1463
1267
1464 def _get_input_buffer_cursor_column(self):
1268 def _get_input_buffer_cursor_column(self):
1465 """ Returns the column of the cursor in the input buffer, excluding the
1269 """ Returns the column of the cursor in the input buffer, excluding the
1466 contribution by the prompt, or -1 if there is no such column.
1270 contribution by the prompt, or -1 if there is no such column.
1467 """
1271 """
1468 prompt = self._get_input_buffer_cursor_prompt()
1272 prompt = self._get_input_buffer_cursor_prompt()
1469 if prompt is None:
1273 if prompt is None:
1470 return -1
1274 return -1
1471 else:
1275 else:
1472 cursor = self._control.textCursor()
1276 cursor = self._control.textCursor()
1473 return cursor.columnNumber() - len(prompt)
1277 return cursor.columnNumber() - len(prompt)
1474
1278
1475 def _get_input_buffer_cursor_line(self):
1279 def _get_input_buffer_cursor_line(self):
1476 """ Returns the text of the line of the input buffer that contains the
1280 """ Returns the text of the line of the input buffer that contains the
1477 cursor, or None if there is no such line.
1281 cursor, or None if there is no such line.
1478 """
1282 """
1479 prompt = self._get_input_buffer_cursor_prompt()
1283 prompt = self._get_input_buffer_cursor_prompt()
1480 if prompt is None:
1284 if prompt is None:
1481 return None
1285 return None
1482 else:
1286 else:
1483 cursor = self._control.textCursor()
1287 cursor = self._control.textCursor()
1484 text = self._get_block_plain_text(cursor.block())
1288 text = self._get_block_plain_text(cursor.block())
1485 return text[len(prompt):]
1289 return text[len(prompt):]
1486
1290
1487 def _get_input_buffer_cursor_prompt(self):
1291 def _get_input_buffer_cursor_prompt(self):
1488 """ Returns the (plain text) prompt for line of the input buffer that
1292 """ Returns the (plain text) prompt for line of the input buffer that
1489 contains the cursor, or None if there is no such line.
1293 contains the cursor, or None if there is no such line.
1490 """
1294 """
1491 if self._executing:
1295 if self._executing:
1492 return None
1296 return None
1493 cursor = self._control.textCursor()
1297 cursor = self._control.textCursor()
1494 if cursor.position() >= self._prompt_pos:
1298 if cursor.position() >= self._prompt_pos:
1495 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1299 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1496 return self._prompt
1300 return self._prompt
1497 else:
1301 else:
1498 return self._continuation_prompt
1302 return self._continuation_prompt
1499 else:
1303 else:
1500 return None
1304 return None
1501
1305
1502 def _get_prompt_cursor(self):
1306 def _get_prompt_cursor(self):
1503 """ Convenience method that returns a cursor for the prompt position.
1307 """ Convenience method that returns a cursor for the prompt position.
1504 """
1308 """
1505 cursor = self._control.textCursor()
1309 cursor = self._control.textCursor()
1506 cursor.setPosition(self._prompt_pos)
1310 cursor.setPosition(self._prompt_pos)
1507 return cursor
1311 return cursor
1508
1312
1509 def _get_selection_cursor(self, start, end):
1313 def _get_selection_cursor(self, start, end):
1510 """ Convenience method that returns a cursor with text selected between
1314 """ Convenience method that returns a cursor with text selected between
1511 the positions 'start' and 'end'.
1315 the positions 'start' and 'end'.
1512 """
1316 """
1513 cursor = self._control.textCursor()
1317 cursor = self._control.textCursor()
1514 cursor.setPosition(start)
1318 cursor.setPosition(start)
1515 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1319 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1516 return cursor
1320 return cursor
1517
1321
1518 def _get_word_start_cursor(self, position):
1322 def _get_word_start_cursor(self, position):
1519 """ Find the start of the word to the left the given position. If a
1323 """ Find the start of the word to the left the given position. If a
1520 sequence of non-word characters precedes the first word, skip over
1324 sequence of non-word characters precedes the first word, skip over
1521 them. (This emulates the behavior of bash, emacs, etc.)
1325 them. (This emulates the behavior of bash, emacs, etc.)
1522 """
1326 """
1523 document = self._control.document()
1327 document = self._control.document()
1524 position -= 1
1328 position -= 1
1525 while position >= self._prompt_pos and \
1329 while position >= self._prompt_pos and \
1526 not is_letter_or_number(document.characterAt(position)):
1330 not is_letter_or_number(document.characterAt(position)):
1527 position -= 1
1331 position -= 1
1528 while position >= self._prompt_pos and \
1332 while position >= self._prompt_pos and \
1529 is_letter_or_number(document.characterAt(position)):
1333 is_letter_or_number(document.characterAt(position)):
1530 position -= 1
1334 position -= 1
1531 cursor = self._control.textCursor()
1335 cursor = self._control.textCursor()
1532 cursor.setPosition(position + 1)
1336 cursor.setPosition(position + 1)
1533 return cursor
1337 return cursor
1534
1338
1535 def _get_word_end_cursor(self, position):
1339 def _get_word_end_cursor(self, position):
1536 """ Find the end of the word to the right the given position. If a
1340 """ Find the end of the word to the right the given position. If a
1537 sequence of non-word characters precedes the first word, skip over
1341 sequence of non-word characters precedes the first word, skip over
1538 them. (This emulates the behavior of bash, emacs, etc.)
1342 them. (This emulates the behavior of bash, emacs, etc.)
1539 """
1343 """
1540 document = self._control.document()
1344 document = self._control.document()
1541 end = self._get_end_cursor().position()
1345 end = self._get_end_cursor().position()
1542 while position < end and \
1346 while position < end and \
1543 not is_letter_or_number(document.characterAt(position)):
1347 not is_letter_or_number(document.characterAt(position)):
1544 position += 1
1348 position += 1
1545 while position < end and \
1349 while position < end and \
1546 is_letter_or_number(document.characterAt(position)):
1350 is_letter_or_number(document.characterAt(position)):
1547 position += 1
1351 position += 1
1548 cursor = self._control.textCursor()
1352 cursor = self._control.textCursor()
1549 cursor.setPosition(position)
1353 cursor.setPosition(position)
1550 return cursor
1354 return cursor
1551
1355
1552 def _insert_continuation_prompt(self, cursor):
1356 def _insert_continuation_prompt(self, cursor):
1553 """ Inserts new continuation prompt using the specified cursor.
1357 """ Inserts new continuation prompt using the specified cursor.
1554 """
1358 """
1555 if self._continuation_prompt_html is None:
1359 if self._continuation_prompt_html is None:
1556 self._insert_plain_text(cursor, self._continuation_prompt)
1360 self._insert_plain_text(cursor, self._continuation_prompt)
1557 else:
1361 else:
1558 self._continuation_prompt = self._insert_html_fetching_plain_text(
1362 self._continuation_prompt = self._insert_html_fetching_plain_text(
1559 cursor, self._continuation_prompt_html)
1363 cursor, self._continuation_prompt_html)
1560
1364
1561 def _insert_html(self, cursor, html):
1365 def _insert_html(self, cursor, html):
1562 """ Inserts HTML using the specified cursor in such a way that future
1366 """ Inserts HTML using the specified cursor in such a way that future
1563 formatting is unaffected.
1367 formatting is unaffected.
1564 """
1368 """
1565 cursor.beginEditBlock()
1369 cursor.beginEditBlock()
1566 cursor.insertHtml(html)
1370 cursor.insertHtml(html)
1567
1371
1568 # After inserting HTML, the text document "remembers" it's in "html
1372 # After inserting HTML, the text document "remembers" it's in "html
1569 # mode", which means that subsequent calls adding plain text will result
1373 # mode", which means that subsequent calls adding plain text will result
1570 # in unwanted formatting, lost tab characters, etc. The following code
1374 # in unwanted formatting, lost tab characters, etc. The following code
1571 # hacks around this behavior, which I consider to be a bug in Qt, by
1375 # hacks around this behavior, which I consider to be a bug in Qt, by
1572 # (crudely) resetting the document's style state.
1376 # (crudely) resetting the document's style state.
1573 cursor.movePosition(QtGui.QTextCursor.Left,
1377 cursor.movePosition(QtGui.QTextCursor.Left,
1574 QtGui.QTextCursor.KeepAnchor)
1378 QtGui.QTextCursor.KeepAnchor)
1575 if cursor.selection().toPlainText() == ' ':
1379 if cursor.selection().toPlainText() == ' ':
1576 cursor.removeSelectedText()
1380 cursor.removeSelectedText()
1577 else:
1381 else:
1578 cursor.movePosition(QtGui.QTextCursor.Right)
1382 cursor.movePosition(QtGui.QTextCursor.Right)
1579 cursor.insertText(' ', QtGui.QTextCharFormat())
1383 cursor.insertText(' ', QtGui.QTextCharFormat())
1580 cursor.endEditBlock()
1384 cursor.endEditBlock()
1581
1385
1582 def _insert_html_fetching_plain_text(self, cursor, html):
1386 def _insert_html_fetching_plain_text(self, cursor, html):
1583 """ Inserts HTML using the specified cursor, then returns its plain text
1387 """ Inserts HTML using the specified cursor, then returns its plain text
1584 version.
1388 version.
1585 """
1389 """
1586 cursor.beginEditBlock()
1390 cursor.beginEditBlock()
1587 cursor.removeSelectedText()
1391 cursor.removeSelectedText()
1588
1392
1589 start = cursor.position()
1393 start = cursor.position()
1590 self._insert_html(cursor, html)
1394 self._insert_html(cursor, html)
1591 end = cursor.position()
1395 end = cursor.position()
1592 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1396 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1593 text = cursor.selection().toPlainText()
1397 text = cursor.selection().toPlainText()
1594
1398
1595 cursor.setPosition(end)
1399 cursor.setPosition(end)
1596 cursor.endEditBlock()
1400 cursor.endEditBlock()
1597 return text
1401 return text
1598
1402
1599 def _insert_plain_text(self, cursor, text):
1403 def _insert_plain_text(self, cursor, text):
1600 """ Inserts plain text using the specified cursor, processing ANSI codes
1404 """ Inserts plain text using the specified cursor, processing ANSI codes
1601 if enabled.
1405 if enabled.
1602 """
1406 """
1603 cursor.beginEditBlock()
1407 cursor.beginEditBlock()
1604 if self.ansi_codes:
1408 if self.ansi_codes:
1605 for substring in self._ansi_processor.split_string(text):
1409 for substring in self._ansi_processor.split_string(text):
1606 for act in self._ansi_processor.actions:
1410 for act in self._ansi_processor.actions:
1607
1411
1608 # Unlike real terminal emulators, we don't distinguish
1412 # Unlike real terminal emulators, we don't distinguish
1609 # between the screen and the scrollback buffer. A screen
1413 # between the screen and the scrollback buffer. A screen
1610 # erase request clears everything.
1414 # erase request clears everything.
1611 if act.action == 'erase' and act.area == 'screen':
1415 if act.action == 'erase' and act.area == 'screen':
1612 cursor.select(QtGui.QTextCursor.Document)
1416 cursor.select(QtGui.QTextCursor.Document)
1613 cursor.removeSelectedText()
1417 cursor.removeSelectedText()
1614
1418
1615 # Simulate a form feed by scrolling just past the last line.
1419 # Simulate a form feed by scrolling just past the last line.
1616 elif act.action == 'scroll' and act.unit == 'page':
1420 elif act.action == 'scroll' and act.unit == 'page':
1617 cursor.insertText('\n')
1421 cursor.insertText('\n')
1618 cursor.endEditBlock()
1422 cursor.endEditBlock()
1619 self._set_top_cursor(cursor)
1423 self._set_top_cursor(cursor)
1620 cursor.joinPreviousEditBlock()
1424 cursor.joinPreviousEditBlock()
1621 cursor.deletePreviousChar()
1425 cursor.deletePreviousChar()
1622
1426
1623 format = self._ansi_processor.get_format()
1427 format = self._ansi_processor.get_format()
1624 cursor.insertText(substring, format)
1428 cursor.insertText(substring, format)
1625 else:
1429 else:
1626 cursor.insertText(text)
1430 cursor.insertText(text)
1627 cursor.endEditBlock()
1431 cursor.endEditBlock()
1628
1432
1629 def _insert_plain_text_into_buffer(self, cursor, text):
1433 def _insert_plain_text_into_buffer(self, cursor, text):
1630 """ Inserts text into the input buffer using the specified cursor (which
1434 """ Inserts text into the input buffer using the specified cursor (which
1631 must be in the input buffer), ensuring that continuation prompts are
1435 must be in the input buffer), ensuring that continuation prompts are
1632 inserted as necessary.
1436 inserted as necessary.
1633 """
1437 """
1634 lines = text.splitlines(True)
1438 lines = text.splitlines(True)
1635 if lines:
1439 if lines:
1636 cursor.beginEditBlock()
1440 cursor.beginEditBlock()
1637 cursor.insertText(lines[0])
1441 cursor.insertText(lines[0])
1638 for line in lines[1:]:
1442 for line in lines[1:]:
1639 if self._continuation_prompt_html is None:
1443 if self._continuation_prompt_html is None:
1640 cursor.insertText(self._continuation_prompt)
1444 cursor.insertText(self._continuation_prompt)
1641 else:
1445 else:
1642 self._continuation_prompt = \
1446 self._continuation_prompt = \
1643 self._insert_html_fetching_plain_text(
1447 self._insert_html_fetching_plain_text(
1644 cursor, self._continuation_prompt_html)
1448 cursor, self._continuation_prompt_html)
1645 cursor.insertText(line)
1449 cursor.insertText(line)
1646 cursor.endEditBlock()
1450 cursor.endEditBlock()
1647
1451
1648 def _in_buffer(self, position=None):
1452 def _in_buffer(self, position=None):
1649 """ Returns whether the current cursor (or, if specified, a position) is
1453 """ Returns whether the current cursor (or, if specified, a position) is
1650 inside the editing region.
1454 inside the editing region.
1651 """
1455 """
1652 cursor = self._control.textCursor()
1456 cursor = self._control.textCursor()
1653 if position is None:
1457 if position is None:
1654 position = cursor.position()
1458 position = cursor.position()
1655 else:
1459 else:
1656 cursor.setPosition(position)
1460 cursor.setPosition(position)
1657 line = cursor.blockNumber()
1461 line = cursor.blockNumber()
1658 prompt_line = self._get_prompt_cursor().blockNumber()
1462 prompt_line = self._get_prompt_cursor().blockNumber()
1659 if line == prompt_line:
1463 if line == prompt_line:
1660 return position >= self._prompt_pos
1464 return position >= self._prompt_pos
1661 elif line > prompt_line:
1465 elif line > prompt_line:
1662 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1466 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1663 prompt_pos = cursor.position() + len(self._continuation_prompt)
1467 prompt_pos = cursor.position() + len(self._continuation_prompt)
1664 return position >= prompt_pos
1468 return position >= prompt_pos
1665 return False
1469 return False
1666
1470
1667 def _keep_cursor_in_buffer(self):
1471 def _keep_cursor_in_buffer(self):
1668 """ Ensures that the cursor is inside the editing region. Returns
1472 """ Ensures that the cursor is inside the editing region. Returns
1669 whether the cursor was moved.
1473 whether the cursor was moved.
1670 """
1474 """
1671 moved = not self._in_buffer()
1475 moved = not self._in_buffer()
1672 if moved:
1476 if moved:
1673 cursor = self._control.textCursor()
1477 cursor = self._control.textCursor()
1674 cursor.movePosition(QtGui.QTextCursor.End)
1478 cursor.movePosition(QtGui.QTextCursor.End)
1675 self._control.setTextCursor(cursor)
1479 self._control.setTextCursor(cursor)
1676 return moved
1480 return moved
1677
1481
1678 def _keyboard_quit(self):
1482 def _keyboard_quit(self):
1679 """ Cancels the current editing task ala Ctrl-G in Emacs.
1483 """ Cancels the current editing task ala Ctrl-G in Emacs.
1680 """
1484 """
1681 if self._text_completing_pos:
1485 if self._text_completing_pos:
1682 self._cancel_text_completion()
1486 self._cancel_text_completion()
1683 else:
1487 else:
1684 self.input_buffer = ''
1488 self.input_buffer = ''
1685
1489
1686 def _page(self, text, html=False):
1490 def _page(self, text, html=False):
1687 """ Displays text using the pager if it exceeds the height of the
1491 """ Displays text using the pager if it exceeds the height of the
1688 viewport.
1492 viewport.
1689
1493
1690 Parameters:
1494 Parameters:
1691 -----------
1495 -----------
1692 html : bool, optional (default False)
1496 html : bool, optional (default False)
1693 If set, the text will be interpreted as HTML instead of plain text.
1497 If set, the text will be interpreted as HTML instead of plain text.
1694 """
1498 """
1695 line_height = QtGui.QFontMetrics(self.font).height()
1499 line_height = QtGui.QFontMetrics(self.font).height()
1696 minlines = self._control.viewport().height() / line_height
1500 minlines = self._control.viewport().height() / line_height
1697 if self.paging != 'none' and \
1501 if self.paging != 'none' and \
1698 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1502 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1699 if self.paging == 'custom':
1503 if self.paging == 'custom':
1700 self.custom_page_requested.emit(text)
1504 self.custom_page_requested.emit(text)
1701 else:
1505 else:
1702 self._page_control.clear()
1506 self._page_control.clear()
1703 cursor = self._page_control.textCursor()
1507 cursor = self._page_control.textCursor()
1704 if html:
1508 if html:
1705 self._insert_html(cursor, text)
1509 self._insert_html(cursor, text)
1706 else:
1510 else:
1707 self._insert_plain_text(cursor, text)
1511 self._insert_plain_text(cursor, text)
1708 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1512 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1709
1513
1710 self._page_control.viewport().resize(self._control.size())
1514 self._page_control.viewport().resize(self._control.size())
1711 if self._splitter:
1515 if self._splitter:
1712 self._page_control.show()
1516 self._page_control.show()
1713 self._page_control.setFocus()
1517 self._page_control.setFocus()
1714 else:
1518 else:
1715 self.layout().setCurrentWidget(self._page_control)
1519 self.layout().setCurrentWidget(self._page_control)
1716 elif html:
1520 elif html:
1717 self._append_plain_html(text)
1521 self._append_plain_html(text)
1718 else:
1522 else:
1719 self._append_plain_text(text)
1523 self._append_plain_text(text)
1720
1524
1721 def _prompt_finished(self):
1525 def _prompt_finished(self):
1722 """ Called immediately after a prompt is finished, i.e. when some input
1526 """ Called immediately after a prompt is finished, i.e. when some input
1723 will be processed and a new prompt displayed.
1527 will be processed and a new prompt displayed.
1724 """
1528 """
1725 # Flush all state from the input splitter so the next round of
1726 # reading input starts with a clean buffer.
1727 self._input_splitter.reset()
1728
1729 self._control.setReadOnly(True)
1529 self._control.setReadOnly(True)
1730 self._prompt_finished_hook()
1530 self._prompt_finished_hook()
1731
1531
1732 def _prompt_started(self):
1532 def _prompt_started(self):
1733 """ Called immediately after a new prompt is displayed.
1533 """ Called immediately after a new prompt is displayed.
1734 """
1534 """
1735 # Temporarily disable the maximum block count to permit undo/redo and
1535 # Temporarily disable the maximum block count to permit undo/redo and
1736 # to ensure that the prompt position does not change due to truncation.
1536 # to ensure that the prompt position does not change due to truncation.
1737 self._control.document().setMaximumBlockCount(0)
1537 self._control.document().setMaximumBlockCount(0)
1738 self._control.setUndoRedoEnabled(True)
1538 self._control.setUndoRedoEnabled(True)
1739
1539
1740 self._control.setReadOnly(False)
1540 self._control.setReadOnly(False)
1741 self._control.moveCursor(QtGui.QTextCursor.End)
1541 self._control.moveCursor(QtGui.QTextCursor.End)
1742 self._executing = False
1542 self._executing = False
1743 self._prompt_started_hook()
1543 self._prompt_started_hook()
1744
1544
1745 def _readline(self, prompt='', callback=None):
1545 def _readline(self, prompt='', callback=None):
1746 """ Reads one line of input from the user.
1546 """ Reads one line of input from the user.
1747
1547
1748 Parameters
1548 Parameters
1749 ----------
1549 ----------
1750 prompt : str, optional
1550 prompt : str, optional
1751 The prompt to print before reading the line.
1551 The prompt to print before reading the line.
1752
1552
1753 callback : callable, optional
1553 callback : callable, optional
1754 A callback to execute with the read line. If not specified, input is
1554 A callback to execute with the read line. If not specified, input is
1755 read *synchronously* and this method does not return until it has
1555 read *synchronously* and this method does not return until it has
1756 been read.
1556 been read.
1757
1557
1758 Returns
1558 Returns
1759 -------
1559 -------
1760 If a callback is specified, returns nothing. Otherwise, returns the
1560 If a callback is specified, returns nothing. Otherwise, returns the
1761 input string with the trailing newline stripped.
1561 input string with the trailing newline stripped.
1762 """
1562 """
1763 if self._reading:
1563 if self._reading:
1764 raise RuntimeError('Cannot read a line. Widget is already reading.')
1564 raise RuntimeError('Cannot read a line. Widget is already reading.')
1765
1565
1766 if not callback and not self.isVisible():
1566 if not callback and not self.isVisible():
1767 # If the user cannot see the widget, this function cannot return.
1567 # If the user cannot see the widget, this function cannot return.
1768 raise RuntimeError('Cannot synchronously read a line if the widget '
1568 raise RuntimeError('Cannot synchronously read a line if the widget '
1769 'is not visible!')
1569 'is not visible!')
1770
1570
1771 self._reading = True
1571 self._reading = True
1772 self._show_prompt(prompt, newline=False)
1572 self._show_prompt(prompt, newline=False)
1773
1573
1774 if callback is None:
1574 if callback is None:
1775 self._reading_callback = None
1575 self._reading_callback = None
1776 while self._reading:
1576 while self._reading:
1777 QtCore.QCoreApplication.processEvents()
1577 QtCore.QCoreApplication.processEvents()
1778 return self.input_buffer.rstrip('\n')
1578 return self.input_buffer.rstrip('\n')
1779
1579
1780 else:
1580 else:
1781 self._reading_callback = lambda: \
1581 self._reading_callback = lambda: \
1782 callback(self.input_buffer.rstrip('\n'))
1582 callback(self.input_buffer.rstrip('\n'))
1783
1583
1784 def _set_continuation_prompt(self, prompt, html=False):
1584 def _set_continuation_prompt(self, prompt, html=False):
1785 """ Sets the continuation prompt.
1585 """ Sets the continuation prompt.
1786
1586
1787 Parameters
1587 Parameters
1788 ----------
1588 ----------
1789 prompt : str
1589 prompt : str
1790 The prompt to show when more input is needed.
1590 The prompt to show when more input is needed.
1791
1591
1792 html : bool, optional (default False)
1592 html : bool, optional (default False)
1793 If set, the prompt will be inserted as formatted HTML. Otherwise,
1593 If set, the prompt will be inserted as formatted HTML. Otherwise,
1794 the prompt will be treated as plain text, though ANSI color codes
1594 the prompt will be treated as plain text, though ANSI color codes
1795 will be handled.
1595 will be handled.
1796 """
1596 """
1797 if html:
1597 if html:
1798 self._continuation_prompt_html = prompt
1598 self._continuation_prompt_html = prompt
1799 else:
1599 else:
1800 self._continuation_prompt = prompt
1600 self._continuation_prompt = prompt
1801 self._continuation_prompt_html = None
1601 self._continuation_prompt_html = None
1802
1602
1803 def _set_cursor(self, cursor):
1603 def _set_cursor(self, cursor):
1804 """ Convenience method to set the current cursor.
1604 """ Convenience method to set the current cursor.
1805 """
1605 """
1806 self._control.setTextCursor(cursor)
1606 self._control.setTextCursor(cursor)
1807
1607
1808 def _set_top_cursor(self, cursor):
1608 def _set_top_cursor(self, cursor):
1809 """ Scrolls the viewport so that the specified cursor is at the top.
1609 """ Scrolls the viewport so that the specified cursor is at the top.
1810 """
1610 """
1811 scrollbar = self._control.verticalScrollBar()
1611 scrollbar = self._control.verticalScrollBar()
1812 scrollbar.setValue(scrollbar.maximum())
1612 scrollbar.setValue(scrollbar.maximum())
1813 original_cursor = self._control.textCursor()
1613 original_cursor = self._control.textCursor()
1814 self._control.setTextCursor(cursor)
1614 self._control.setTextCursor(cursor)
1815 self._control.ensureCursorVisible()
1615 self._control.ensureCursorVisible()
1816 self._control.setTextCursor(original_cursor)
1616 self._control.setTextCursor(original_cursor)
1817
1617
1818 def _show_prompt(self, prompt=None, html=False, newline=True):
1618 def _show_prompt(self, prompt=None, html=False, newline=True):
1819 """ Writes a new prompt at the end of the buffer.
1619 """ Writes a new prompt at the end of the buffer.
1820
1620
1821 Parameters
1621 Parameters
1822 ----------
1622 ----------
1823 prompt : str, optional
1623 prompt : str, optional
1824 The prompt to show. If not specified, the previous prompt is used.
1624 The prompt to show. If not specified, the previous prompt is used.
1825
1625
1826 html : bool, optional (default False)
1626 html : bool, optional (default False)
1827 Only relevant when a prompt is specified. If set, the prompt will
1627 Only relevant when a prompt is specified. If set, the prompt will
1828 be inserted as formatted HTML. Otherwise, the prompt will be treated
1628 be inserted as formatted HTML. Otherwise, the prompt will be treated
1829 as plain text, though ANSI color codes will be handled.
1629 as plain text, though ANSI color codes will be handled.
1830
1630
1831 newline : bool, optional (default True)
1631 newline : bool, optional (default True)
1832 If set, a new line will be written before showing the prompt if
1632 If set, a new line will be written before showing the prompt if
1833 there is not already a newline at the end of the buffer.
1633 there is not already a newline at the end of the buffer.
1834 """
1634 """
1835 # Insert a preliminary newline, if necessary.
1635 # Insert a preliminary newline, if necessary.
1836 if newline:
1636 if newline:
1837 cursor = self._get_end_cursor()
1637 cursor = self._get_end_cursor()
1838 if cursor.position() > 0:
1638 if cursor.position() > 0:
1839 cursor.movePosition(QtGui.QTextCursor.Left,
1639 cursor.movePosition(QtGui.QTextCursor.Left,
1840 QtGui.QTextCursor.KeepAnchor)
1640 QtGui.QTextCursor.KeepAnchor)
1841 if cursor.selection().toPlainText() != '\n':
1641 if cursor.selection().toPlainText() != '\n':
1842 self._append_plain_text('\n')
1642 self._append_plain_text('\n')
1843
1643
1844 # Write the prompt.
1644 # Write the prompt.
1845 self._append_plain_text(self._prompt_sep)
1645 self._append_plain_text(self._prompt_sep)
1846 if prompt is None:
1646 if prompt is None:
1847 if self._prompt_html is None:
1647 if self._prompt_html is None:
1848 self._append_plain_text(self._prompt)
1648 self._append_plain_text(self._prompt)
1849 else:
1649 else:
1850 self._append_html(self._prompt_html)
1650 self._append_html(self._prompt_html)
1851 else:
1651 else:
1852 if html:
1652 if html:
1853 self._prompt = self._append_html_fetching_plain_text(prompt)
1653 self._prompt = self._append_html_fetching_plain_text(prompt)
1854 self._prompt_html = prompt
1654 self._prompt_html = prompt
1855 else:
1655 else:
1856 self._append_plain_text(prompt)
1656 self._append_plain_text(prompt)
1857 self._prompt = prompt
1657 self._prompt = prompt
1858 self._prompt_html = None
1658 self._prompt_html = None
1859
1659
1860 self._prompt_pos = self._get_end_cursor().position()
1660 self._prompt_pos = self._get_end_cursor().position()
1861 self._prompt_started()
1661 self._prompt_started()
1862
1662
1863 #------ Signal handlers ----------------------------------------------------
1663 #------ Signal handlers ----------------------------------------------------
1864
1664
1865 def _adjust_scrollbars(self):
1665 def _adjust_scrollbars(self):
1866 """ Expands the vertical scrollbar beyond the range set by Qt.
1666 """ Expands the vertical scrollbar beyond the range set by Qt.
1867 """
1667 """
1868 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1668 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1869 # and qtextedit.cpp.
1669 # and qtextedit.cpp.
1870 document = self._control.document()
1670 document = self._control.document()
1871 scrollbar = self._control.verticalScrollBar()
1671 scrollbar = self._control.verticalScrollBar()
1872 viewport_height = self._control.viewport().height()
1672 viewport_height = self._control.viewport().height()
1873 if isinstance(self._control, QtGui.QPlainTextEdit):
1673 if isinstance(self._control, QtGui.QPlainTextEdit):
1874 maximum = max(0, document.lineCount() - 1)
1674 maximum = max(0, document.lineCount() - 1)
1875 step = viewport_height / self._control.fontMetrics().lineSpacing()
1675 step = viewport_height / self._control.fontMetrics().lineSpacing()
1876 else:
1676 else:
1877 # QTextEdit does not do line-based layout and blocks will not in
1677 # QTextEdit does not do line-based layout and blocks will not in
1878 # general have the same height. Therefore it does not make sense to
1678 # general have the same height. Therefore it does not make sense to
1879 # attempt to scroll in line height increments.
1679 # attempt to scroll in line height increments.
1880 maximum = document.size().height()
1680 maximum = document.size().height()
1881 step = viewport_height
1681 step = viewport_height
1882 diff = maximum - scrollbar.maximum()
1682 diff = maximum - scrollbar.maximum()
1883 scrollbar.setRange(0, maximum)
1683 scrollbar.setRange(0, maximum)
1884 scrollbar.setPageStep(step)
1684 scrollbar.setPageStep(step)
1685
1885 # Compensate for undesirable scrolling that occurs automatically due to
1686 # Compensate for undesirable scrolling that occurs automatically due to
1886 # maximumBlockCount() text truncation.
1687 # maximumBlockCount() text truncation.
1887 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1688 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1888 scrollbar.setValue(scrollbar.value() + diff)
1689 scrollbar.setValue(scrollbar.value() + diff)
1889
1690
1890 def _cursor_position_changed(self):
1691 def _cursor_position_changed(self):
1891 """ Clears the temporary buffer based on the cursor position.
1692 """ Clears the temporary buffer based on the cursor position.
1892 """
1693 """
1893 if self._text_completing_pos:
1694 if self._text_completing_pos:
1894 document = self._control.document()
1695 document = self._control.document()
1895 if self._text_completing_pos < document.characterCount():
1696 if self._text_completing_pos < document.characterCount():
1896 cursor = self._control.textCursor()
1697 cursor = self._control.textCursor()
1897 pos = cursor.position()
1698 pos = cursor.position()
1898 text_cursor = self._control.textCursor()
1699 text_cursor = self._control.textCursor()
1899 text_cursor.setPosition(self._text_completing_pos)
1700 text_cursor.setPosition(self._text_completing_pos)
1900 if pos < self._text_completing_pos or \
1701 if pos < self._text_completing_pos or \
1901 cursor.blockNumber() > text_cursor.blockNumber():
1702 cursor.blockNumber() > text_cursor.blockNumber():
1902 self._clear_temporary_buffer()
1703 self._clear_temporary_buffer()
1903 self._text_completing_pos = 0
1704 self._text_completing_pos = 0
1904 else:
1705 else:
1905 self._clear_temporary_buffer()
1706 self._clear_temporary_buffer()
1906 self._text_completing_pos = 0
1707 self._text_completing_pos = 0
1907
1708
1908 def _custom_context_menu_requested(self, pos):
1709 def _custom_context_menu_requested(self, pos):
1909 """ Shows a context menu at the given QPoint (in widget coordinates).
1710 """ Shows a context menu at the given QPoint (in widget coordinates).
1910 """
1711 """
1911 menu = self._context_menu_make(pos)
1712 menu = self._context_menu_make(pos)
1912 menu.exec_(self._control.mapToGlobal(pos))
1713 menu.exec_(self._control.mapToGlobal(pos))
@@ -1,598 +1,604 b''
1 from __future__ import print_function
1 from __future__ import print_function
2
2
3 # Standard library imports
3 # Standard library imports
4 from collections import namedtuple
4 from collections import namedtuple
5 import sys
5 import sys
6 import time
6 import time
7
7
8 # System library imports
8 # System library imports
9 from pygments.lexers import PythonLexer
9 from pygments.lexers import PythonLexer
10 from IPython.external.qt import QtCore, QtGui
10 from IPython.external.qt import QtCore, QtGui
11
11
12 # Local imports
12 # Local imports
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 from IPython.core.oinspect import call_tip
14 from IPython.core.oinspect import call_tip
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 from IPython.utils.traitlets import Bool
16 from IPython.utils.traitlets import Bool
17 from bracket_matcher import BracketMatcher
17 from bracket_matcher import BracketMatcher
18 from call_tip_widget import CallTipWidget
18 from call_tip_widget import CallTipWidget
19 from completion_lexer import CompletionLexer
19 from completion_lexer import CompletionLexer
20 from history_console_widget import HistoryConsoleWidget
20 from history_console_widget import HistoryConsoleWidget
21 from pygments_highlighter import PygmentsHighlighter
21 from pygments_highlighter import PygmentsHighlighter
22
22
23
23
24 class FrontendHighlighter(PygmentsHighlighter):
24 class FrontendHighlighter(PygmentsHighlighter):
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 prompts.
26 prompts.
27 """
27 """
28
28
29 def __init__(self, frontend):
29 def __init__(self, frontend):
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 self._current_offset = 0
31 self._current_offset = 0
32 self._frontend = frontend
32 self._frontend = frontend
33 self.highlighting_on = False
33 self.highlighting_on = False
34
34
35 def highlightBlock(self, string):
35 def highlightBlock(self, string):
36 """ Highlight a block of text. Reimplemented to highlight selectively.
36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 """
37 """
38 if not self.highlighting_on:
38 if not self.highlighting_on:
39 return
39 return
40
40
41 # The input to this function is a unicode string that may contain
41 # The input to this function is a unicode string that may contain
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 # the string as plain text so we can compare it.
43 # the string as plain text so we can compare it.
44 current_block = self.currentBlock()
44 current_block = self.currentBlock()
45 string = self._frontend._get_block_plain_text(current_block)
45 string = self._frontend._get_block_plain_text(current_block)
46
46
47 # Decide whether to check for the regular or continuation prompt.
47 # Decide whether to check for the regular or continuation prompt.
48 if current_block.contains(self._frontend._prompt_pos):
48 if current_block.contains(self._frontend._prompt_pos):
49 prompt = self._frontend._prompt
49 prompt = self._frontend._prompt
50 else:
50 else:
51 prompt = self._frontend._continuation_prompt
51 prompt = self._frontend._continuation_prompt
52
52
53 # Don't highlight the part of the string that contains the prompt.
53 # Don't highlight the part of the string that contains the prompt.
54 if string.startswith(prompt):
54 if string.startswith(prompt):
55 self._current_offset = len(prompt)
55 self._current_offset = len(prompt)
56 string = string[len(prompt):]
56 string = string[len(prompt):]
57 else:
57 else:
58 self._current_offset = 0
58 self._current_offset = 0
59
59
60 PygmentsHighlighter.highlightBlock(self, string)
60 PygmentsHighlighter.highlightBlock(self, string)
61
61
62 def rehighlightBlock(self, block):
62 def rehighlightBlock(self, block):
63 """ Reimplemented to temporarily enable highlighting if disabled.
63 """ Reimplemented to temporarily enable highlighting if disabled.
64 """
64 """
65 old = self.highlighting_on
65 old = self.highlighting_on
66 self.highlighting_on = True
66 self.highlighting_on = True
67 super(FrontendHighlighter, self).rehighlightBlock(block)
67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 self.highlighting_on = old
68 self.highlighting_on = old
69
69
70 def setFormat(self, start, count, format):
70 def setFormat(self, start, count, format):
71 """ Reimplemented to highlight selectively.
71 """ Reimplemented to highlight selectively.
72 """
72 """
73 start += self._current_offset
73 start += self._current_offset
74 PygmentsHighlighter.setFormat(self, start, count, format)
74 PygmentsHighlighter.setFormat(self, start, count, format)
75
75
76
76
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 """ A Qt frontend for a generic Python kernel.
78 """ A Qt frontend for a generic Python kernel.
79 """
79 """
80
80
81 # An option and corresponding signal for overriding the default kernel
81 # An option and corresponding signal for overriding the default kernel
82 # interrupt behavior.
82 # interrupt behavior.
83 custom_interrupt = Bool(False)
83 custom_interrupt = Bool(False)
84 custom_interrupt_requested = QtCore.Signal()
84 custom_interrupt_requested = QtCore.Signal()
85
85
86 # An option and corresponding signals for overriding the default kernel
86 # An option and corresponding signals for overriding the default kernel
87 # restart behavior.
87 # restart behavior.
88 custom_restart = Bool(False)
88 custom_restart = Bool(False)
89 custom_restart_kernel_died = QtCore.Signal(float)
89 custom_restart_kernel_died = QtCore.Signal(float)
90 custom_restart_requested = QtCore.Signal()
90 custom_restart_requested = QtCore.Signal()
91
91
92 # Emitted when an 'execute_reply' has been received from the kernel and
92 # Emitted when an 'execute_reply' has been received from the kernel and
93 # processed by the FrontendWidget.
93 # processed by the FrontendWidget.
94 executed = QtCore.Signal(object)
94 executed = QtCore.Signal(object)
95
95
96 # Emitted when an exit request has been received from the kernel.
96 # Emitted when an exit request has been received from the kernel.
97 exit_requested = QtCore.Signal()
97 exit_requested = QtCore.Signal()
98
98
99 # Protected class variables.
99 # Protected class variables.
100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 _input_splitter_class = InputSplitter
103 _input_splitter_class = InputSplitter
104 _local_kernel = False
104 _local_kernel = False
105
105
106 #---------------------------------------------------------------------------
106 #---------------------------------------------------------------------------
107 # 'object' interface
107 # 'object' interface
108 #---------------------------------------------------------------------------
108 #---------------------------------------------------------------------------
109
109
110 def __init__(self, *args, **kw):
110 def __init__(self, *args, **kw):
111 super(FrontendWidget, self).__init__(*args, **kw)
111 super(FrontendWidget, self).__init__(*args, **kw)
112
112
113 # FrontendWidget protected variables.
113 # FrontendWidget protected variables.
114 self._bracket_matcher = BracketMatcher(self._control)
114 self._bracket_matcher = BracketMatcher(self._control)
115 self._call_tip_widget = CallTipWidget(self._control)
115 self._call_tip_widget = CallTipWidget(self._control)
116 self._completion_lexer = CompletionLexer(PythonLexer())
116 self._completion_lexer = CompletionLexer(PythonLexer())
117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
118 self._hidden = False
118 self._hidden = False
119 self._highlighter = FrontendHighlighter(self)
119 self._highlighter = FrontendHighlighter(self)
120 self._input_splitter = self._input_splitter_class(input_mode='cell')
120 self._input_splitter = self._input_splitter_class(input_mode='cell')
121 self._kernel_manager = None
121 self._kernel_manager = None
122 self._request_info = {}
122 self._request_info = {}
123
123
124 # Configure the ConsoleWidget.
124 # Configure the ConsoleWidget.
125 self.tab_width = 4
125 self.tab_width = 4
126 self._set_continuation_prompt('... ')
126 self._set_continuation_prompt('... ')
127
127
128 # Configure the CallTipWidget.
128 # Configure the CallTipWidget.
129 self._call_tip_widget.setFont(self.font)
129 self._call_tip_widget.setFont(self.font)
130 self.font_changed.connect(self._call_tip_widget.setFont)
130 self.font_changed.connect(self._call_tip_widget.setFont)
131
131
132 # Configure actions.
132 # Configure actions.
133 action = self._copy_raw_action
133 action = self._copy_raw_action
134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
135 action.setEnabled(False)
135 action.setEnabled(False)
136 action.setShortcut(QtGui.QKeySequence(key))
136 action.setShortcut(QtGui.QKeySequence(key))
137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
138 action.triggered.connect(self.copy_raw)
138 action.triggered.connect(self.copy_raw)
139 self.copy_available.connect(action.setEnabled)
139 self.copy_available.connect(action.setEnabled)
140 self.addAction(action)
140 self.addAction(action)
141
141
142 # Connect signal handlers.
142 # Connect signal handlers.
143 document = self._control.document()
143 document = self._control.document()
144 document.contentsChange.connect(self._document_contents_change)
144 document.contentsChange.connect(self._document_contents_change)
145
145
146 # set flag for whether we are connected via localhost
146 # Set flag for whether we are connected via localhost.
147 self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel)
147 self._local_kernel = kw.get('local_kernel',
148 FrontendWidget._local_kernel)
148
149
149 #---------------------------------------------------------------------------
150 #---------------------------------------------------------------------------
150 # 'ConsoleWidget' public interface
151 # 'ConsoleWidget' public interface
151 #---------------------------------------------------------------------------
152 #---------------------------------------------------------------------------
152
153
153 def copy(self):
154 def copy(self):
154 """ Copy the currently selected text to the clipboard, removing prompts.
155 """ Copy the currently selected text to the clipboard, removing prompts.
155 """
156 """
156 text = self._control.textCursor().selection().toPlainText()
157 text = self._control.textCursor().selection().toPlainText()
157 if text:
158 if text:
158 lines = map(transform_classic_prompt, text.splitlines())
159 lines = map(transform_classic_prompt, text.splitlines())
159 text = '\n'.join(lines)
160 text = '\n'.join(lines)
160 QtGui.QApplication.clipboard().setText(text)
161 QtGui.QApplication.clipboard().setText(text)
161
162
162 #---------------------------------------------------------------------------
163 #---------------------------------------------------------------------------
163 # 'ConsoleWidget' abstract interface
164 # 'ConsoleWidget' abstract interface
164 #---------------------------------------------------------------------------
165 #---------------------------------------------------------------------------
165
166
166 def _is_complete(self, source, interactive):
167 def _is_complete(self, source, interactive):
167 """ Returns whether 'source' can be completely processed and a new
168 """ Returns whether 'source' can be completely processed and a new
168 prompt created. When triggered by an Enter/Return key press,
169 prompt created. When triggered by an Enter/Return key press,
169 'interactive' is True; otherwise, it is False.
170 'interactive' is True; otherwise, it is False.
170 """
171 """
171 complete = self._input_splitter.push(source)
172 complete = self._input_splitter.push(source)
172 if interactive:
173 if interactive:
173 complete = not self._input_splitter.push_accepts_more()
174 complete = not self._input_splitter.push_accepts_more()
174 return complete
175 return complete
175
176
176 def _execute(self, source, hidden):
177 def _execute(self, source, hidden):
177 """ Execute 'source'. If 'hidden', do not show any output.
178 """ Execute 'source'. If 'hidden', do not show any output.
178
179
179 See parent class :meth:`execute` docstring for full details.
180 See parent class :meth:`execute` docstring for full details.
180 """
181 """
181 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
182 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
182 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
183 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
183 self._hidden = hidden
184 self._hidden = hidden
184
185
185 def _prompt_started_hook(self):
186 def _prompt_started_hook(self):
186 """ Called immediately after a new prompt is displayed.
187 """ Called immediately after a new prompt is displayed.
187 """
188 """
188 if not self._reading:
189 if not self._reading:
189 self._highlighter.highlighting_on = True
190 self._highlighter.highlighting_on = True
190
191
191 def _prompt_finished_hook(self):
192 def _prompt_finished_hook(self):
192 """ Called immediately after a prompt is finished, i.e. when some input
193 """ Called immediately after a prompt is finished, i.e. when some input
193 will be processed and a new prompt displayed.
194 will be processed and a new prompt displayed.
194 """
195 """
196 # Flush all state from the input splitter so the next round of
197 # reading input starts with a clean buffer.
198 self._input_splitter.reset()
199
195 if not self._reading:
200 if not self._reading:
196 self._highlighter.highlighting_on = False
201 self._highlighter.highlighting_on = False
197
202
198 def _tab_pressed(self):
203 def _tab_pressed(self):
199 """ Called when the tab key is pressed. Returns whether to continue
204 """ Called when the tab key is pressed. Returns whether to continue
200 processing the event.
205 processing the event.
201 """
206 """
202 # Perform tab completion if:
207 # Perform tab completion if:
203 # 1) The cursor is in the input buffer.
208 # 1) The cursor is in the input buffer.
204 # 2) There is a non-whitespace character before the cursor.
209 # 2) There is a non-whitespace character before the cursor.
205 text = self._get_input_buffer_cursor_line()
210 text = self._get_input_buffer_cursor_line()
206 if text is None:
211 if text is None:
207 return False
212 return False
208 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
213 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
209 if complete:
214 if complete:
210 self._complete()
215 self._complete()
211 return not complete
216 return not complete
212
217
213 #---------------------------------------------------------------------------
218 #---------------------------------------------------------------------------
214 # 'ConsoleWidget' protected interface
219 # 'ConsoleWidget' protected interface
215 #---------------------------------------------------------------------------
220 #---------------------------------------------------------------------------
216
221
217 def _context_menu_make(self, pos):
222 def _context_menu_make(self, pos):
218 """ Reimplemented to add an action for raw copy.
223 """ Reimplemented to add an action for raw copy.
219 """
224 """
220 menu = super(FrontendWidget, self)._context_menu_make(pos)
225 menu = super(FrontendWidget, self)._context_menu_make(pos)
221 for before_action in menu.actions():
226 for before_action in menu.actions():
222 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
227 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
223 QtGui.QKeySequence.ExactMatch:
228 QtGui.QKeySequence.ExactMatch:
224 menu.insertAction(before_action, self._copy_raw_action)
229 menu.insertAction(before_action, self._copy_raw_action)
225 break
230 break
226 return menu
231 return menu
227
232
228 def _event_filter_console_keypress(self, event):
233 def _event_filter_console_keypress(self, event):
229 """ Reimplemented for execution interruption and smart backspace.
234 """ Reimplemented for execution interruption and smart backspace.
230 """
235 """
231 key = event.key()
236 key = event.key()
232 if self._control_key_down(event.modifiers(), include_command=False):
237 if self._control_key_down(event.modifiers(), include_command=False):
233
238
234 if key == QtCore.Qt.Key_C and self._executing:
239 if key == QtCore.Qt.Key_C and self._executing:
235 self.interrupt_kernel()
240 self.interrupt_kernel()
236 return True
241 return True
237
242
238 elif key == QtCore.Qt.Key_Period:
243 elif key == QtCore.Qt.Key_Period:
239 message = 'Are you sure you want to restart the kernel?'
244 message = 'Are you sure you want to restart the kernel?'
240 self.restart_kernel(message, now=False)
245 self.restart_kernel(message, now=False)
241 return True
246 return True
242
247
243 elif not event.modifiers() & QtCore.Qt.AltModifier:
248 elif not event.modifiers() & QtCore.Qt.AltModifier:
244
249
245 # Smart backspace: remove four characters in one backspace if:
250 # Smart backspace: remove four characters in one backspace if:
246 # 1) everything left of the cursor is whitespace
251 # 1) everything left of the cursor is whitespace
247 # 2) the four characters immediately left of the cursor are spaces
252 # 2) the four characters immediately left of the cursor are spaces
248 if key == QtCore.Qt.Key_Backspace:
253 if key == QtCore.Qt.Key_Backspace:
249 col = self._get_input_buffer_cursor_column()
254 col = self._get_input_buffer_cursor_column()
250 cursor = self._control.textCursor()
255 cursor = self._control.textCursor()
251 if col > 3 and not cursor.hasSelection():
256 if col > 3 and not cursor.hasSelection():
252 text = self._get_input_buffer_cursor_line()[:col]
257 text = self._get_input_buffer_cursor_line()[:col]
253 if text.endswith(' ') and not text.strip():
258 if text.endswith(' ') and not text.strip():
254 cursor.movePosition(QtGui.QTextCursor.Left,
259 cursor.movePosition(QtGui.QTextCursor.Left,
255 QtGui.QTextCursor.KeepAnchor, 4)
260 QtGui.QTextCursor.KeepAnchor, 4)
256 cursor.removeSelectedText()
261 cursor.removeSelectedText()
257 return True
262 return True
258
263
259 return super(FrontendWidget, self)._event_filter_console_keypress(event)
264 return super(FrontendWidget, self)._event_filter_console_keypress(event)
260
265
261 def _insert_continuation_prompt(self, cursor):
266 def _insert_continuation_prompt(self, cursor):
262 """ Reimplemented for auto-indentation.
267 """ Reimplemented for auto-indentation.
263 """
268 """
264 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
269 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
265 cursor.insertText(' ' * self._input_splitter.indent_spaces)
270 cursor.insertText(' ' * self._input_splitter.indent_spaces)
266
271
267 #---------------------------------------------------------------------------
272 #---------------------------------------------------------------------------
268 # 'BaseFrontendMixin' abstract interface
273 # 'BaseFrontendMixin' abstract interface
269 #---------------------------------------------------------------------------
274 #---------------------------------------------------------------------------
270
275
271 def _handle_complete_reply(self, rep):
276 def _handle_complete_reply(self, rep):
272 """ Handle replies for tab completion.
277 """ Handle replies for tab completion.
273 """
278 """
274 cursor = self._get_cursor()
279 cursor = self._get_cursor()
275 info = self._request_info.get('complete')
280 info = self._request_info.get('complete')
276 if info and info.id == rep['parent_header']['msg_id'] and \
281 if info and info.id == rep['parent_header']['msg_id'] and \
277 info.pos == cursor.position():
282 info.pos == cursor.position():
278 text = '.'.join(self._get_context())
283 text = '.'.join(self._get_context())
279 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
284 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
280 self._complete_with_items(cursor, rep['content']['matches'])
285 self._complete_with_items(cursor, rep['content']['matches'])
281
286
282 def _handle_execute_reply(self, msg):
287 def _handle_execute_reply(self, msg):
283 """ Handles replies for code execution.
288 """ Handles replies for code execution.
284 """
289 """
285 info = self._request_info.get('execute')
290 info = self._request_info.get('execute')
286 if info and info.id == msg['parent_header']['msg_id'] and \
291 if info and info.id == msg['parent_header']['msg_id'] and \
287 info.kind == 'user' and not self._hidden:
292 info.kind == 'user' and not self._hidden:
288 # Make sure that all output from the SUB channel has been processed
293 # Make sure that all output from the SUB channel has been processed
289 # before writing a new prompt.
294 # before writing a new prompt.
290 self.kernel_manager.sub_channel.flush()
295 self.kernel_manager.sub_channel.flush()
291
296
292 # Reset the ANSI style information to prevent bad text in stdout
297 # Reset the ANSI style information to prevent bad text in stdout
293 # from messing up our colors. We're not a true terminal so we're
298 # from messing up our colors. We're not a true terminal so we're
294 # allowed to do this.
299 # allowed to do this.
295 if self.ansi_codes:
300 if self.ansi_codes:
296 self._ansi_processor.reset_sgr()
301 self._ansi_processor.reset_sgr()
297
302
298 content = msg['content']
303 content = msg['content']
299 status = content['status']
304 status = content['status']
300 if status == 'ok':
305 if status == 'ok':
301 self._process_execute_ok(msg)
306 self._process_execute_ok(msg)
302 elif status == 'error':
307 elif status == 'error':
303 self._process_execute_error(msg)
308 self._process_execute_error(msg)
304 elif status == 'abort':
309 elif status == 'abort':
305 self._process_execute_abort(msg)
310 self._process_execute_abort(msg)
306
311
307 self._show_interpreter_prompt_for_reply(msg)
312 self._show_interpreter_prompt_for_reply(msg)
308 self.executed.emit(msg)
313 self.executed.emit(msg)
309
314
310 def _handle_input_request(self, msg):
315 def _handle_input_request(self, msg):
311 """ Handle requests for raw_input.
316 """ Handle requests for raw_input.
312 """
317 """
313 if self._hidden:
318 if self._hidden:
314 raise RuntimeError('Request for raw input during hidden execution.')
319 raise RuntimeError('Request for raw input during hidden execution.')
315
320
316 # Make sure that all output from the SUB channel has been processed
321 # Make sure that all output from the SUB channel has been processed
317 # before entering readline mode.
322 # before entering readline mode.
318 self.kernel_manager.sub_channel.flush()
323 self.kernel_manager.sub_channel.flush()
319
324
320 def callback(line):
325 def callback(line):
321 self.kernel_manager.rep_channel.input(line)
326 self.kernel_manager.rep_channel.input(line)
322 self._readline(msg['content']['prompt'], callback=callback)
327 self._readline(msg['content']['prompt'], callback=callback)
323
328
324 def _handle_kernel_died(self, since_last_heartbeat):
329 def _handle_kernel_died(self, since_last_heartbeat):
325 """ Handle the kernel's death by asking if the user wants to restart.
330 """ Handle the kernel's death by asking if the user wants to restart.
326 """
331 """
327 if self.custom_restart:
332 if self.custom_restart:
328 self.custom_restart_kernel_died.emit(since_last_heartbeat)
333 self.custom_restart_kernel_died.emit(since_last_heartbeat)
329 else:
334 else:
330 message = 'The kernel heartbeat has been inactive for %.2f ' \
335 message = 'The kernel heartbeat has been inactive for %.2f ' \
331 'seconds. Do you want to restart the kernel? You may ' \
336 'seconds. Do you want to restart the kernel? You may ' \
332 'first want to check the network connection.' % \
337 'first want to check the network connection.' % \
333 since_last_heartbeat
338 since_last_heartbeat
334 self.restart_kernel(message, now=True)
339 self.restart_kernel(message, now=True)
335
340
336 def _handle_object_info_reply(self, rep):
341 def _handle_object_info_reply(self, rep):
337 """ Handle replies for call tips.
342 """ Handle replies for call tips.
338 """
343 """
339 cursor = self._get_cursor()
344 cursor = self._get_cursor()
340 info = self._request_info.get('call_tip')
345 info = self._request_info.get('call_tip')
341 if info and info.id == rep['parent_header']['msg_id'] and \
346 if info and info.id == rep['parent_header']['msg_id'] and \
342 info.pos == cursor.position():
347 info.pos == cursor.position():
343 # Get the information for a call tip. For now we format the call
348 # Get the information for a call tip. For now we format the call
344 # line as string, later we can pass False to format_call and
349 # line as string, later we can pass False to format_call and
345 # syntax-highlight it ourselves for nicer formatting in the
350 # syntax-highlight it ourselves for nicer formatting in the
346 # calltip.
351 # calltip.
347 call_info, doc = call_tip(rep['content'], format_call=True)
352 call_info, doc = call_tip(rep['content'], format_call=True)
348 if call_info or doc:
353 if call_info or doc:
349 self._call_tip_widget.show_call_info(call_info, doc)
354 self._call_tip_widget.show_call_info(call_info, doc)
350
355
351 def _handle_pyout(self, msg):
356 def _handle_pyout(self, msg):
352 """ Handle display hook output.
357 """ Handle display hook output.
353 """
358 """
354 if not self._hidden and self._is_from_this_session(msg):
359 if not self._hidden and self._is_from_this_session(msg):
355 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
360 self._append_plain_text(msg['content']['data']['text/plain'] + '\n')
356
361
357 def _handle_stream(self, msg):
362 def _handle_stream(self, msg):
358 """ Handle stdout, stderr, and stdin.
363 """ Handle stdout, stderr, and stdin.
359 """
364 """
360 if not self._hidden and self._is_from_this_session(msg):
365 if not self._hidden and self._is_from_this_session(msg):
361 # Most consoles treat tabs as being 8 space characters. Convert tabs
366 # Most consoles treat tabs as being 8 space characters. Convert tabs
362 # to spaces so that output looks as expected regardless of this
367 # to spaces so that output looks as expected regardless of this
363 # widget's tab width.
368 # widget's tab width.
364 text = msg['content']['data'].expandtabs(8)
369 text = msg['content']['data'].expandtabs(8)
365
370
366 self._append_plain_text(text)
371 self._append_plain_text(text)
367 self._control.moveCursor(QtGui.QTextCursor.End)
372 self._control.moveCursor(QtGui.QTextCursor.End)
368
373
369 def _handle_shutdown_reply(self, msg):
374 def _handle_shutdown_reply(self, msg):
370 """ Handle shutdown signal, only if from other console.
375 """ Handle shutdown signal, only if from other console.
371 """
376 """
372 if not self._hidden and not self._is_from_this_session(msg):
377 if not self._hidden and not self._is_from_this_session(msg):
373 if self._local_kernel:
378 if self._local_kernel:
374 if not msg['content']['restart']:
379 if not msg['content']['restart']:
375 sys.exit(0)
380 sys.exit(0)
376 else:
381 else:
377 # we just got notified of a restart!
382 # we just got notified of a restart!
378 time.sleep(0.25) # wait 1/4 sec to reset
383 time.sleep(0.25) # wait 1/4 sec to reset
379 # lest the request for a new prompt
384 # lest the request for a new prompt
380 # goes to the old kernel
385 # goes to the old kernel
381 self.reset()
386 self.reset()
382 else: # remote kernel, prompt on Kernel shutdown/reset
387 else: # remote kernel, prompt on Kernel shutdown/reset
383 title = self.window().windowTitle()
388 title = self.window().windowTitle()
384 if not msg['content']['restart']:
389 if not msg['content']['restart']:
385 reply = QtGui.QMessageBox.question(self, title,
390 reply = QtGui.QMessageBox.question(self, title,
386 "Kernel has been shutdown permanently. Close the Console?",
391 "Kernel has been shutdown permanently. "
392 "Close the Console?",
387 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
388 if reply == QtGui.QMessageBox.Yes:
394 if reply == QtGui.QMessageBox.Yes:
389 sys.exit(0)
395 sys.exit(0)
390 else:
396 else:
391 reply = QtGui.QMessageBox.question(self, title,
397 reply = QtGui.QMessageBox.question(self, title,
392 "Kernel has been reset. Clear the Console?",
398 "Kernel has been reset. Clear the Console?",
393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
399 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
394 if reply == QtGui.QMessageBox.Yes:
400 if reply == QtGui.QMessageBox.Yes:
395 time.sleep(0.25) # wait 1/4 sec to reset
401 time.sleep(0.25) # wait 1/4 sec to reset
396 # lest the request for a new prompt
402 # lest the request for a new prompt
397 # goes to the old kernel
403 # goes to the old kernel
398 self.reset()
404 self.reset()
399
405
400 def _started_channels(self):
406 def _started_channels(self):
401 """ Called when the KernelManager channels have started listening or
407 """ Called when the KernelManager channels have started listening or
402 when the frontend is assigned an already listening KernelManager.
408 when the frontend is assigned an already listening KernelManager.
403 """
409 """
404 self.reset()
410 self.reset()
405
411
406 #---------------------------------------------------------------------------
412 #---------------------------------------------------------------------------
407 # 'FrontendWidget' public interface
413 # 'FrontendWidget' public interface
408 #---------------------------------------------------------------------------
414 #---------------------------------------------------------------------------
409
415
410 def copy_raw(self):
416 def copy_raw(self):
411 """ Copy the currently selected text to the clipboard without attempting
417 """ Copy the currently selected text to the clipboard without attempting
412 to remove prompts or otherwise alter the text.
418 to remove prompts or otherwise alter the text.
413 """
419 """
414 self._control.copy()
420 self._control.copy()
415
421
416 def execute_file(self, path, hidden=False):
422 def execute_file(self, path, hidden=False):
417 """ Attempts to execute file with 'path'. If 'hidden', no output is
423 """ Attempts to execute file with 'path'. If 'hidden', no output is
418 shown.
424 shown.
419 """
425 """
420 self.execute('execfile("%s")' % path, hidden=hidden)
426 self.execute('execfile("%s")' % path, hidden=hidden)
421
427
422 def interrupt_kernel(self):
428 def interrupt_kernel(self):
423 """ Attempts to interrupt the running kernel.
429 """ Attempts to interrupt the running kernel.
424 """
430 """
425 if self.custom_interrupt:
431 if self.custom_interrupt:
426 self.custom_interrupt_requested.emit()
432 self.custom_interrupt_requested.emit()
427 elif self.kernel_manager.has_kernel:
433 elif self.kernel_manager.has_kernel:
428 self.kernel_manager.interrupt_kernel()
434 self.kernel_manager.interrupt_kernel()
429 else:
435 else:
430 self._append_plain_text('Kernel process is either remote or '
436 self._append_plain_text('Kernel process is either remote or '
431 'unspecified. Cannot interrupt.\n')
437 'unspecified. Cannot interrupt.\n')
432
438
433 def reset(self):
439 def reset(self):
434 """ Resets the widget to its initial state. Similar to ``clear``, but
440 """ Resets the widget to its initial state. Similar to ``clear``, but
435 also re-writes the banner and aborts execution if necessary.
441 also re-writes the banner and aborts execution if necessary.
436 """
442 """
437 if self._executing:
443 if self._executing:
438 self._executing = False
444 self._executing = False
439 self._request_info['execute'] = None
445 self._request_info['execute'] = None
440 self._reading = False
446 self._reading = False
441 self._highlighter.highlighting_on = False
447 self._highlighter.highlighting_on = False
442
448
443 self._control.clear()
449 self._control.clear()
444 self._append_plain_text(self._get_banner())
450 self._append_plain_text(self._get_banner())
445 self._show_interpreter_prompt()
451 self._show_interpreter_prompt()
446
452
447 def restart_kernel(self, message, now=False):
453 def restart_kernel(self, message, now=False):
448 """ Attempts to restart the running kernel.
454 """ Attempts to restart the running kernel.
449 """
455 """
450 # FIXME: now should be configurable via a checkbox in the dialog. Right
456 # FIXME: now should be configurable via a checkbox in the dialog. Right
451 # now at least the heartbeat path sets it to True and the manual restart
457 # now at least the heartbeat path sets it to True and the manual restart
452 # to False. But those should just be the pre-selected states of a
458 # to False. But those should just be the pre-selected states of a
453 # checkbox that the user could override if so desired. But I don't know
459 # checkbox that the user could override if so desired. But I don't know
454 # enough Qt to go implementing the checkbox now.
460 # enough Qt to go implementing the checkbox now.
455
461
456 if self.custom_restart:
462 if self.custom_restart:
457 self.custom_restart_requested.emit()
463 self.custom_restart_requested.emit()
458
464
459 elif self.kernel_manager.has_kernel:
465 elif self.kernel_manager.has_kernel:
460 # Pause the heart beat channel to prevent further warnings.
466 # Pause the heart beat channel to prevent further warnings.
461 self.kernel_manager.hb_channel.pause()
467 self.kernel_manager.hb_channel.pause()
462
468
463 # Prompt the user to restart the kernel. Un-pause the heartbeat if
469 # Prompt the user to restart the kernel. Un-pause the heartbeat if
464 # they decline. (If they accept, the heartbeat will be un-paused
470 # they decline. (If they accept, the heartbeat will be un-paused
465 # automatically when the kernel is restarted.)
471 # automatically when the kernel is restarted.)
466 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
472 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
467 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
473 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
468 message, buttons)
474 message, buttons)
469 if result == QtGui.QMessageBox.Yes:
475 if result == QtGui.QMessageBox.Yes:
470 try:
476 try:
471 self.kernel_manager.restart_kernel(now=now)
477 self.kernel_manager.restart_kernel(now=now)
472 except RuntimeError:
478 except RuntimeError:
473 self._append_plain_text('Kernel started externally. '
479 self._append_plain_text('Kernel started externally. '
474 'Cannot restart.\n')
480 'Cannot restart.\n')
475 else:
481 else:
476 self.reset()
482 self.reset()
477 else:
483 else:
478 self.kernel_manager.hb_channel.unpause()
484 self.kernel_manager.hb_channel.unpause()
479
485
480 else:
486 else:
481 self._append_plain_text('Kernel process is either remote or '
487 self._append_plain_text('Kernel process is either remote or '
482 'unspecified. Cannot restart.\n')
488 'unspecified. Cannot restart.\n')
483
489
484 #---------------------------------------------------------------------------
490 #---------------------------------------------------------------------------
485 # 'FrontendWidget' protected interface
491 # 'FrontendWidget' protected interface
486 #---------------------------------------------------------------------------
492 #---------------------------------------------------------------------------
487
493
488 def _call_tip(self):
494 def _call_tip(self):
489 """ Shows a call tip, if appropriate, at the current cursor location.
495 """ Shows a call tip, if appropriate, at the current cursor location.
490 """
496 """
491 # Decide if it makes sense to show a call tip
497 # Decide if it makes sense to show a call tip
492 cursor = self._get_cursor()
498 cursor = self._get_cursor()
493 cursor.movePosition(QtGui.QTextCursor.Left)
499 cursor.movePosition(QtGui.QTextCursor.Left)
494 if cursor.document().characterAt(cursor.position()) != '(':
500 if cursor.document().characterAt(cursor.position()) != '(':
495 return False
501 return False
496 context = self._get_context(cursor)
502 context = self._get_context(cursor)
497 if not context:
503 if not context:
498 return False
504 return False
499
505
500 # Send the metadata request to the kernel
506 # Send the metadata request to the kernel
501 name = '.'.join(context)
507 name = '.'.join(context)
502 msg_id = self.kernel_manager.xreq_channel.object_info(name)
508 msg_id = self.kernel_manager.xreq_channel.object_info(name)
503 pos = self._get_cursor().position()
509 pos = self._get_cursor().position()
504 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
510 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
505 return True
511 return True
506
512
507 def _complete(self):
513 def _complete(self):
508 """ Performs completion at the current cursor location.
514 """ Performs completion at the current cursor location.
509 """
515 """
510 context = self._get_context()
516 context = self._get_context()
511 if context:
517 if context:
512 # Send the completion request to the kernel
518 # Send the completion request to the kernel
513 msg_id = self.kernel_manager.xreq_channel.complete(
519 msg_id = self.kernel_manager.xreq_channel.complete(
514 '.'.join(context), # text
520 '.'.join(context), # text
515 self._get_input_buffer_cursor_line(), # line
521 self._get_input_buffer_cursor_line(), # line
516 self._get_input_buffer_cursor_column(), # cursor_pos
522 self._get_input_buffer_cursor_column(), # cursor_pos
517 self.input_buffer) # block
523 self.input_buffer) # block
518 pos = self._get_cursor().position()
524 pos = self._get_cursor().position()
519 info = self._CompletionRequest(msg_id, pos)
525 info = self._CompletionRequest(msg_id, pos)
520 self._request_info['complete'] = info
526 self._request_info['complete'] = info
521
527
522 def _get_banner(self):
528 def _get_banner(self):
523 """ Gets a banner to display at the beginning of a session.
529 """ Gets a banner to display at the beginning of a session.
524 """
530 """
525 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
531 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
526 '"license" for more information.'
532 '"license" for more information.'
527 return banner % (sys.version, sys.platform)
533 return banner % (sys.version, sys.platform)
528
534
529 def _get_context(self, cursor=None):
535 def _get_context(self, cursor=None):
530 """ Gets the context for the specified cursor (or the current cursor
536 """ Gets the context for the specified cursor (or the current cursor
531 if none is specified).
537 if none is specified).
532 """
538 """
533 if cursor is None:
539 if cursor is None:
534 cursor = self._get_cursor()
540 cursor = self._get_cursor()
535 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
541 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
536 QtGui.QTextCursor.KeepAnchor)
542 QtGui.QTextCursor.KeepAnchor)
537 text = cursor.selection().toPlainText()
543 text = cursor.selection().toPlainText()
538 return self._completion_lexer.get_context(text)
544 return self._completion_lexer.get_context(text)
539
545
540 def _process_execute_abort(self, msg):
546 def _process_execute_abort(self, msg):
541 """ Process a reply for an aborted execution request.
547 """ Process a reply for an aborted execution request.
542 """
548 """
543 self._append_plain_text("ERROR: execution aborted\n")
549 self._append_plain_text("ERROR: execution aborted\n")
544
550
545 def _process_execute_error(self, msg):
551 def _process_execute_error(self, msg):
546 """ Process a reply for an execution request that resulted in an error.
552 """ Process a reply for an execution request that resulted in an error.
547 """
553 """
548 content = msg['content']
554 content = msg['content']
549 # If a SystemExit is passed along, this means exit() was called - also
555 # If a SystemExit is passed along, this means exit() was called - also
550 # all the ipython %exit magic syntax of '-k' to be used to keep
556 # all the ipython %exit magic syntax of '-k' to be used to keep
551 # the kernel running
557 # the kernel running
552 if content['ename']=='SystemExit':
558 if content['ename']=='SystemExit':
553 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
559 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
554 self._keep_kernel_on_exit = keepkernel
560 self._keep_kernel_on_exit = keepkernel
555 self.exit_requested.emit()
561 self.exit_requested.emit()
556 else:
562 else:
557 traceback = ''.join(content['traceback'])
563 traceback = ''.join(content['traceback'])
558 self._append_plain_text(traceback)
564 self._append_plain_text(traceback)
559
565
560 def _process_execute_ok(self, msg):
566 def _process_execute_ok(self, msg):
561 """ Process a reply for a successful execution equest.
567 """ Process a reply for a successful execution equest.
562 """
568 """
563 payload = msg['content']['payload']
569 payload = msg['content']['payload']
564 for item in payload:
570 for item in payload:
565 if not self._process_execute_payload(item):
571 if not self._process_execute_payload(item):
566 warning = 'Warning: received unknown payload of type %s'
572 warning = 'Warning: received unknown payload of type %s'
567 print(warning % repr(item['source']))
573 print(warning % repr(item['source']))
568
574
569 def _process_execute_payload(self, item):
575 def _process_execute_payload(self, item):
570 """ Process a single payload item from the list of payload items in an
576 """ Process a single payload item from the list of payload items in an
571 execution reply. Returns whether the payload was handled.
577 execution reply. Returns whether the payload was handled.
572 """
578 """
573 # The basic FrontendWidget doesn't handle payloads, as they are a
579 # The basic FrontendWidget doesn't handle payloads, as they are a
574 # mechanism for going beyond the standard Python interpreter model.
580 # mechanism for going beyond the standard Python interpreter model.
575 return False
581 return False
576
582
577 def _show_interpreter_prompt(self):
583 def _show_interpreter_prompt(self):
578 """ Shows a prompt for the interpreter.
584 """ Shows a prompt for the interpreter.
579 """
585 """
580 self._show_prompt('>>> ')
586 self._show_prompt('>>> ')
581
587
582 def _show_interpreter_prompt_for_reply(self, msg):
588 def _show_interpreter_prompt_for_reply(self, msg):
583 """ Shows a prompt for the interpreter given an 'execute_reply' message.
589 """ Shows a prompt for the interpreter given an 'execute_reply' message.
584 """
590 """
585 self._show_interpreter_prompt()
591 self._show_interpreter_prompt()
586
592
587 #------ Signal handlers ----------------------------------------------------
593 #------ Signal handlers ----------------------------------------------------
588
594
589 def _document_contents_change(self, position, removed, added):
595 def _document_contents_change(self, position, removed, added):
590 """ Called whenever the document's content changes. Display a call tip
596 """ Called whenever the document's content changes. Display a call tip
591 if appropriate.
597 if appropriate.
592 """
598 """
593 # Calculate where the cursor should be *after* the change:
599 # Calculate where the cursor should be *after* the change:
594 position += added
600 position += added
595
601
596 document = self._control.document()
602 document = self._control.document()
597 if position == self._get_cursor().position():
603 if position == self._get_cursor().position():
598 self._call_tip()
604 self._call_tip()
@@ -1,273 +1,272 b''
1 # Standard libary imports.
1 # Standard libary imports.
2 from base64 import decodestring
2 from base64 import decodestring
3 import os
3 import os
4 import re
4 import re
5
5
6 # System libary imports.
6 # System libary imports.
7 from IPython.external.qt import QtCore, QtGui
7 from IPython.external.qt import QtCore, QtGui
8
8
9 # Local imports
9 # Local imports
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
10 from IPython.frontend.qt.svg import save_svg, svg_to_clipboard, svg_to_image
11 from ipython_widget import IPythonWidget
11 from ipython_widget import IPythonWidget
12
12
13
13
14 class RichIPythonWidget(IPythonWidget):
14 class RichIPythonWidget(IPythonWidget):
15 """ An IPythonWidget that supports rich text, including lists, images, and
15 """ An IPythonWidget that supports rich text, including lists, images, and
16 tables. Note that raw performance will be reduced compared to the plain
16 tables. Note that raw performance will be reduced compared to the plain
17 text version.
17 text version.
18 """
18 """
19
19
20 # RichIPythonWidget protected class variables.
20 # RichIPythonWidget protected class variables.
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
21 _payload_source_plot = 'IPython.zmq.pylab.backend_payload.add_plot_payload'
22 _svg_text_format_property = 1
22 _svg_text_format_property = 1
23
23
24 #---------------------------------------------------------------------------
24 #---------------------------------------------------------------------------
25 # 'object' interface
25 # 'object' interface
26 #---------------------------------------------------------------------------
26 #---------------------------------------------------------------------------
27
27
28 def __init__(self, *args, **kw):
28 def __init__(self, *args, **kw):
29 """ Create a RichIPythonWidget.
29 """ Create a RichIPythonWidget.
30 """
30 """
31 kw['kind'] = 'rich'
31 kw['kind'] = 'rich'
32 super(RichIPythonWidget, self).__init__(*args, **kw)
32 super(RichIPythonWidget, self).__init__(*args, **kw)
33 # Dictionary for resolving Qt names to images when
33
34 # generating XHTML output
34 # Configure the ConsoleWidget HTML exporter for our formats.
35 self._html_exporter.image_tag = self._get_image_tag
36
37 # Dictionary for resolving Qt names to images when generating XHTML
38 # output
35 self._name_to_svg = {}
39 self._name_to_svg = {}
36
40
37 #---------------------------------------------------------------------------
41 #---------------------------------------------------------------------------
38 # 'ConsoleWidget' protected interface
42 # 'ConsoleWidget' protected interface
39 #---------------------------------------------------------------------------
43 #---------------------------------------------------------------------------
40
44
41 def _context_menu_make(self, pos):
45 def _context_menu_make(self, pos):
42 """ Reimplemented to return a custom context menu for images.
46 """ Reimplemented to return a custom context menu for images.
43 """
47 """
44 format = self._control.cursorForPosition(pos).charFormat()
48 format = self._control.cursorForPosition(pos).charFormat()
45 name = format.stringProperty(QtGui.QTextFormat.ImageName)
49 name = format.stringProperty(QtGui.QTextFormat.ImageName)
46 if name:
50 if name:
47 menu = QtGui.QMenu()
51 menu = QtGui.QMenu()
48
52
49 menu.addAction('Copy Image', lambda: self._copy_image(name))
53 menu.addAction('Copy Image', lambda: self._copy_image(name))
50 menu.addAction('Save Image As...', lambda: self._save_image(name))
54 menu.addAction('Save Image As...', lambda: self._save_image(name))
51 menu.addSeparator()
55 menu.addSeparator()
52
56
53 svg = format.stringProperty(self._svg_text_format_property)
57 svg = format.stringProperty(self._svg_text_format_property)
54 if svg:
58 if svg:
55 menu.addSeparator()
59 menu.addSeparator()
56 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
60 menu.addAction('Copy SVG', lambda: svg_to_clipboard(svg))
57 menu.addAction('Save SVG As...',
61 menu.addAction('Save SVG As...',
58 lambda: save_svg(svg, self._control))
62 lambda: save_svg(svg, self._control))
59 else:
63 else:
60 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
64 menu = super(RichIPythonWidget, self)._context_menu_make(pos)
61 return menu
65 return menu
62
66
63 #---------------------------------------------------------------------------
67 #---------------------------------------------------------------------------
64 # 'BaseFrontendMixin' abstract interface
68 # 'BaseFrontendMixin' abstract interface
65 #---------------------------------------------------------------------------
69 #---------------------------------------------------------------------------
66
70
67 def _handle_pyout(self, msg):
71 def _handle_pyout(self, msg):
68 """ Overridden to handle rich data types, like SVG.
72 """ Overridden to handle rich data types, like SVG.
69 """
73 """
70 if not self._hidden and self._is_from_this_session(msg):
74 if not self._hidden and self._is_from_this_session(msg):
71 content = msg['content']
75 content = msg['content']
72 prompt_number = content['execution_count']
76 prompt_number = content['execution_count']
73 data = content['data']
77 data = content['data']
74 if data.has_key('image/svg+xml'):
78 if data.has_key('image/svg+xml'):
75 self._append_plain_text(self.output_sep)
79 self._append_plain_text(self.output_sep)
76 self._append_html(self._make_out_prompt(prompt_number))
80 self._append_html(self._make_out_prompt(prompt_number))
77 # TODO: try/except this call.
81 # TODO: try/except this call.
78 self._append_svg(data['image/svg+xml'])
82 self._append_svg(data['image/svg+xml'])
79 self._append_html(self.output_sep2)
83 self._append_html(self.output_sep2)
80 elif data.has_key('image/png'):
84 elif data.has_key('image/png'):
81 self._append_plain_text(self.output_sep)
85 self._append_plain_text(self.output_sep)
82 self._append_html(self._make_out_prompt(prompt_number))
86 self._append_html(self._make_out_prompt(prompt_number))
83 # This helps the output to look nice.
87 # This helps the output to look nice.
84 self._append_plain_text('\n')
88 self._append_plain_text('\n')
85 # TODO: try/except these calls
89 # TODO: try/except these calls
86 png = decodestring(data['image/png'])
90 png = decodestring(data['image/png'])
87 self._append_png(png)
91 self._append_png(png)
88 self._append_html(self.output_sep2)
92 self._append_html(self.output_sep2)
89 else:
93 else:
90 # Default back to the plain text representation.
94 # Default back to the plain text representation.
91 return super(RichIPythonWidget, self)._handle_pyout(msg)
95 return super(RichIPythonWidget, self)._handle_pyout(msg)
92
96
93 def _handle_display_data(self, msg):
97 def _handle_display_data(self, msg):
94 """ Overridden to handle rich data types, like SVG.
98 """ Overridden to handle rich data types, like SVG.
95 """
99 """
96 if not self._hidden and self._is_from_this_session(msg):
100 if not self._hidden and self._is_from_this_session(msg):
97 source = msg['content']['source']
101 source = msg['content']['source']
98 data = msg['content']['data']
102 data = msg['content']['data']
99 metadata = msg['content']['metadata']
103 metadata = msg['content']['metadata']
100 # Try to use the svg or html representations.
104 # Try to use the svg or html representations.
101 # FIXME: Is this the right ordering of things to try?
105 # FIXME: Is this the right ordering of things to try?
102 if data.has_key('image/svg+xml'):
106 if data.has_key('image/svg+xml'):
103 svg = data['image/svg+xml']
107 svg = data['image/svg+xml']
104 # TODO: try/except this call.
108 # TODO: try/except this call.
105 self._append_svg(svg)
109 self._append_svg(svg)
106 elif data.has_key('image/png'):
110 elif data.has_key('image/png'):
107 # TODO: try/except these calls
111 # TODO: try/except these calls
108 # PNG data is base64 encoded as it passes over the network
112 # PNG data is base64 encoded as it passes over the network
109 # in a JSON structure so we decode it.
113 # in a JSON structure so we decode it.
110 png = decodestring(data['image/png'])
114 png = decodestring(data['image/png'])
111 self._append_png(png)
115 self._append_png(png)
112 else:
116 else:
113 # Default back to the plain text representation.
117 # Default back to the plain text representation.
114 return super(RichIPythonWidget, self)._handle_display_data(msg)
118 return super(RichIPythonWidget, self)._handle_display_data(msg)
115
119
116 #---------------------------------------------------------------------------
120 #---------------------------------------------------------------------------
117 # 'FrontendWidget' protected interface
121 # 'FrontendWidget' protected interface
118 #---------------------------------------------------------------------------
122 #---------------------------------------------------------------------------
119
123
120 def _process_execute_payload(self, item):
124 def _process_execute_payload(self, item):
121 """ Reimplemented to handle matplotlib plot payloads.
125 """ Reimplemented to handle matplotlib plot payloads.
122 """
126 """
123 # TODO: remove this as all plot data is coming back through the
127 # TODO: remove this as all plot data is coming back through the
124 # display_data message type.
128 # display_data message type.
125 if item['source'] == self._payload_source_plot:
129 if item['source'] == self._payload_source_plot:
126 if item['format'] == 'svg':
130 if item['format'] == 'svg':
127 svg = item['data']
131 svg = item['data']
128 self._append_svg(svg)
132 self._append_svg(svg)
129 return True
133 return True
130 else:
134 else:
131 # Add other plot formats here!
135 # Add other plot formats here!
132 return False
136 return False
133 else:
137 else:
134 return super(RichIPythonWidget, self)._process_execute_payload(item)
138 return super(RichIPythonWidget, self)._process_execute_payload(item)
135
139
136 #---------------------------------------------------------------------------
140 #---------------------------------------------------------------------------
137 # 'RichIPythonWidget' protected interface
141 # 'RichIPythonWidget' protected interface
138 #---------------------------------------------------------------------------
142 #---------------------------------------------------------------------------
139
143
140 def _append_svg(self, svg):
144 def _append_svg(self, svg):
141 """ Append raw svg data to the widget.
145 """ Append raw svg data to the widget.
142 """
146 """
143 try:
147 try:
144 image = svg_to_image(svg)
148 image = svg_to_image(svg)
145 except ValueError:
149 except ValueError:
146 self._append_plain_text('Received invalid plot data.')
150 self._append_plain_text('Received invalid plot data.')
147 else:
151 else:
148 format = self._add_image(image)
152 format = self._add_image(image)
149 self._name_to_svg[str(format.name())] = svg
153 self._name_to_svg[str(format.name())] = svg
150 format.setProperty(self._svg_text_format_property, svg)
154 format.setProperty(self._svg_text_format_property, svg)
151 cursor = self._get_end_cursor()
155 cursor = self._get_end_cursor()
152 cursor.insertBlock()
156 cursor.insertBlock()
153 cursor.insertImage(format)
157 cursor.insertImage(format)
154 cursor.insertBlock()
158 cursor.insertBlock()
155
159
156 def _append_png(self, png):
160 def _append_png(self, png):
157 """ Append raw svg data to the widget.
161 """ Append raw svg data to the widget.
158 """
162 """
159 try:
163 try:
160 image = QtGui.QImage()
164 image = QtGui.QImage()
161 image.loadFromData(png, 'PNG')
165 image.loadFromData(png, 'PNG')
162 except ValueError:
166 except ValueError:
163 self._append_plain_text('Received invalid plot data.')
167 self._append_plain_text('Received invalid plot data.')
164 else:
168 else:
165 format = self._add_image(image)
169 format = self._add_image(image)
166 cursor = self._get_end_cursor()
170 cursor = self._get_end_cursor()
167 cursor.insertBlock()
171 cursor.insertBlock()
168 cursor.insertImage(format)
172 cursor.insertImage(format)
169 cursor.insertBlock()
173 cursor.insertBlock()
170
174
171 def _add_image(self, image):
175 def _add_image(self, image):
172 """ Adds the specified QImage to the document and returns a
176 """ Adds the specified QImage to the document and returns a
173 QTextImageFormat that references it.
177 QTextImageFormat that references it.
174 """
178 """
175 document = self._control.document()
179 document = self._control.document()
176 name = str(image.cacheKey())
180 name = str(image.cacheKey())
177 document.addResource(QtGui.QTextDocument.ImageResource,
181 document.addResource(QtGui.QTextDocument.ImageResource,
178 QtCore.QUrl(name), image)
182 QtCore.QUrl(name), image)
179 format = QtGui.QTextImageFormat()
183 format = QtGui.QTextImageFormat()
180 format.setName(name)
184 format.setName(name)
181 return format
185 return format
182
186
183 def _copy_image(self, name):
187 def _copy_image(self, name):
184 """ Copies the ImageResource with 'name' to the clipboard.
188 """ Copies the ImageResource with 'name' to the clipboard.
185 """
189 """
186 image = self._get_image(name)
190 image = self._get_image(name)
187 QtGui.QApplication.clipboard().setImage(image)
191 QtGui.QApplication.clipboard().setImage(image)
188
192
189 def _get_image(self, name):
193 def _get_image(self, name):
190 """ Returns the QImage stored as the ImageResource with 'name'.
194 """ Returns the QImage stored as the ImageResource with 'name'.
191 """
195 """
192 document = self._control.document()
196 document = self._control.document()
193 variant = document.resource(QtGui.QTextDocument.ImageResource,
197 variant = document.resource(QtGui.QTextDocument.ImageResource,
194 QtCore.QUrl(name))
198 QtCore.QUrl(name))
195 return variant.toPyObject()
199 return variant.toPyObject()
196
200
197 def _save_image(self, name, format='PNG'):
201 def _get_image_tag(self, match, path = None, format = "png"):
198 """ Shows a save dialog for the ImageResource with 'name'.
199 """
200 dialog = QtGui.QFileDialog(self._control, 'Save Image')
201 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
202 dialog.setDefaultSuffix(format.lower())
203 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
204 if dialog.exec_():
205 filename = dialog.selectedFiles()[0]
206 image = self._get_image(name)
207 image.save(filename, format)
208
209 def image_tag(self, match, path = None, format = "png"):
210 """ Return (X)HTML mark-up for the image-tag given by match.
202 """ Return (X)HTML mark-up for the image-tag given by match.
211
203
212 Parameters
204 Parameters
213 ----------
205 ----------
214 match : re.SRE_Match
206 match : re.SRE_Match
215 A match to an HTML image tag as exported by Qt, with
207 A match to an HTML image tag as exported by Qt, with
216 match.group("Name") containing the matched image ID.
208 match.group("Name") containing the matched image ID.
217
209
218 path : string|None, optional [default None]
210 path : string|None, optional [default None]
219 If not None, specifies a path to which supporting files
211 If not None, specifies a path to which supporting files may be
220 may be written (e.g., for linked images).
212 written (e.g., for linked images). If None, all images are to be
221 If None, all images are to be included inline.
213 included inline.
222
214
223 format : "png"|"svg", optional [default "png"]
215 format : "png"|"svg", optional [default "png"]
224 Format for returned or referenced images.
216 Format for returned or referenced images.
225
226 Subclasses supporting image display should override this
227 method.
228 """
217 """
229
218 if format == "png":
230 if(format == "png"):
231 try:
219 try:
232 image = self._get_image(match.group("name"))
220 image = self._get_image(match.group("name"))
233 except KeyError:
221 except KeyError:
234 return "<b>Couldn't find image %s</b>" % match.group("name")
222 return "<b>Couldn't find image %s</b>" % match.group("name")
235
223
236 if(path is not None):
224 if path is not None:
237 if not os.path.exists(path):
225 if not os.path.exists(path):
238 os.mkdir(path)
226 os.mkdir(path)
239 relpath = os.path.basename(path)
227 relpath = os.path.basename(path)
240 if(image.save("%s/qt_img%s.png" % (path,match.group("name")),
228 if image.save("%s/qt_img%s.png" % (path,match.group("name")),
241 "PNG")):
229 "PNG"):
242 return '<img src="%s/qt_img%s.png">' % (relpath,
230 return '<img src="%s/qt_img%s.png">' % (relpath,
243 match.group("name"))
231 match.group("name"))
244 else:
232 else:
245 return "<b>Couldn't save image!</b>"
233 return "<b>Couldn't save image!</b>"
246 else:
234 else:
247 ba = QtCore.QByteArray()
235 ba = QtCore.QByteArray()
248 buffer_ = QtCore.QBuffer(ba)
236 buffer_ = QtCore.QBuffer(ba)
249 buffer_.open(QtCore.QIODevice.WriteOnly)
237 buffer_.open(QtCore.QIODevice.WriteOnly)
250 image.save(buffer_, "PNG")
238 image.save(buffer_, "PNG")
251 buffer_.close()
239 buffer_.close()
252 return '<img src="data:image/png;base64,\n%s\n" />' % (
240 return '<img src="data:image/png;base64,\n%s\n" />' % (
253 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
241 re.sub(r'(.{60})',r'\1\n',str(ba.toBase64())))
254
242
255 elif(format == "svg"):
243 elif format == "svg":
256 try:
244 try:
257 svg = str(self._name_to_svg[match.group("name")])
245 svg = str(self._name_to_svg[match.group("name")])
258 except KeyError:
246 except KeyError:
259 return "<b>Couldn't find image %s</b>" % match.group("name")
247 return "<b>Couldn't find image %s</b>" % match.group("name")
260
248
261 # Not currently checking path, because it's tricky to find a
249 # Not currently checking path, because it's tricky to find a
262 # cross-browser way to embed external SVG images (e.g., via
250 # cross-browser way to embed external SVG images (e.g., via
263 # object or embed tags).
251 # object or embed tags).
264
252
265 # Chop stand-alone header from matplotlib SVG
253 # Chop stand-alone header from matplotlib SVG
266 offset = svg.find("<svg")
254 offset = svg.find("<svg")
267 assert(offset > -1)
255 assert(offset > -1)
268
256
269 return svg[offset:]
257 return svg[offset:]
270
258
271 else:
259 else:
272 return '<b>Unrecognized image format</b>'
260 return '<b>Unrecognized image format</b>'
273
261
262 def _save_image(self, name, format='PNG'):
263 """ Shows a save dialog for the ImageResource with 'name'.
264 """
265 dialog = QtGui.QFileDialog(self._control, 'Save Image')
266 dialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
267 dialog.setDefaultSuffix(format.lower())
268 dialog.setNameFilter('%s file (*.%s)' % (format, format.lower()))
269 if dialog.exec_():
270 filename = dialog.selectedFiles()[0]
271 image = self._get_image(name)
272 image.save(filename, format)
General Comments 0
You need to be logged in to leave comments. Login now