##// END OF EJS Templates
Add support for beep ('\b') character in qtconsole.
Michael Droettboom -
Show More
@@ -1,342 +1,348 b''
1 1 """ Utilities for processing ANSI escape codes and special ASCII characters.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 from collections import namedtuple
9 9 import re
10 10
11 11 # System library imports
12 12 from IPython.external.qt import QtCore, QtGui
13 13
14 14 #-----------------------------------------------------------------------------
15 15 # Constants and datatypes
16 16 #-----------------------------------------------------------------------------
17 17
18 18 # An action for erase requests (ED and EL commands).
19 19 EraseAction = namedtuple('EraseAction', ['action', 'area', 'erase_to'])
20 20
21 21 # An action for cursor move requests (CUU, CUD, CUF, CUB, CNL, CPL, CHA, CUP,
22 22 # and HVP commands).
23 23 # FIXME: Not implemented in AnsiCodeProcessor.
24 24 MoveAction = namedtuple('MoveAction', ['action', 'dir', 'unit', 'count'])
25 25
26 26 # An action for scroll requests (SU and ST) and form feeds.
27 27 ScrollAction = namedtuple('ScrollAction', ['action', 'dir', 'unit', 'count'])
28 28
29 29 # An action for the carriage return character
30 30 CarriageReturnAction = namedtuple('CarriageReturnAction', ['action'])
31 31
32 # An action for the beep character
33 BeepAction = namedtuple('BeepAction', ['action'])
34
32 35 # Regular expressions.
33 36 CSI_COMMANDS = 'ABCDEFGHJKSTfmnsu'
34 37 CSI_SUBPATTERN = '\[(.*?)([%s])' % CSI_COMMANDS
35 38 OSC_SUBPATTERN = '\](.*?)[\x07\x1b]'
36 39 ANSI_PATTERN = ('\x01?\x1b(%s|%s)\x02?' % \
37 40 (CSI_SUBPATTERN, OSC_SUBPATTERN))
38 ANSI_OR_CR_PATTERN = re.compile('(\r)|(?:%s)' % ANSI_PATTERN)
41 ANSI_OR_SPECIAL_PATTERN = re.compile('(\b|\r)|(?:%s)' % ANSI_PATTERN)
39 42 SPECIAL_PATTERN = re.compile('([\f])')
40 43
41 44 #-----------------------------------------------------------------------------
42 45 # Classes
43 46 #-----------------------------------------------------------------------------
44 47
45 48 class AnsiCodeProcessor(object):
46 49 """ Translates special ASCII characters and ANSI escape codes into readable
47 50 attributes. It also supports a few non-standard, xterm-specific codes.
48 51 """
49 52
50 53 # Whether to increase intensity or set boldness for SGR code 1.
51 54 # (Different terminals handle this in different ways.)
52 55 bold_text_enabled = False
53 56
54 57 # We provide an empty default color map because subclasses will likely want
55 58 # to use a custom color format.
56 59 default_color_map = {}
57 60
58 61 #---------------------------------------------------------------------------
59 62 # AnsiCodeProcessor interface
60 63 #---------------------------------------------------------------------------
61 64
62 65 def __init__(self):
63 66 self.actions = []
64 67 self.color_map = self.default_color_map.copy()
65 68 self.reset_sgr()
66 69
67 70 def reset_sgr(self):
68 71 """ Reset graphics attributs to their default values.
69 72 """
70 73 self.intensity = 0
71 74 self.italic = False
72 75 self.bold = False
73 76 self.underline = False
74 77 self.foreground_color = None
75 78 self.background_color = None
76 79
77 80 def split_string(self, string):
78 81 """ Yields substrings for which the same escape code applies.
79 82 """
80 83 self.actions = []
81 84 start = 0
82 85
83 for match in ANSI_OR_CR_PATTERN.finditer(string):
86 for match in ANSI_OR_SPECIAL_PATTERN.finditer(string):
84 87 raw = string[start:match.start()]
85 88 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
86 89 if substring or self.actions:
87 90 yield substring
88 91 start = match.end()
89 92
90 93 self.actions = []
91 94 groups = filter(lambda x: x is not None, match.groups())
92 95 if groups[0] == '\r':
93 96 self.actions.append(CarriageReturnAction('carriage-return'))
94 97 yield ''
98 elif groups[0] == '\b':
99 self.actions.append(BeepAction('beep'))
100 yield ''
95 101 else:
96 102 params = [ param for param in groups[1].split(';') if param ]
97 103 if groups[0].startswith('['):
98 104 # Case 1: CSI code.
99 105 try:
100 106 params = map(int, params)
101 107 except ValueError:
102 108 # Silently discard badly formed codes.
103 109 pass
104 110 else:
105 111 self.set_csi_code(groups[2], params)
106 112
107 113 elif groups[0].startswith(']'):
108 114 # Case 2: OSC code.
109 115 self.set_osc_code(params)
110 116
111 117 raw = string[start:]
112 118 substring = SPECIAL_PATTERN.sub(self._replace_special, raw)
113 119 if substring or self.actions:
114 120 yield substring
115 121
116 122 def set_csi_code(self, command, params=[]):
117 123 """ Set attributes based on CSI (Control Sequence Introducer) code.
118 124
119 125 Parameters
120 126 ----------
121 127 command : str
122 128 The code identifier, i.e. the final character in the sequence.
123 129
124 130 params : sequence of integers, optional
125 131 The parameter codes for the command.
126 132 """
127 133 if command == 'm': # SGR - Select Graphic Rendition
128 134 if params:
129 135 self.set_sgr_code(params)
130 136 else:
131 137 self.set_sgr_code([0])
132 138
133 139 elif (command == 'J' or # ED - Erase Data
134 140 command == 'K'): # EL - Erase in Line
135 141 code = params[0] if params else 0
136 142 if 0 <= code <= 2:
137 143 area = 'screen' if command == 'J' else 'line'
138 144 if code == 0:
139 145 erase_to = 'end'
140 146 elif code == 1:
141 147 erase_to = 'start'
142 148 elif code == 2:
143 149 erase_to = 'all'
144 150 self.actions.append(EraseAction('erase', area, erase_to))
145 151
146 152 elif (command == 'S' or # SU - Scroll Up
147 153 command == 'T'): # SD - Scroll Down
148 154 dir = 'up' if command == 'S' else 'down'
149 155 count = params[0] if params else 1
150 156 self.actions.append(ScrollAction('scroll', dir, 'line', count))
151 157
152 158 def set_osc_code(self, params):
153 159 """ Set attributes based on OSC (Operating System Command) parameters.
154 160
155 161 Parameters
156 162 ----------
157 163 params : sequence of str
158 164 The parameters for the command.
159 165 """
160 166 try:
161 167 command = int(params.pop(0))
162 168 except (IndexError, ValueError):
163 169 return
164 170
165 171 if command == 4:
166 172 # xterm-specific: set color number to color spec.
167 173 try:
168 174 color = int(params.pop(0))
169 175 spec = params.pop(0)
170 176 self.color_map[color] = self._parse_xterm_color_spec(spec)
171 177 except (IndexError, ValueError):
172 178 pass
173 179
174 180 def set_sgr_code(self, params):
175 181 """ Set attributes based on SGR (Select Graphic Rendition) codes.
176 182
177 183 Parameters
178 184 ----------
179 185 params : sequence of ints
180 186 A list of SGR codes for one or more SGR commands. Usually this
181 187 sequence will have one element per command, although certain
182 188 xterm-specific commands requires multiple elements.
183 189 """
184 190 # Always consume the first parameter.
185 191 if not params:
186 192 return
187 193 code = params.pop(0)
188 194
189 195 if code == 0:
190 196 self.reset_sgr()
191 197 elif code == 1:
192 198 if self.bold_text_enabled:
193 199 self.bold = True
194 200 else:
195 201 self.intensity = 1
196 202 elif code == 2:
197 203 self.intensity = 0
198 204 elif code == 3:
199 205 self.italic = True
200 206 elif code == 4:
201 207 self.underline = True
202 208 elif code == 22:
203 209 self.intensity = 0
204 210 self.bold = False
205 211 elif code == 23:
206 212 self.italic = False
207 213 elif code == 24:
208 214 self.underline = False
209 215 elif code >= 30 and code <= 37:
210 216 self.foreground_color = code - 30
211 217 elif code == 38 and params and params.pop(0) == 5:
212 218 # xterm-specific: 256 color support.
213 219 if params:
214 220 self.foreground_color = params.pop(0)
215 221 elif code == 39:
216 222 self.foreground_color = None
217 223 elif code >= 40 and code <= 47:
218 224 self.background_color = code - 40
219 225 elif code == 48 and params and params.pop(0) == 5:
220 226 # xterm-specific: 256 color support.
221 227 if params:
222 228 self.background_color = params.pop(0)
223 229 elif code == 49:
224 230 self.background_color = None
225 231
226 232 # Recurse with unconsumed parameters.
227 233 self.set_sgr_code(params)
228 234
229 235 #---------------------------------------------------------------------------
230 236 # Protected interface
231 237 #---------------------------------------------------------------------------
232 238
233 239 def _parse_xterm_color_spec(self, spec):
234 240 if spec.startswith('rgb:'):
235 241 return tuple(map(lambda x: int(x, 16), spec[4:].split('/')))
236 242 elif spec.startswith('rgbi:'):
237 243 return tuple(map(lambda x: int(float(x) * 255),
238 244 spec[5:].split('/')))
239 245 elif spec == '?':
240 246 raise ValueError('Unsupported xterm color spec')
241 247 return spec
242 248
243 249 def _replace_special(self, match):
244 250 special = match.group(1)
245 251 if special == '\f':
246 252 self.actions.append(ScrollAction('scroll', 'down', 'page', 1))
247 253 return ''
248 254
249 255
250 256 class QtAnsiCodeProcessor(AnsiCodeProcessor):
251 257 """ Translates ANSI escape codes into QTextCharFormats.
252 258 """
253 259
254 260 # A map from ANSI color codes to SVG color names or RGB(A) tuples.
255 261 darkbg_color_map = {
256 262 0 : 'black', # black
257 263 1 : 'darkred', # red
258 264 2 : 'darkgreen', # green
259 265 3 : 'brown', # yellow
260 266 4 : 'darkblue', # blue
261 267 5 : 'darkviolet', # magenta
262 268 6 : 'steelblue', # cyan
263 269 7 : 'grey', # white
264 270 8 : 'grey', # black (bright)
265 271 9 : 'red', # red (bright)
266 272 10 : 'lime', # green (bright)
267 273 11 : 'yellow', # yellow (bright)
268 274 12 : 'deepskyblue', # blue (bright)
269 275 13 : 'magenta', # magenta (bright)
270 276 14 : 'cyan', # cyan (bright)
271 277 15 : 'white' } # white (bright)
272 278
273 279 # Set the default color map for super class.
274 280 default_color_map = darkbg_color_map.copy()
275 281
276 282 def get_color(self, color, intensity=0):
277 283 """ Returns a QColor for a given color code, or None if one cannot be
278 284 constructed.
279 285 """
280 286 if color is None:
281 287 return None
282 288
283 289 # Adjust for intensity, if possible.
284 290 if color < 8 and intensity > 0:
285 291 color += 8
286 292
287 293 constructor = self.color_map.get(color, None)
288 294 if isinstance(constructor, basestring):
289 295 # If this is an X11 color name, we just hope there is a close SVG
290 296 # color name. We could use QColor's static method
291 297 # 'setAllowX11ColorNames()', but this is global and only available
292 298 # on X11. It seems cleaner to aim for uniformity of behavior.
293 299 return QtGui.QColor(constructor)
294 300
295 301 elif isinstance(constructor, (tuple, list)):
296 302 return QtGui.QColor(*constructor)
297 303
298 304 return None
299 305
300 306 def get_format(self):
301 307 """ Returns a QTextCharFormat that encodes the current style attributes.
302 308 """
303 309 format = QtGui.QTextCharFormat()
304 310
305 311 # Set foreground color
306 312 qcolor = self.get_color(self.foreground_color, self.intensity)
307 313 if qcolor is not None:
308 314 format.setForeground(qcolor)
309 315
310 316 # Set background color
311 317 qcolor = self.get_color(self.background_color, self.intensity)
312 318 if qcolor is not None:
313 319 format.setBackground(qcolor)
314 320
315 321 # Set font weight/style options
316 322 if self.bold:
317 323 format.setFontWeight(QtGui.QFont.Bold)
318 324 else:
319 325 format.setFontWeight(QtGui.QFont.Normal)
320 326 format.setFontItalic(self.italic)
321 327 format.setFontUnderline(self.underline)
322 328
323 329 return format
324 330
325 331 def set_background_color(self, color):
326 332 """ Given a background color (a QColor), attempt to set a color map
327 333 that will be aesthetically pleasing.
328 334 """
329 335 # Set a new default color map.
330 336 self.default_color_map = self.darkbg_color_map.copy()
331 337
332 338 if color.value() >= 127:
333 339 # Colors appropriate for a terminal with a light background. For
334 340 # now, only use non-bright colors...
335 341 for i in xrange(8):
336 342 self.default_color_map[i + 8] = self.default_color_map[i]
337 343
338 344 # ...and replace white with black.
339 345 self.default_color_map[7] = self.default_color_map[15] = 'black'
340 346
341 347 # Update the current color map with the new defaults.
342 348 self.color_map.update(self.default_color_map)
@@ -1,1819 +1,1822 b''
1 1 """ An abstract base class for console-type widgets.
2 2 """
3 3 #-----------------------------------------------------------------------------
4 4 # Imports
5 5 #-----------------------------------------------------------------------------
6 6
7 7 # Standard library imports
8 8 import os
9 9 from os.path import commonprefix
10 10 import re
11 11 import sys
12 12 from textwrap import dedent
13 13 from unicodedata import category
14 14
15 15 # System library imports
16 16 from IPython.external.qt import QtCore, QtGui
17 17
18 18 # Local imports
19 19 from IPython.config.configurable import LoggingConfigurable
20 20 from IPython.frontend.qt.rich_text import HtmlExporter
21 21 from IPython.frontend.qt.util import MetaQObjectHasTraits, get_font
22 22 from IPython.utils.text import columnize
23 23 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 24 from ansi_code_processor import QtAnsiCodeProcessor
25 25 from completion_widget import CompletionWidget
26 26 from kill_ring import QtKillRing
27 27
28 28 #-----------------------------------------------------------------------------
29 29 # Functions
30 30 #-----------------------------------------------------------------------------
31 31
32 32 def is_letter_or_number(char):
33 33 """ Returns whether the specified unicode character is a letter or a number.
34 34 """
35 35 cat = category(char)
36 36 return cat.startswith('L') or cat.startswith('N')
37 37
38 38 #-----------------------------------------------------------------------------
39 39 # Classes
40 40 #-----------------------------------------------------------------------------
41 41
42 42 class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
43 43 """ An abstract base class for console-type widgets. This class has
44 44 functionality for:
45 45
46 46 * Maintaining a prompt and editing region
47 47 * Providing the traditional Unix-style console keyboard shortcuts
48 48 * Performing tab completion
49 49 * Paging text
50 50 * Handling ANSI escape codes
51 51
52 52 ConsoleWidget also provides a number of utility methods that will be
53 53 convenient to implementors of a console-style widget.
54 54 """
55 55 __metaclass__ = MetaQObjectHasTraits
56 56
57 57 #------ Configuration ------------------------------------------------------
58 58
59 59 ansi_codes = Bool(True, config=True,
60 60 help="Whether to process ANSI escape codes."
61 61 )
62 62 buffer_size = Integer(500, config=True,
63 63 help="""
64 64 The maximum number of lines of text before truncation. Specifying a
65 65 non-positive number disables text truncation (not recommended).
66 66 """
67 67 )
68 68 gui_completion = Bool(False, config=True,
69 69 help="""
70 70 Use a list widget instead of plain text output for tab completion.
71 71 """
72 72 )
73 73 # NOTE: this value can only be specified during initialization.
74 74 kind = Enum(['plain', 'rich'], default_value='plain', config=True,
75 75 help="""
76 76 The type of underlying text widget to use. Valid values are 'plain',
77 77 which specifies a QPlainTextEdit, and 'rich', which specifies a
78 78 QTextEdit.
79 79 """
80 80 )
81 81 # NOTE: this value can only be specified during initialization.
82 82 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
83 83 default_value='inside', config=True,
84 84 help="""
85 85 The type of paging to use. Valid values are:
86 86
87 87 'inside' : The widget pages like a traditional terminal.
88 88 'hsplit' : When paging is requested, the widget is split
89 89 horizontally. The top pane contains the console, and the
90 90 bottom pane contains the paged text.
91 91 'vsplit' : Similar to 'hsplit', except that a vertical splitter
92 92 used.
93 93 'custom' : No action is taken by the widget beyond emitting a
94 94 'custom_page_requested(str)' signal.
95 95 'none' : The text is written directly to the console.
96 96 """)
97 97
98 98 font_family = Unicode(config=True,
99 99 help="""The font family to use for the console.
100 100 On OSX this defaults to Monaco, on Windows the default is
101 101 Consolas with fallback of Courier, and on other platforms
102 102 the default is Monospace.
103 103 """)
104 104 def _font_family_default(self):
105 105 if sys.platform == 'win32':
106 106 # Consolas ships with Vista/Win7, fallback to Courier if needed
107 107 return 'Consolas'
108 108 elif sys.platform == 'darwin':
109 109 # OSX always has Monaco, no need for a fallback
110 110 return 'Monaco'
111 111 else:
112 112 # Monospace should always exist, no need for a fallback
113 113 return 'Monospace'
114 114
115 115 font_size = Integer(config=True,
116 116 help="""The font size. If unconfigured, Qt will be entrusted
117 117 with the size of the font.
118 118 """)
119 119
120 120 # Whether to override ShortcutEvents for the keybindings defined by this
121 121 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
122 122 # priority (when it has focus) over, e.g., window-level menu shortcuts.
123 123 override_shortcuts = Bool(False)
124 124
125 125 #------ Signals ------------------------------------------------------------
126 126
127 127 # Signals that indicate ConsoleWidget state.
128 128 copy_available = QtCore.Signal(bool)
129 129 redo_available = QtCore.Signal(bool)
130 130 undo_available = QtCore.Signal(bool)
131 131
132 132 # Signal emitted when paging is needed and the paging style has been
133 133 # specified as 'custom'.
134 134 custom_page_requested = QtCore.Signal(object)
135 135
136 136 # Signal emitted when the font is changed.
137 137 font_changed = QtCore.Signal(QtGui.QFont)
138 138
139 139 #------ Protected class variables ------------------------------------------
140 140
141 141 # When the control key is down, these keys are mapped.
142 142 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
143 143 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
144 144 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
145 145 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
146 146 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
147 147 QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace,
148 148 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
149 149 if not sys.platform == 'darwin':
150 150 # On OS X, Ctrl-E already does the right thing, whereas End moves the
151 151 # cursor to the bottom of the buffer.
152 152 _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
153 153
154 154 # The shortcuts defined by this widget. We need to keep track of these to
155 155 # support 'override_shortcuts' above.
156 156 _shortcuts = set(_ctrl_down_remap.keys() +
157 157 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
158 158 QtCore.Qt.Key_V ])
159 159
160 160 #---------------------------------------------------------------------------
161 161 # 'QObject' interface
162 162 #---------------------------------------------------------------------------
163 163
164 164 def __init__(self, parent=None, **kw):
165 165 """ Create a ConsoleWidget.
166 166
167 167 Parameters:
168 168 -----------
169 169 parent : QWidget, optional [default None]
170 170 The parent for this widget.
171 171 """
172 172 QtGui.QWidget.__init__(self, parent)
173 173 LoggingConfigurable.__init__(self, **kw)
174 174
175 175 # Create the layout and underlying text widget.
176 176 layout = QtGui.QStackedLayout(self)
177 177 layout.setContentsMargins(0, 0, 0, 0)
178 178 self._control = self._create_control()
179 179 self._page_control = None
180 180 self._splitter = None
181 181 if self.paging in ('hsplit', 'vsplit'):
182 182 self._splitter = QtGui.QSplitter()
183 183 if self.paging == 'hsplit':
184 184 self._splitter.setOrientation(QtCore.Qt.Horizontal)
185 185 else:
186 186 self._splitter.setOrientation(QtCore.Qt.Vertical)
187 187 self._splitter.addWidget(self._control)
188 188 layout.addWidget(self._splitter)
189 189 else:
190 190 layout.addWidget(self._control)
191 191
192 192 # Create the paging widget, if necessary.
193 193 if self.paging in ('inside', 'hsplit', 'vsplit'):
194 194 self._page_control = self._create_page_control()
195 195 if self._splitter:
196 196 self._page_control.hide()
197 197 self._splitter.addWidget(self._page_control)
198 198 else:
199 199 layout.addWidget(self._page_control)
200 200
201 201 # Initialize protected variables. Some variables contain useful state
202 202 # information for subclasses; they should be considered read-only.
203 203 self._append_before_prompt_pos = 0
204 204 self._ansi_processor = QtAnsiCodeProcessor()
205 205 self._completion_widget = CompletionWidget(self._control)
206 206 self._continuation_prompt = '> '
207 207 self._continuation_prompt_html = None
208 208 self._executing = False
209 209 self._filter_drag = False
210 210 self._filter_resize = False
211 211 self._html_exporter = HtmlExporter(self._control)
212 212 self._input_buffer_executing = ''
213 213 self._input_buffer_pending = ''
214 214 self._kill_ring = QtKillRing(self._control)
215 215 self._prompt = ''
216 216 self._prompt_html = None
217 217 self._prompt_pos = 0
218 218 self._prompt_sep = ''
219 219 self._reading = False
220 220 self._reading_callback = None
221 221 self._tab_width = 8
222 222 self._text_completing_pos = 0
223 223
224 224 # Set a monospaced font.
225 225 self.reset_font()
226 226
227 227 # Configure actions.
228 228 action = QtGui.QAction('Print', None)
229 229 action.setEnabled(True)
230 230 printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
231 231 if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
232 232 # Only override the default if there is a collision.
233 233 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
234 234 printkey = "Ctrl+Shift+P"
235 235 action.setShortcut(printkey)
236 236 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
237 237 action.triggered.connect(self.print_)
238 238 self.addAction(action)
239 239 self.print_action = action
240 240
241 241 action = QtGui.QAction('Save as HTML/XML', None)
242 242 action.setShortcut(QtGui.QKeySequence.Save)
243 243 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
244 244 action.triggered.connect(self.export_html)
245 245 self.addAction(action)
246 246 self.export_action = action
247 247
248 248 action = QtGui.QAction('Select All', None)
249 249 action.setEnabled(True)
250 250 selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
251 251 if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
252 252 # Only override the default if there is a collision.
253 253 # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
254 254 selectall = "Ctrl+Shift+A"
255 255 action.setShortcut(selectall)
256 256 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
257 257 action.triggered.connect(self.select_all)
258 258 self.addAction(action)
259 259 self.select_all_action = action
260 260
261 261 self.increase_font_size = QtGui.QAction("Bigger Font",
262 262 self,
263 263 shortcut=QtGui.QKeySequence.ZoomIn,
264 264 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
265 265 statusTip="Increase the font size by one point",
266 266 triggered=self._increase_font_size)
267 267 self.addAction(self.increase_font_size)
268 268
269 269 self.decrease_font_size = QtGui.QAction("Smaller Font",
270 270 self,
271 271 shortcut=QtGui.QKeySequence.ZoomOut,
272 272 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
273 273 statusTip="Decrease the font size by one point",
274 274 triggered=self._decrease_font_size)
275 275 self.addAction(self.decrease_font_size)
276 276
277 277 self.reset_font_size = QtGui.QAction("Normal Font",
278 278 self,
279 279 shortcut="Ctrl+0",
280 280 shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
281 281 statusTip="Restore the Normal font size",
282 282 triggered=self.reset_font)
283 283 self.addAction(self.reset_font_size)
284 284
285 285
286 286
287 287 def eventFilter(self, obj, event):
288 288 """ Reimplemented to ensure a console-like behavior in the underlying
289 289 text widgets.
290 290 """
291 291 etype = event.type()
292 292 if etype == QtCore.QEvent.KeyPress:
293 293
294 294 # Re-map keys for all filtered widgets.
295 295 key = event.key()
296 296 if self._control_key_down(event.modifiers()) and \
297 297 key in self._ctrl_down_remap:
298 298 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
299 299 self._ctrl_down_remap[key],
300 300 QtCore.Qt.NoModifier)
301 301 QtGui.qApp.sendEvent(obj, new_event)
302 302 return True
303 303
304 304 elif obj == self._control:
305 305 return self._event_filter_console_keypress(event)
306 306
307 307 elif obj == self._page_control:
308 308 return self._event_filter_page_keypress(event)
309 309
310 310 # Make middle-click paste safe.
311 311 elif etype == QtCore.QEvent.MouseButtonRelease and \
312 312 event.button() == QtCore.Qt.MidButton and \
313 313 obj == self._control.viewport():
314 314 cursor = self._control.cursorForPosition(event.pos())
315 315 self._control.setTextCursor(cursor)
316 316 self.paste(QtGui.QClipboard.Selection)
317 317 return True
318 318
319 319 # Manually adjust the scrollbars *after* a resize event is dispatched.
320 320 elif etype == QtCore.QEvent.Resize and not self._filter_resize:
321 321 self._filter_resize = True
322 322 QtGui.qApp.sendEvent(obj, event)
323 323 self._adjust_scrollbars()
324 324 self._filter_resize = False
325 325 return True
326 326
327 327 # Override shortcuts for all filtered widgets.
328 328 elif etype == QtCore.QEvent.ShortcutOverride and \
329 329 self.override_shortcuts and \
330 330 self._control_key_down(event.modifiers()) and \
331 331 event.key() in self._shortcuts:
332 332 event.accept()
333 333
334 334 # Ensure that drags are safe. The problem is that the drag starting
335 335 # logic, which determines whether the drag is a Copy or Move, is locked
336 336 # down in QTextControl. If the widget is editable, which it must be if
337 337 # we're not executing, the drag will be a Move. The following hack
338 338 # prevents QTextControl from deleting the text by clearing the selection
339 339 # when a drag leave event originating from this widget is dispatched.
340 340 # The fact that we have to clear the user's selection is unfortunate,
341 341 # but the alternative--trying to prevent Qt from using its hardwired
342 342 # drag logic and writing our own--is worse.
343 343 elif etype == QtCore.QEvent.DragEnter and \
344 344 obj == self._control.viewport() and \
345 345 event.source() == self._control.viewport():
346 346 self._filter_drag = True
347 347 elif etype == QtCore.QEvent.DragLeave and \
348 348 obj == self._control.viewport() and \
349 349 self._filter_drag:
350 350 cursor = self._control.textCursor()
351 351 cursor.clearSelection()
352 352 self._control.setTextCursor(cursor)
353 353 self._filter_drag = False
354 354
355 355 # Ensure that drops are safe.
356 356 elif etype == QtCore.QEvent.Drop and obj == self._control.viewport():
357 357 cursor = self._control.cursorForPosition(event.pos())
358 358 if self._in_buffer(cursor.position()):
359 359 text = event.mimeData().text()
360 360 self._insert_plain_text_into_buffer(cursor, text)
361 361
362 362 # Qt is expecting to get something here--drag and drop occurs in its
363 363 # own event loop. Send a DragLeave event to end it.
364 364 QtGui.qApp.sendEvent(obj, QtGui.QDragLeaveEvent())
365 365 return True
366 366
367 367 return super(ConsoleWidget, self).eventFilter(obj, event)
368 368
369 369 #---------------------------------------------------------------------------
370 370 # 'QWidget' interface
371 371 #---------------------------------------------------------------------------
372 372
373 373 def sizeHint(self):
374 374 """ Reimplemented to suggest a size that is 80 characters wide and
375 375 25 lines high.
376 376 """
377 377 font_metrics = QtGui.QFontMetrics(self.font)
378 378 margin = (self._control.frameWidth() +
379 379 self._control.document().documentMargin()) * 2
380 380 style = self.style()
381 381 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
382 382
383 383 # Note 1: Despite my best efforts to take the various margins into
384 384 # account, the width is still coming out a bit too small, so we include
385 385 # a fudge factor of one character here.
386 386 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
387 387 # to a Qt bug on certain Mac OS systems where it returns 0.
388 388 width = font_metrics.width(' ') * 81 + margin
389 389 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
390 390 if self.paging == 'hsplit':
391 391 width = width * 2 + splitwidth
392 392
393 393 height = font_metrics.height() * 25 + margin
394 394 if self.paging == 'vsplit':
395 395 height = height * 2 + splitwidth
396 396
397 397 return QtCore.QSize(width, height)
398 398
399 399 #---------------------------------------------------------------------------
400 400 # 'ConsoleWidget' public interface
401 401 #---------------------------------------------------------------------------
402 402
403 403 def can_copy(self):
404 404 """ Returns whether text can be copied to the clipboard.
405 405 """
406 406 return self._control.textCursor().hasSelection()
407 407
408 408 def can_cut(self):
409 409 """ Returns whether text can be cut to the clipboard.
410 410 """
411 411 cursor = self._control.textCursor()
412 412 return (cursor.hasSelection() and
413 413 self._in_buffer(cursor.anchor()) and
414 414 self._in_buffer(cursor.position()))
415 415
416 416 def can_paste(self):
417 417 """ Returns whether text can be pasted from the clipboard.
418 418 """
419 419 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
420 420 return bool(QtGui.QApplication.clipboard().text())
421 421 return False
422 422
423 423 def clear(self, keep_input=True):
424 424 """ Clear the console.
425 425
426 426 Parameters:
427 427 -----------
428 428 keep_input : bool, optional (default True)
429 429 If set, restores the old input buffer if a new prompt is written.
430 430 """
431 431 if self._executing:
432 432 self._control.clear()
433 433 else:
434 434 if keep_input:
435 435 input_buffer = self.input_buffer
436 436 self._control.clear()
437 437 self._show_prompt()
438 438 if keep_input:
439 439 self.input_buffer = input_buffer
440 440
441 441 def copy(self):
442 442 """ Copy the currently selected text to the clipboard.
443 443 """
444 444 self._control.copy()
445 445
446 446 def cut(self):
447 447 """ Copy the currently selected text to the clipboard and delete it
448 448 if it's inside the input buffer.
449 449 """
450 450 self.copy()
451 451 if self.can_cut():
452 452 self._control.textCursor().removeSelectedText()
453 453
454 454 def execute(self, source=None, hidden=False, interactive=False):
455 455 """ Executes source or the input buffer, possibly prompting for more
456 456 input.
457 457
458 458 Parameters:
459 459 -----------
460 460 source : str, optional
461 461
462 462 The source to execute. If not specified, the input buffer will be
463 463 used. If specified and 'hidden' is False, the input buffer will be
464 464 replaced with the source before execution.
465 465
466 466 hidden : bool, optional (default False)
467 467
468 468 If set, no output will be shown and the prompt will not be modified.
469 469 In other words, it will be completely invisible to the user that
470 470 an execution has occurred.
471 471
472 472 interactive : bool, optional (default False)
473 473
474 474 Whether the console is to treat the source as having been manually
475 475 entered by the user. The effect of this parameter depends on the
476 476 subclass implementation.
477 477
478 478 Raises:
479 479 -------
480 480 RuntimeError
481 481 If incomplete input is given and 'hidden' is True. In this case,
482 482 it is not possible to prompt for more input.
483 483
484 484 Returns:
485 485 --------
486 486 A boolean indicating whether the source was executed.
487 487 """
488 488 # WARNING: The order in which things happen here is very particular, in
489 489 # large part because our syntax highlighting is fragile. If you change
490 490 # something, test carefully!
491 491
492 492 # Decide what to execute.
493 493 if source is None:
494 494 source = self.input_buffer
495 495 if not hidden:
496 496 # A newline is appended later, but it should be considered part
497 497 # of the input buffer.
498 498 source += '\n'
499 499 elif not hidden:
500 500 self.input_buffer = source
501 501
502 502 # Execute the source or show a continuation prompt if it is incomplete.
503 503 complete = self._is_complete(source, interactive)
504 504 if hidden:
505 505 if complete:
506 506 self._execute(source, hidden)
507 507 else:
508 508 error = 'Incomplete noninteractive input: "%s"'
509 509 raise RuntimeError(error % source)
510 510 else:
511 511 if complete:
512 512 self._append_plain_text('\n')
513 513 self._input_buffer_executing = self.input_buffer
514 514 self._executing = True
515 515 self._prompt_finished()
516 516
517 517 # The maximum block count is only in effect during execution.
518 518 # This ensures that _prompt_pos does not become invalid due to
519 519 # text truncation.
520 520 self._control.document().setMaximumBlockCount(self.buffer_size)
521 521
522 522 # Setting a positive maximum block count will automatically
523 523 # disable the undo/redo history, but just to be safe:
524 524 self._control.setUndoRedoEnabled(False)
525 525
526 526 # Perform actual execution.
527 527 self._execute(source, hidden)
528 528
529 529 else:
530 530 # Do this inside an edit block so continuation prompts are
531 531 # removed seamlessly via undo/redo.
532 532 cursor = self._get_end_cursor()
533 533 cursor.beginEditBlock()
534 534 cursor.insertText('\n')
535 535 self._insert_continuation_prompt(cursor)
536 536 cursor.endEditBlock()
537 537
538 538 # Do not do this inside the edit block. It works as expected
539 539 # when using a QPlainTextEdit control, but does not have an
540 540 # effect when using a QTextEdit. I believe this is a Qt bug.
541 541 self._control.moveCursor(QtGui.QTextCursor.End)
542 542
543 543 return complete
544 544
545 545 def export_html(self):
546 546 """ Shows a dialog to export HTML/XML in various formats.
547 547 """
548 548 self._html_exporter.export()
549 549
550 550 def _get_input_buffer(self, force=False):
551 551 """ The text that the user has entered entered at the current prompt.
552 552
553 553 If the console is currently executing, the text that is executing will
554 554 always be returned.
555 555 """
556 556 # If we're executing, the input buffer may not even exist anymore due to
557 557 # the limit imposed by 'buffer_size'. Therefore, we store it.
558 558 if self._executing and not force:
559 559 return self._input_buffer_executing
560 560
561 561 cursor = self._get_end_cursor()
562 562 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
563 563 input_buffer = cursor.selection().toPlainText()
564 564
565 565 # Strip out continuation prompts.
566 566 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
567 567
568 568 def _set_input_buffer(self, string):
569 569 """ Sets the text in the input buffer.
570 570
571 571 If the console is currently executing, this call has no *immediate*
572 572 effect. When the execution is finished, the input buffer will be updated
573 573 appropriately.
574 574 """
575 575 # If we're executing, store the text for later.
576 576 if self._executing:
577 577 self._input_buffer_pending = string
578 578 return
579 579
580 580 # Remove old text.
581 581 cursor = self._get_end_cursor()
582 582 cursor.beginEditBlock()
583 583 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
584 584 cursor.removeSelectedText()
585 585
586 586 # Insert new text with continuation prompts.
587 587 self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
588 588 cursor.endEditBlock()
589 589 self._control.moveCursor(QtGui.QTextCursor.End)
590 590
591 591 input_buffer = property(_get_input_buffer, _set_input_buffer)
592 592
593 593 def _get_font(self):
594 594 """ The base font being used by the ConsoleWidget.
595 595 """
596 596 return self._control.document().defaultFont()
597 597
598 598 def _set_font(self, font):
599 599 """ Sets the base font for the ConsoleWidget to the specified QFont.
600 600 """
601 601 font_metrics = QtGui.QFontMetrics(font)
602 602 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
603 603
604 604 self._completion_widget.setFont(font)
605 605 self._control.document().setDefaultFont(font)
606 606 if self._page_control:
607 607 self._page_control.document().setDefaultFont(font)
608 608
609 609 self.font_changed.emit(font)
610 610
611 611 font = property(_get_font, _set_font)
612 612
613 613 def paste(self, mode=QtGui.QClipboard.Clipboard):
614 614 """ Paste the contents of the clipboard into the input region.
615 615
616 616 Parameters:
617 617 -----------
618 618 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
619 619
620 620 Controls which part of the system clipboard is used. This can be
621 621 used to access the selection clipboard in X11 and the Find buffer
622 622 in Mac OS. By default, the regular clipboard is used.
623 623 """
624 624 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
625 625 # Make sure the paste is safe.
626 626 self._keep_cursor_in_buffer()
627 627 cursor = self._control.textCursor()
628 628
629 629 # Remove any trailing newline, which confuses the GUI and forces the
630 630 # user to backspace.
631 631 text = QtGui.QApplication.clipboard().text(mode).rstrip()
632 632 self._insert_plain_text_into_buffer(cursor, dedent(text))
633 633
634 634 def print_(self, printer = None):
635 635 """ Print the contents of the ConsoleWidget to the specified QPrinter.
636 636 """
637 637 if (not printer):
638 638 printer = QtGui.QPrinter()
639 639 if(QtGui.QPrintDialog(printer).exec_() != QtGui.QDialog.Accepted):
640 640 return
641 641 self._control.print_(printer)
642 642
643 643 def prompt_to_top(self):
644 644 """ Moves the prompt to the top of the viewport.
645 645 """
646 646 if not self._executing:
647 647 prompt_cursor = self._get_prompt_cursor()
648 648 if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
649 649 self._set_cursor(prompt_cursor)
650 650 self._set_top_cursor(prompt_cursor)
651 651
652 652 def redo(self):
653 653 """ Redo the last operation. If there is no operation to redo, nothing
654 654 happens.
655 655 """
656 656 self._control.redo()
657 657
658 658 def reset_font(self):
659 659 """ Sets the font to the default fixed-width font for this platform.
660 660 """
661 661 if sys.platform == 'win32':
662 662 # Consolas ships with Vista/Win7, fallback to Courier if needed
663 663 fallback = 'Courier'
664 664 elif sys.platform == 'darwin':
665 665 # OSX always has Monaco
666 666 fallback = 'Monaco'
667 667 else:
668 668 # Monospace should always exist
669 669 fallback = 'Monospace'
670 670 font = get_font(self.font_family, fallback)
671 671 if self.font_size:
672 672 font.setPointSize(self.font_size)
673 673 else:
674 674 font.setPointSize(QtGui.qApp.font().pointSize())
675 675 font.setStyleHint(QtGui.QFont.TypeWriter)
676 676 self._set_font(font)
677 677
678 678 def change_font_size(self, delta):
679 679 """Change the font size by the specified amount (in points).
680 680 """
681 681 font = self.font
682 682 size = max(font.pointSize() + delta, 1) # minimum 1 point
683 683 font.setPointSize(size)
684 684 self._set_font(font)
685 685
686 686 def _increase_font_size(self):
687 687 self.change_font_size(1)
688 688
689 689 def _decrease_font_size(self):
690 690 self.change_font_size(-1)
691 691
692 692 def select_all(self):
693 693 """ Selects all the text in the buffer.
694 694 """
695 695 self._control.selectAll()
696 696
697 697 def _get_tab_width(self):
698 698 """ The width (in terms of space characters) for tab characters.
699 699 """
700 700 return self._tab_width
701 701
702 702 def _set_tab_width(self, tab_width):
703 703 """ Sets the width (in terms of space characters) for tab characters.
704 704 """
705 705 font_metrics = QtGui.QFontMetrics(self.font)
706 706 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
707 707
708 708 self._tab_width = tab_width
709 709
710 710 tab_width = property(_get_tab_width, _set_tab_width)
711 711
712 712 def undo(self):
713 713 """ Undo the last operation. If there is no operation to undo, nothing
714 714 happens.
715 715 """
716 716 self._control.undo()
717 717
718 718 #---------------------------------------------------------------------------
719 719 # 'ConsoleWidget' abstract interface
720 720 #---------------------------------------------------------------------------
721 721
722 722 def _is_complete(self, source, interactive):
723 723 """ Returns whether 'source' can be executed. When triggered by an
724 724 Enter/Return key press, 'interactive' is True; otherwise, it is
725 725 False.
726 726 """
727 727 raise NotImplementedError
728 728
729 729 def _execute(self, source, hidden):
730 730 """ Execute 'source'. If 'hidden', do not show any output.
731 731 """
732 732 raise NotImplementedError
733 733
734 734 def _prompt_started_hook(self):
735 735 """ Called immediately after a new prompt is displayed.
736 736 """
737 737 pass
738 738
739 739 def _prompt_finished_hook(self):
740 740 """ Called immediately after a prompt is finished, i.e. when some input
741 741 will be processed and a new prompt displayed.
742 742 """
743 743 pass
744 744
745 745 def _up_pressed(self, shift_modifier):
746 746 """ Called when the up key is pressed. Returns whether to continue
747 747 processing the event.
748 748 """
749 749 return True
750 750
751 751 def _down_pressed(self, shift_modifier):
752 752 """ Called when the down key is pressed. Returns whether to continue
753 753 processing the event.
754 754 """
755 755 return True
756 756
757 757 def _tab_pressed(self):
758 758 """ Called when the tab key is pressed. Returns whether to continue
759 759 processing the event.
760 760 """
761 761 return False
762 762
763 763 #--------------------------------------------------------------------------
764 764 # 'ConsoleWidget' protected interface
765 765 #--------------------------------------------------------------------------
766 766
767 767 def _append_custom(self, insert, input, before_prompt=False):
768 768 """ A low-level method for appending content to the end of the buffer.
769 769
770 770 If 'before_prompt' is enabled, the content will be inserted before the
771 771 current prompt, if there is one.
772 772 """
773 773 # Determine where to insert the content.
774 774 cursor = self._control.textCursor()
775 775 if before_prompt and not self._executing:
776 776 cursor.setPosition(self._append_before_prompt_pos)
777 777 else:
778 778 cursor.movePosition(QtGui.QTextCursor.End)
779 779 start_pos = cursor.position()
780 780
781 781 # Perform the insertion.
782 782 result = insert(cursor, input)
783 783
784 784 # Adjust the prompt position if we have inserted before it. This is safe
785 785 # because buffer truncation is disabled when not executing.
786 786 if before_prompt and not self._executing:
787 787 diff = cursor.position() - start_pos
788 788 self._append_before_prompt_pos += diff
789 789 self._prompt_pos += diff
790 790
791 791 return result
792 792
793 793 def _append_html(self, html, before_prompt=False):
794 794 """ Appends HTML at the end of the console buffer.
795 795 """
796 796 self._append_custom(self._insert_html, html, before_prompt)
797 797
798 798 def _append_html_fetching_plain_text(self, html, before_prompt=False):
799 799 """ Appends HTML, then returns the plain text version of it.
800 800 """
801 801 return self._append_custom(self._insert_html_fetching_plain_text,
802 802 html, before_prompt)
803 803
804 804 def _append_plain_text(self, text, before_prompt=False):
805 805 """ Appends plain text, processing ANSI codes if enabled.
806 806 """
807 807 self._append_custom(self._insert_plain_text, text, before_prompt)
808 808
809 809 def _cancel_text_completion(self):
810 810 """ If text completion is progress, cancel it.
811 811 """
812 812 if self._text_completing_pos:
813 813 self._clear_temporary_buffer()
814 814 self._text_completing_pos = 0
815 815
816 816 def _clear_temporary_buffer(self):
817 817 """ Clears the "temporary text" buffer, i.e. all the text following
818 818 the prompt region.
819 819 """
820 820 # Select and remove all text below the input buffer.
821 821 cursor = self._get_prompt_cursor()
822 822 prompt = self._continuation_prompt.lstrip()
823 823 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
824 824 temp_cursor = QtGui.QTextCursor(cursor)
825 825 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
826 826 text = temp_cursor.selection().toPlainText().lstrip()
827 827 if not text.startswith(prompt):
828 828 break
829 829 else:
830 830 # We've reached the end of the input buffer and no text follows.
831 831 return
832 832 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
833 833 cursor.movePosition(QtGui.QTextCursor.End,
834 834 QtGui.QTextCursor.KeepAnchor)
835 835 cursor.removeSelectedText()
836 836
837 837 # After doing this, we have no choice but to clear the undo/redo
838 838 # history. Otherwise, the text is not "temporary" at all, because it
839 839 # can be recalled with undo/redo. Unfortunately, Qt does not expose
840 840 # fine-grained control to the undo/redo system.
841 841 if self._control.isUndoRedoEnabled():
842 842 self._control.setUndoRedoEnabled(False)
843 843 self._control.setUndoRedoEnabled(True)
844 844
845 845 def _complete_with_items(self, cursor, items):
846 846 """ Performs completion with 'items' at the specified cursor location.
847 847 """
848 848 self._cancel_text_completion()
849 849
850 850 if len(items) == 1:
851 851 cursor.setPosition(self._control.textCursor().position(),
852 852 QtGui.QTextCursor.KeepAnchor)
853 853 cursor.insertText(items[0])
854 854
855 855 elif len(items) > 1:
856 856 current_pos = self._control.textCursor().position()
857 857 prefix = commonprefix(items)
858 858 if prefix:
859 859 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
860 860 cursor.insertText(prefix)
861 861 current_pos = cursor.position()
862 862
863 863 if self.gui_completion:
864 864 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
865 865 self._completion_widget.show_items(cursor, items)
866 866 else:
867 867 cursor.beginEditBlock()
868 868 self._append_plain_text('\n')
869 869 self._page(self._format_as_columns(items))
870 870 cursor.endEditBlock()
871 871
872 872 cursor.setPosition(current_pos)
873 873 self._control.moveCursor(QtGui.QTextCursor.End)
874 874 self._control.setTextCursor(cursor)
875 875 self._text_completing_pos = current_pos
876 876
877 877 def _context_menu_make(self, pos):
878 878 """ Creates a context menu for the given QPoint (in widget coordinates).
879 879 """
880 880 menu = QtGui.QMenu(self)
881 881
882 882 self.cut_action = menu.addAction('Cut', self.cut)
883 883 self.cut_action.setEnabled(self.can_cut())
884 884 self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
885 885
886 886 self.copy_action = menu.addAction('Copy', self.copy)
887 887 self.copy_action.setEnabled(self.can_copy())
888 888 self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
889 889
890 890 self.paste_action = menu.addAction('Paste', self.paste)
891 891 self.paste_action.setEnabled(self.can_paste())
892 892 self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
893 893
894 894 menu.addSeparator()
895 895 menu.addAction(self.select_all_action)
896 896
897 897 menu.addSeparator()
898 898 menu.addAction(self.export_action)
899 899 menu.addAction(self.print_action)
900 900
901 901 return menu
902 902
903 903 def _control_key_down(self, modifiers, include_command=False):
904 904 """ Given a KeyboardModifiers flags object, return whether the Control
905 905 key is down.
906 906
907 907 Parameters:
908 908 -----------
909 909 include_command : bool, optional (default True)
910 910 Whether to treat the Command key as a (mutually exclusive) synonym
911 911 for Control when in Mac OS.
912 912 """
913 913 # Note that on Mac OS, ControlModifier corresponds to the Command key
914 914 # while MetaModifier corresponds to the Control key.
915 915 if sys.platform == 'darwin':
916 916 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
917 917 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
918 918 else:
919 919 return bool(modifiers & QtCore.Qt.ControlModifier)
920 920
921 921 def _create_control(self):
922 922 """ Creates and connects the underlying text widget.
923 923 """
924 924 # Create the underlying control.
925 925 if self.kind == 'plain':
926 926 control = QtGui.QPlainTextEdit()
927 927 elif self.kind == 'rich':
928 928 control = QtGui.QTextEdit()
929 929 control.setAcceptRichText(False)
930 930
931 931 # Install event filters. The filter on the viewport is needed for
932 932 # mouse events and drag events.
933 933 control.installEventFilter(self)
934 934 control.viewport().installEventFilter(self)
935 935
936 936 # Connect signals.
937 937 control.cursorPositionChanged.connect(self._cursor_position_changed)
938 938 control.customContextMenuRequested.connect(
939 939 self._custom_context_menu_requested)
940 940 control.copyAvailable.connect(self.copy_available)
941 941 control.redoAvailable.connect(self.redo_available)
942 942 control.undoAvailable.connect(self.undo_available)
943 943
944 944 # Hijack the document size change signal to prevent Qt from adjusting
945 945 # the viewport's scrollbar. We are relying on an implementation detail
946 946 # of Q(Plain)TextEdit here, which is potentially dangerous, but without
947 947 # this functionality we cannot create a nice terminal interface.
948 948 layout = control.document().documentLayout()
949 949 layout.documentSizeChanged.disconnect()
950 950 layout.documentSizeChanged.connect(self._adjust_scrollbars)
951 951
952 952 # Configure the control.
953 953 control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
954 954 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
955 955 control.setReadOnly(True)
956 956 control.setUndoRedoEnabled(False)
957 957 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
958 958 return control
959 959
960 960 def _create_page_control(self):
961 961 """ Creates and connects the underlying paging widget.
962 962 """
963 963 if self.kind == 'plain':
964 964 control = QtGui.QPlainTextEdit()
965 965 elif self.kind == 'rich':
966 966 control = QtGui.QTextEdit()
967 967 control.installEventFilter(self)
968 968 control.setReadOnly(True)
969 969 control.setUndoRedoEnabled(False)
970 970 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
971 971 return control
972 972
973 973 def _event_filter_console_keypress(self, event):
974 974 """ Filter key events for the underlying text widget to create a
975 975 console-like interface.
976 976 """
977 977 intercepted = False
978 978 cursor = self._control.textCursor()
979 979 position = cursor.position()
980 980 key = event.key()
981 981 ctrl_down = self._control_key_down(event.modifiers())
982 982 alt_down = event.modifiers() & QtCore.Qt.AltModifier
983 983 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
984 984
985 985 #------ Special sequences ----------------------------------------------
986 986
987 987 if event.matches(QtGui.QKeySequence.Copy):
988 988 self.copy()
989 989 intercepted = True
990 990
991 991 elif event.matches(QtGui.QKeySequence.Cut):
992 992 self.cut()
993 993 intercepted = True
994 994
995 995 elif event.matches(QtGui.QKeySequence.Paste):
996 996 self.paste()
997 997 intercepted = True
998 998
999 999 #------ Special modifier logic -----------------------------------------
1000 1000
1001 1001 elif key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
1002 1002 intercepted = True
1003 1003
1004 1004 # Special handling when tab completing in text mode.
1005 1005 self._cancel_text_completion()
1006 1006
1007 1007 if self._in_buffer(position):
1008 1008 # Special handling when a reading a line of raw input.
1009 1009 if self._reading:
1010 1010 self._append_plain_text('\n')
1011 1011 self._reading = False
1012 1012 if self._reading_callback:
1013 1013 self._reading_callback()
1014 1014
1015 1015 # If the input buffer is a single line or there is only
1016 1016 # whitespace after the cursor, execute. Otherwise, split the
1017 1017 # line with a continuation prompt.
1018 1018 elif not self._executing:
1019 1019 cursor.movePosition(QtGui.QTextCursor.End,
1020 1020 QtGui.QTextCursor.KeepAnchor)
1021 1021 at_end = len(cursor.selectedText().strip()) == 0
1022 1022 single_line = (self._get_end_cursor().blockNumber() ==
1023 1023 self._get_prompt_cursor().blockNumber())
1024 1024 if (at_end or shift_down or single_line) and not ctrl_down:
1025 1025 self.execute(interactive = not shift_down)
1026 1026 else:
1027 1027 # Do this inside an edit block for clean undo/redo.
1028 1028 cursor.beginEditBlock()
1029 1029 cursor.setPosition(position)
1030 1030 cursor.insertText('\n')
1031 1031 self._insert_continuation_prompt(cursor)
1032 1032 cursor.endEditBlock()
1033 1033
1034 1034 # Ensure that the whole input buffer is visible.
1035 1035 # FIXME: This will not be usable if the input buffer is
1036 1036 # taller than the console widget.
1037 1037 self._control.moveCursor(QtGui.QTextCursor.End)
1038 1038 self._control.setTextCursor(cursor)
1039 1039
1040 1040 #------ Control/Cmd modifier -------------------------------------------
1041 1041
1042 1042 elif ctrl_down:
1043 1043 if key == QtCore.Qt.Key_G:
1044 1044 self._keyboard_quit()
1045 1045 intercepted = True
1046 1046
1047 1047 elif key == QtCore.Qt.Key_K:
1048 1048 if self._in_buffer(position):
1049 1049 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
1050 1050 QtGui.QTextCursor.KeepAnchor)
1051 1051 if not cursor.hasSelection():
1052 1052 # Line deletion (remove continuation prompt)
1053 1053 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1054 1054 QtGui.QTextCursor.KeepAnchor)
1055 1055 cursor.movePosition(QtGui.QTextCursor.Right,
1056 1056 QtGui.QTextCursor.KeepAnchor,
1057 1057 len(self._continuation_prompt))
1058 1058 self._kill_ring.kill_cursor(cursor)
1059 1059 intercepted = True
1060 1060
1061 1061 elif key == QtCore.Qt.Key_L:
1062 1062 self.prompt_to_top()
1063 1063 intercepted = True
1064 1064
1065 1065 elif key == QtCore.Qt.Key_O:
1066 1066 if self._page_control and self._page_control.isVisible():
1067 1067 self._page_control.setFocus()
1068 1068 intercepted = True
1069 1069
1070 1070 elif key == QtCore.Qt.Key_U:
1071 1071 if self._in_buffer(position):
1072 1072 start_line = cursor.blockNumber()
1073 1073 if start_line == self._get_prompt_cursor().blockNumber():
1074 1074 offset = len(self._prompt)
1075 1075 else:
1076 1076 offset = len(self._continuation_prompt)
1077 1077 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1078 1078 QtGui.QTextCursor.KeepAnchor)
1079 1079 cursor.movePosition(QtGui.QTextCursor.Right,
1080 1080 QtGui.QTextCursor.KeepAnchor, offset)
1081 1081 self._kill_ring.kill_cursor(cursor)
1082 1082 intercepted = True
1083 1083
1084 1084 elif key == QtCore.Qt.Key_Y:
1085 1085 self._keep_cursor_in_buffer()
1086 1086 self._kill_ring.yank()
1087 1087 intercepted = True
1088 1088
1089 1089 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
1090 1090 if key == QtCore.Qt.Key_Backspace:
1091 1091 cursor = self._get_word_start_cursor(position)
1092 1092 else: # key == QtCore.Qt.Key_Delete
1093 1093 cursor = self._get_word_end_cursor(position)
1094 1094 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1095 1095 self._kill_ring.kill_cursor(cursor)
1096 1096 intercepted = True
1097 1097
1098 1098 #------ Alt modifier ---------------------------------------------------
1099 1099
1100 1100 elif alt_down:
1101 1101 if key == QtCore.Qt.Key_B:
1102 1102 self._set_cursor(self._get_word_start_cursor(position))
1103 1103 intercepted = True
1104 1104
1105 1105 elif key == QtCore.Qt.Key_F:
1106 1106 self._set_cursor(self._get_word_end_cursor(position))
1107 1107 intercepted = True
1108 1108
1109 1109 elif key == QtCore.Qt.Key_Y:
1110 1110 self._kill_ring.rotate()
1111 1111 intercepted = True
1112 1112
1113 1113 elif key == QtCore.Qt.Key_Backspace:
1114 1114 cursor = self._get_word_start_cursor(position)
1115 1115 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1116 1116 self._kill_ring.kill_cursor(cursor)
1117 1117 intercepted = True
1118 1118
1119 1119 elif key == QtCore.Qt.Key_D:
1120 1120 cursor = self._get_word_end_cursor(position)
1121 1121 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
1122 1122 self._kill_ring.kill_cursor(cursor)
1123 1123 intercepted = True
1124 1124
1125 1125 elif key == QtCore.Qt.Key_Delete:
1126 1126 intercepted = True
1127 1127
1128 1128 elif key == QtCore.Qt.Key_Greater:
1129 1129 self._control.moveCursor(QtGui.QTextCursor.End)
1130 1130 intercepted = True
1131 1131
1132 1132 elif key == QtCore.Qt.Key_Less:
1133 1133 self._control.setTextCursor(self._get_prompt_cursor())
1134 1134 intercepted = True
1135 1135
1136 1136 #------ No modifiers ---------------------------------------------------
1137 1137
1138 1138 else:
1139 1139 if shift_down:
1140 1140 anchormode = QtGui.QTextCursor.KeepAnchor
1141 1141 else:
1142 1142 anchormode = QtGui.QTextCursor.MoveAnchor
1143 1143
1144 1144 if key == QtCore.Qt.Key_Escape:
1145 1145 self._keyboard_quit()
1146 1146 intercepted = True
1147 1147
1148 1148 elif key == QtCore.Qt.Key_Up:
1149 1149 if self._reading or not self._up_pressed(shift_down):
1150 1150 intercepted = True
1151 1151 else:
1152 1152 prompt_line = self._get_prompt_cursor().blockNumber()
1153 1153 intercepted = cursor.blockNumber() <= prompt_line
1154 1154
1155 1155 elif key == QtCore.Qt.Key_Down:
1156 1156 if self._reading or not self._down_pressed(shift_down):
1157 1157 intercepted = True
1158 1158 else:
1159 1159 end_line = self._get_end_cursor().blockNumber()
1160 1160 intercepted = cursor.blockNumber() == end_line
1161 1161
1162 1162 elif key == QtCore.Qt.Key_Tab:
1163 1163 if not self._reading:
1164 1164 if self._tab_pressed():
1165 1165 # real tab-key, insert four spaces
1166 1166 cursor.insertText(' '*4)
1167 1167 intercepted = True
1168 1168
1169 1169 elif key == QtCore.Qt.Key_Left:
1170 1170
1171 1171 # Move to the previous line
1172 1172 line, col = cursor.blockNumber(), cursor.columnNumber()
1173 1173 if line > self._get_prompt_cursor().blockNumber() and \
1174 1174 col == len(self._continuation_prompt):
1175 1175 self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
1176 1176 mode=anchormode)
1177 1177 self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
1178 1178 mode=anchormode)
1179 1179 intercepted = True
1180 1180
1181 1181 # Regular left movement
1182 1182 else:
1183 1183 intercepted = not self._in_buffer(position - 1)
1184 1184
1185 1185 elif key == QtCore.Qt.Key_Right:
1186 1186 original_block_number = cursor.blockNumber()
1187 1187 cursor.movePosition(QtGui.QTextCursor.Right,
1188 1188 mode=anchormode)
1189 1189 if cursor.blockNumber() != original_block_number:
1190 1190 cursor.movePosition(QtGui.QTextCursor.Right,
1191 1191 n=len(self._continuation_prompt),
1192 1192 mode=anchormode)
1193 1193 self._set_cursor(cursor)
1194 1194 intercepted = True
1195 1195
1196 1196 elif key == QtCore.Qt.Key_Home:
1197 1197 start_line = cursor.blockNumber()
1198 1198 if start_line == self._get_prompt_cursor().blockNumber():
1199 1199 start_pos = self._prompt_pos
1200 1200 else:
1201 1201 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1202 1202 QtGui.QTextCursor.KeepAnchor)
1203 1203 start_pos = cursor.position()
1204 1204 start_pos += len(self._continuation_prompt)
1205 1205 cursor.setPosition(position)
1206 1206 if shift_down and self._in_buffer(position):
1207 1207 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
1208 1208 else:
1209 1209 cursor.setPosition(start_pos)
1210 1210 self._set_cursor(cursor)
1211 1211 intercepted = True
1212 1212
1213 1213 elif key == QtCore.Qt.Key_Backspace:
1214 1214
1215 1215 # Line deletion (remove continuation prompt)
1216 1216 line, col = cursor.blockNumber(), cursor.columnNumber()
1217 1217 if not self._reading and \
1218 1218 col == len(self._continuation_prompt) and \
1219 1219 line > self._get_prompt_cursor().blockNumber():
1220 1220 cursor.beginEditBlock()
1221 1221 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
1222 1222 QtGui.QTextCursor.KeepAnchor)
1223 1223 cursor.removeSelectedText()
1224 1224 cursor.deletePreviousChar()
1225 1225 cursor.endEditBlock()
1226 1226 intercepted = True
1227 1227
1228 1228 # Regular backwards deletion
1229 1229 else:
1230 1230 anchor = cursor.anchor()
1231 1231 if anchor == position:
1232 1232 intercepted = not self._in_buffer(position - 1)
1233 1233 else:
1234 1234 intercepted = not self._in_buffer(min(anchor, position))
1235 1235
1236 1236 elif key == QtCore.Qt.Key_Delete:
1237 1237
1238 1238 # Line deletion (remove continuation prompt)
1239 1239 if not self._reading and self._in_buffer(position) and \
1240 1240 cursor.atBlockEnd() and not cursor.hasSelection():
1241 1241 cursor.movePosition(QtGui.QTextCursor.NextBlock,
1242 1242 QtGui.QTextCursor.KeepAnchor)
1243 1243 cursor.movePosition(QtGui.QTextCursor.Right,
1244 1244 QtGui.QTextCursor.KeepAnchor,
1245 1245 len(self._continuation_prompt))
1246 1246 cursor.removeSelectedText()
1247 1247 intercepted = True
1248 1248
1249 1249 # Regular forwards deletion:
1250 1250 else:
1251 1251 anchor = cursor.anchor()
1252 1252 intercepted = (not self._in_buffer(anchor) or
1253 1253 not self._in_buffer(position))
1254 1254
1255 1255 # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1256 1256 # using the keyboard in any part of the buffer. Also, permit scrolling
1257 1257 # with Page Up/Down keys. Finally, if we're executing, don't move the
1258 1258 # cursor (if even this made sense, we can't guarantee that the prompt
1259 1259 # position is still valid due to text truncation).
1260 1260 if not (self._control_key_down(event.modifiers(), include_command=True)
1261 1261 or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1262 1262 or (self._executing and not self._reading)):
1263 1263 self._keep_cursor_in_buffer()
1264 1264
1265 1265 return intercepted
1266 1266
1267 1267 def _event_filter_page_keypress(self, event):
1268 1268 """ Filter key events for the paging widget to create console-like
1269 1269 interface.
1270 1270 """
1271 1271 key = event.key()
1272 1272 ctrl_down = self._control_key_down(event.modifiers())
1273 1273 alt_down = event.modifiers() & QtCore.Qt.AltModifier
1274 1274
1275 1275 if ctrl_down:
1276 1276 if key == QtCore.Qt.Key_O:
1277 1277 self._control.setFocus()
1278 1278 intercept = True
1279 1279
1280 1280 elif alt_down:
1281 1281 if key == QtCore.Qt.Key_Greater:
1282 1282 self._page_control.moveCursor(QtGui.QTextCursor.End)
1283 1283 intercepted = True
1284 1284
1285 1285 elif key == QtCore.Qt.Key_Less:
1286 1286 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1287 1287 intercepted = True
1288 1288
1289 1289 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
1290 1290 if self._splitter:
1291 1291 self._page_control.hide()
1292 1292 self._control.setFocus()
1293 1293 else:
1294 1294 self.layout().setCurrentWidget(self._control)
1295 1295 return True
1296 1296
1297 1297 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
1298 1298 QtCore.Qt.Key_Tab):
1299 1299 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1300 1300 QtCore.Qt.Key_PageDown,
1301 1301 QtCore.Qt.NoModifier)
1302 1302 QtGui.qApp.sendEvent(self._page_control, new_event)
1303 1303 return True
1304 1304
1305 1305 elif key == QtCore.Qt.Key_Backspace:
1306 1306 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
1307 1307 QtCore.Qt.Key_PageUp,
1308 1308 QtCore.Qt.NoModifier)
1309 1309 QtGui.qApp.sendEvent(self._page_control, new_event)
1310 1310 return True
1311 1311
1312 1312 return False
1313 1313
1314 1314 def _format_as_columns(self, items, separator=' '):
1315 1315 """ Transform a list of strings into a single string with columns.
1316 1316
1317 1317 Parameters
1318 1318 ----------
1319 1319 items : sequence of strings
1320 1320 The strings to process.
1321 1321
1322 1322 separator : str, optional [default is two spaces]
1323 1323 The string that separates columns.
1324 1324
1325 1325 Returns
1326 1326 -------
1327 1327 The formatted string.
1328 1328 """
1329 1329 # Calculate the number of characters available.
1330 1330 width = self._control.viewport().width()
1331 1331 char_width = QtGui.QFontMetrics(self.font).width(' ')
1332 1332 displaywidth = max(10, (width / char_width) - 1)
1333 1333
1334 1334 return columnize(items, separator, displaywidth)
1335 1335
1336 1336 def _get_block_plain_text(self, block):
1337 1337 """ Given a QTextBlock, return its unformatted text.
1338 1338 """
1339 1339 cursor = QtGui.QTextCursor(block)
1340 1340 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1341 1341 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1342 1342 QtGui.QTextCursor.KeepAnchor)
1343 1343 return cursor.selection().toPlainText()
1344 1344
1345 1345 def _get_cursor(self):
1346 1346 """ Convenience method that returns a cursor for the current position.
1347 1347 """
1348 1348 return self._control.textCursor()
1349 1349
1350 1350 def _get_end_cursor(self):
1351 1351 """ Convenience method that returns a cursor for the last character.
1352 1352 """
1353 1353 cursor = self._control.textCursor()
1354 1354 cursor.movePosition(QtGui.QTextCursor.End)
1355 1355 return cursor
1356 1356
1357 1357 def _get_input_buffer_cursor_column(self):
1358 1358 """ Returns the column of the cursor in the input buffer, excluding the
1359 1359 contribution by the prompt, or -1 if there is no such column.
1360 1360 """
1361 1361 prompt = self._get_input_buffer_cursor_prompt()
1362 1362 if prompt is None:
1363 1363 return -1
1364 1364 else:
1365 1365 cursor = self._control.textCursor()
1366 1366 return cursor.columnNumber() - len(prompt)
1367 1367
1368 1368 def _get_input_buffer_cursor_line(self):
1369 1369 """ Returns the text of the line of the input buffer that contains the
1370 1370 cursor, or None if there is no such line.
1371 1371 """
1372 1372 prompt = self._get_input_buffer_cursor_prompt()
1373 1373 if prompt is None:
1374 1374 return None
1375 1375 else:
1376 1376 cursor = self._control.textCursor()
1377 1377 text = self._get_block_plain_text(cursor.block())
1378 1378 return text[len(prompt):]
1379 1379
1380 1380 def _get_input_buffer_cursor_prompt(self):
1381 1381 """ Returns the (plain text) prompt for line of the input buffer that
1382 1382 contains the cursor, or None if there is no such line.
1383 1383 """
1384 1384 if self._executing:
1385 1385 return None
1386 1386 cursor = self._control.textCursor()
1387 1387 if cursor.position() >= self._prompt_pos:
1388 1388 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1389 1389 return self._prompt
1390 1390 else:
1391 1391 return self._continuation_prompt
1392 1392 else:
1393 1393 return None
1394 1394
1395 1395 def _get_prompt_cursor(self):
1396 1396 """ Convenience method that returns a cursor for the prompt position.
1397 1397 """
1398 1398 cursor = self._control.textCursor()
1399 1399 cursor.setPosition(self._prompt_pos)
1400 1400 return cursor
1401 1401
1402 1402 def _get_selection_cursor(self, start, end):
1403 1403 """ Convenience method that returns a cursor with text selected between
1404 1404 the positions 'start' and 'end'.
1405 1405 """
1406 1406 cursor = self._control.textCursor()
1407 1407 cursor.setPosition(start)
1408 1408 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1409 1409 return cursor
1410 1410
1411 1411 def _get_word_start_cursor(self, position):
1412 1412 """ Find the start of the word to the left the given position. If a
1413 1413 sequence of non-word characters precedes the first word, skip over
1414 1414 them. (This emulates the behavior of bash, emacs, etc.)
1415 1415 """
1416 1416 document = self._control.document()
1417 1417 position -= 1
1418 1418 while position >= self._prompt_pos and \
1419 1419 not is_letter_or_number(document.characterAt(position)):
1420 1420 position -= 1
1421 1421 while position >= self._prompt_pos and \
1422 1422 is_letter_or_number(document.characterAt(position)):
1423 1423 position -= 1
1424 1424 cursor = self._control.textCursor()
1425 1425 cursor.setPosition(position + 1)
1426 1426 return cursor
1427 1427
1428 1428 def _get_word_end_cursor(self, position):
1429 1429 """ Find the end of the word to the right the given position. If a
1430 1430 sequence of non-word characters precedes the first word, skip over
1431 1431 them. (This emulates the behavior of bash, emacs, etc.)
1432 1432 """
1433 1433 document = self._control.document()
1434 1434 end = self._get_end_cursor().position()
1435 1435 while position < end and \
1436 1436 not is_letter_or_number(document.characterAt(position)):
1437 1437 position += 1
1438 1438 while position < end and \
1439 1439 is_letter_or_number(document.characterAt(position)):
1440 1440 position += 1
1441 1441 cursor = self._control.textCursor()
1442 1442 cursor.setPosition(position)
1443 1443 return cursor
1444 1444
1445 1445 def _insert_continuation_prompt(self, cursor):
1446 1446 """ Inserts new continuation prompt using the specified cursor.
1447 1447 """
1448 1448 if self._continuation_prompt_html is None:
1449 1449 self._insert_plain_text(cursor, self._continuation_prompt)
1450 1450 else:
1451 1451 self._continuation_prompt = self._insert_html_fetching_plain_text(
1452 1452 cursor, self._continuation_prompt_html)
1453 1453
1454 1454 def _insert_html(self, cursor, html):
1455 1455 """ Inserts HTML using the specified cursor in such a way that future
1456 1456 formatting is unaffected.
1457 1457 """
1458 1458 cursor.beginEditBlock()
1459 1459 cursor.insertHtml(html)
1460 1460
1461 1461 # After inserting HTML, the text document "remembers" it's in "html
1462 1462 # mode", which means that subsequent calls adding plain text will result
1463 1463 # in unwanted formatting, lost tab characters, etc. The following code
1464 1464 # hacks around this behavior, which I consider to be a bug in Qt, by
1465 1465 # (crudely) resetting the document's style state.
1466 1466 cursor.movePosition(QtGui.QTextCursor.Left,
1467 1467 QtGui.QTextCursor.KeepAnchor)
1468 1468 if cursor.selection().toPlainText() == ' ':
1469 1469 cursor.removeSelectedText()
1470 1470 else:
1471 1471 cursor.movePosition(QtGui.QTextCursor.Right)
1472 1472 cursor.insertText(' ', QtGui.QTextCharFormat())
1473 1473 cursor.endEditBlock()
1474 1474
1475 1475 def _insert_html_fetching_plain_text(self, cursor, html):
1476 1476 """ Inserts HTML using the specified cursor, then returns its plain text
1477 1477 version.
1478 1478 """
1479 1479 cursor.beginEditBlock()
1480 1480 cursor.removeSelectedText()
1481 1481
1482 1482 start = cursor.position()
1483 1483 self._insert_html(cursor, html)
1484 1484 end = cursor.position()
1485 1485 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1486 1486 text = cursor.selection().toPlainText()
1487 1487
1488 1488 cursor.setPosition(end)
1489 1489 cursor.endEditBlock()
1490 1490 return text
1491 1491
1492 1492 def _insert_plain_text(self, cursor, text):
1493 1493 """ Inserts plain text using the specified cursor, processing ANSI codes
1494 1494 if enabled.
1495 1495 """
1496 1496 cursor.beginEditBlock()
1497 1497 if self.ansi_codes:
1498 1498 for substring in self._ansi_processor.split_string(text):
1499 1499 for act in self._ansi_processor.actions:
1500 1500
1501 1501 # Unlike real terminal emulators, we don't distinguish
1502 1502 # between the screen and the scrollback buffer. A screen
1503 1503 # erase request clears everything.
1504 1504 if act.action == 'erase' and act.area == 'screen':
1505 1505 cursor.select(QtGui.QTextCursor.Document)
1506 1506 cursor.removeSelectedText()
1507 1507
1508 1508 # Simulate a form feed by scrolling just past the last line.
1509 1509 elif act.action == 'scroll' and act.unit == 'page':
1510 1510 cursor.insertText('\n')
1511 1511 cursor.endEditBlock()
1512 1512 self._set_top_cursor(cursor)
1513 1513 cursor.joinPreviousEditBlock()
1514 1514 cursor.deletePreviousChar()
1515 1515
1516 1516 elif act.action == 'carriage-return':
1517 1517 cursor.movePosition(
1518 1518 cursor.StartOfLine, cursor.KeepAnchor)
1519 1519
1520 elif act.action == 'beep':
1521 QtGui.qApp.beep()
1522
1520 1523 format = self._ansi_processor.get_format()
1521 1524 cursor.insertText(substring, format)
1522 1525 else:
1523 1526 cursor.insertText(text)
1524 1527 cursor.endEditBlock()
1525 1528
1526 1529 def _insert_plain_text_into_buffer(self, cursor, text):
1527 1530 """ Inserts text into the input buffer using the specified cursor (which
1528 1531 must be in the input buffer), ensuring that continuation prompts are
1529 1532 inserted as necessary.
1530 1533 """
1531 1534 lines = text.splitlines(True)
1532 1535 if lines:
1533 1536 cursor.beginEditBlock()
1534 1537 cursor.insertText(lines[0])
1535 1538 for line in lines[1:]:
1536 1539 if self._continuation_prompt_html is None:
1537 1540 cursor.insertText(self._continuation_prompt)
1538 1541 else:
1539 1542 self._continuation_prompt = \
1540 1543 self._insert_html_fetching_plain_text(
1541 1544 cursor, self._continuation_prompt_html)
1542 1545 cursor.insertText(line)
1543 1546 cursor.endEditBlock()
1544 1547
1545 1548 def _in_buffer(self, position=None):
1546 1549 """ Returns whether the current cursor (or, if specified, a position) is
1547 1550 inside the editing region.
1548 1551 """
1549 1552 cursor = self._control.textCursor()
1550 1553 if position is None:
1551 1554 position = cursor.position()
1552 1555 else:
1553 1556 cursor.setPosition(position)
1554 1557 line = cursor.blockNumber()
1555 1558 prompt_line = self._get_prompt_cursor().blockNumber()
1556 1559 if line == prompt_line:
1557 1560 return position >= self._prompt_pos
1558 1561 elif line > prompt_line:
1559 1562 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1560 1563 prompt_pos = cursor.position() + len(self._continuation_prompt)
1561 1564 return position >= prompt_pos
1562 1565 return False
1563 1566
1564 1567 def _keep_cursor_in_buffer(self):
1565 1568 """ Ensures that the cursor is inside the editing region. Returns
1566 1569 whether the cursor was moved.
1567 1570 """
1568 1571 moved = not self._in_buffer()
1569 1572 if moved:
1570 1573 cursor = self._control.textCursor()
1571 1574 cursor.movePosition(QtGui.QTextCursor.End)
1572 1575 self._control.setTextCursor(cursor)
1573 1576 return moved
1574 1577
1575 1578 def _keyboard_quit(self):
1576 1579 """ Cancels the current editing task ala Ctrl-G in Emacs.
1577 1580 """
1578 1581 if self._text_completing_pos:
1579 1582 self._cancel_text_completion()
1580 1583 else:
1581 1584 self.input_buffer = ''
1582 1585
1583 1586 def _page(self, text, html=False):
1584 1587 """ Displays text using the pager if it exceeds the height of the
1585 1588 viewport.
1586 1589
1587 1590 Parameters:
1588 1591 -----------
1589 1592 html : bool, optional (default False)
1590 1593 If set, the text will be interpreted as HTML instead of plain text.
1591 1594 """
1592 1595 line_height = QtGui.QFontMetrics(self.font).height()
1593 1596 minlines = self._control.viewport().height() / line_height
1594 1597 if self.paging != 'none' and \
1595 1598 re.match("(?:[^\n]*\n){%i}" % minlines, text):
1596 1599 if self.paging == 'custom':
1597 1600 self.custom_page_requested.emit(text)
1598 1601 else:
1599 1602 self._page_control.clear()
1600 1603 cursor = self._page_control.textCursor()
1601 1604 if html:
1602 1605 self._insert_html(cursor, text)
1603 1606 else:
1604 1607 self._insert_plain_text(cursor, text)
1605 1608 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1606 1609
1607 1610 self._page_control.viewport().resize(self._control.size())
1608 1611 if self._splitter:
1609 1612 self._page_control.show()
1610 1613 self._page_control.setFocus()
1611 1614 else:
1612 1615 self.layout().setCurrentWidget(self._page_control)
1613 1616 elif html:
1614 1617 self._append_plain_html(text)
1615 1618 else:
1616 1619 self._append_plain_text(text)
1617 1620
1618 1621 def _prompt_finished(self):
1619 1622 """ Called immediately after a prompt is finished, i.e. when some input
1620 1623 will be processed and a new prompt displayed.
1621 1624 """
1622 1625 self._control.setReadOnly(True)
1623 1626 self._prompt_finished_hook()
1624 1627
1625 1628 def _prompt_started(self):
1626 1629 """ Called immediately after a new prompt is displayed.
1627 1630 """
1628 1631 # Temporarily disable the maximum block count to permit undo/redo and
1629 1632 # to ensure that the prompt position does not change due to truncation.
1630 1633 self._control.document().setMaximumBlockCount(0)
1631 1634 self._control.setUndoRedoEnabled(True)
1632 1635
1633 1636 # Work around bug in QPlainTextEdit: input method is not re-enabled
1634 1637 # when read-only is disabled.
1635 1638 self._control.setReadOnly(False)
1636 1639 self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
1637 1640
1638 1641 if not self._reading:
1639 1642 self._executing = False
1640 1643 self._prompt_started_hook()
1641 1644
1642 1645 # If the input buffer has changed while executing, load it.
1643 1646 if self._input_buffer_pending:
1644 1647 self.input_buffer = self._input_buffer_pending
1645 1648 self._input_buffer_pending = ''
1646 1649
1647 1650 self._control.moveCursor(QtGui.QTextCursor.End)
1648 1651
1649 1652 def _readline(self, prompt='', callback=None):
1650 1653 """ Reads one line of input from the user.
1651 1654
1652 1655 Parameters
1653 1656 ----------
1654 1657 prompt : str, optional
1655 1658 The prompt to print before reading the line.
1656 1659
1657 1660 callback : callable, optional
1658 1661 A callback to execute with the read line. If not specified, input is
1659 1662 read *synchronously* and this method does not return until it has
1660 1663 been read.
1661 1664
1662 1665 Returns
1663 1666 -------
1664 1667 If a callback is specified, returns nothing. Otherwise, returns the
1665 1668 input string with the trailing newline stripped.
1666 1669 """
1667 1670 if self._reading:
1668 1671 raise RuntimeError('Cannot read a line. Widget is already reading.')
1669 1672
1670 1673 if not callback and not self.isVisible():
1671 1674 # If the user cannot see the widget, this function cannot return.
1672 1675 raise RuntimeError('Cannot synchronously read a line if the widget '
1673 1676 'is not visible!')
1674 1677
1675 1678 self._reading = True
1676 1679 self._show_prompt(prompt, newline=False)
1677 1680
1678 1681 if callback is None:
1679 1682 self._reading_callback = None
1680 1683 while self._reading:
1681 1684 QtCore.QCoreApplication.processEvents()
1682 1685 return self._get_input_buffer(force=True).rstrip('\n')
1683 1686
1684 1687 else:
1685 1688 self._reading_callback = lambda: \
1686 1689 callback(self._get_input_buffer(force=True).rstrip('\n'))
1687 1690
1688 1691 def _set_continuation_prompt(self, prompt, html=False):
1689 1692 """ Sets the continuation prompt.
1690 1693
1691 1694 Parameters
1692 1695 ----------
1693 1696 prompt : str
1694 1697 The prompt to show when more input is needed.
1695 1698
1696 1699 html : bool, optional (default False)
1697 1700 If set, the prompt will be inserted as formatted HTML. Otherwise,
1698 1701 the prompt will be treated as plain text, though ANSI color codes
1699 1702 will be handled.
1700 1703 """
1701 1704 if html:
1702 1705 self._continuation_prompt_html = prompt
1703 1706 else:
1704 1707 self._continuation_prompt = prompt
1705 1708 self._continuation_prompt_html = None
1706 1709
1707 1710 def _set_cursor(self, cursor):
1708 1711 """ Convenience method to set the current cursor.
1709 1712 """
1710 1713 self._control.setTextCursor(cursor)
1711 1714
1712 1715 def _set_top_cursor(self, cursor):
1713 1716 """ Scrolls the viewport so that the specified cursor is at the top.
1714 1717 """
1715 1718 scrollbar = self._control.verticalScrollBar()
1716 1719 scrollbar.setValue(scrollbar.maximum())
1717 1720 original_cursor = self._control.textCursor()
1718 1721 self._control.setTextCursor(cursor)
1719 1722 self._control.ensureCursorVisible()
1720 1723 self._control.setTextCursor(original_cursor)
1721 1724
1722 1725 def _show_prompt(self, prompt=None, html=False, newline=True):
1723 1726 """ Writes a new prompt at the end of the buffer.
1724 1727
1725 1728 Parameters
1726 1729 ----------
1727 1730 prompt : str, optional
1728 1731 The prompt to show. If not specified, the previous prompt is used.
1729 1732
1730 1733 html : bool, optional (default False)
1731 1734 Only relevant when a prompt is specified. If set, the prompt will
1732 1735 be inserted as formatted HTML. Otherwise, the prompt will be treated
1733 1736 as plain text, though ANSI color codes will be handled.
1734 1737
1735 1738 newline : bool, optional (default True)
1736 1739 If set, a new line will be written before showing the prompt if
1737 1740 there is not already a newline at the end of the buffer.
1738 1741 """
1739 1742 # Save the current end position to support _append*(before_prompt=True).
1740 1743 cursor = self._get_end_cursor()
1741 1744 self._append_before_prompt_pos = cursor.position()
1742 1745
1743 1746 # Insert a preliminary newline, if necessary.
1744 1747 if newline and cursor.position() > 0:
1745 1748 cursor.movePosition(QtGui.QTextCursor.Left,
1746 1749 QtGui.QTextCursor.KeepAnchor)
1747 1750 if cursor.selection().toPlainText() != '\n':
1748 1751 self._append_plain_text('\n')
1749 1752
1750 1753 # Write the prompt.
1751 1754 self._append_plain_text(self._prompt_sep)
1752 1755 if prompt is None:
1753 1756 if self._prompt_html is None:
1754 1757 self._append_plain_text(self._prompt)
1755 1758 else:
1756 1759 self._append_html(self._prompt_html)
1757 1760 else:
1758 1761 if html:
1759 1762 self._prompt = self._append_html_fetching_plain_text(prompt)
1760 1763 self._prompt_html = prompt
1761 1764 else:
1762 1765 self._append_plain_text(prompt)
1763 1766 self._prompt = prompt
1764 1767 self._prompt_html = None
1765 1768
1766 1769 self._prompt_pos = self._get_end_cursor().position()
1767 1770 self._prompt_started()
1768 1771
1769 1772 #------ Signal handlers ----------------------------------------------------
1770 1773
1771 1774 def _adjust_scrollbars(self):
1772 1775 """ Expands the vertical scrollbar beyond the range set by Qt.
1773 1776 """
1774 1777 # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
1775 1778 # and qtextedit.cpp.
1776 1779 document = self._control.document()
1777 1780 scrollbar = self._control.verticalScrollBar()
1778 1781 viewport_height = self._control.viewport().height()
1779 1782 if isinstance(self._control, QtGui.QPlainTextEdit):
1780 1783 maximum = max(0, document.lineCount() - 1)
1781 1784 step = viewport_height / self._control.fontMetrics().lineSpacing()
1782 1785 else:
1783 1786 # QTextEdit does not do line-based layout and blocks will not in
1784 1787 # general have the same height. Therefore it does not make sense to
1785 1788 # attempt to scroll in line height increments.
1786 1789 maximum = document.size().height()
1787 1790 step = viewport_height
1788 1791 diff = maximum - scrollbar.maximum()
1789 1792 scrollbar.setRange(0, maximum)
1790 1793 scrollbar.setPageStep(step)
1791 1794
1792 1795 # Compensate for undesirable scrolling that occurs automatically due to
1793 1796 # maximumBlockCount() text truncation.
1794 1797 if diff < 0 and document.blockCount() == document.maximumBlockCount():
1795 1798 scrollbar.setValue(scrollbar.value() + diff)
1796 1799
1797 1800 def _cursor_position_changed(self):
1798 1801 """ Clears the temporary buffer based on the cursor position.
1799 1802 """
1800 1803 if self._text_completing_pos:
1801 1804 document = self._control.document()
1802 1805 if self._text_completing_pos < document.characterCount():
1803 1806 cursor = self._control.textCursor()
1804 1807 pos = cursor.position()
1805 1808 text_cursor = self._control.textCursor()
1806 1809 text_cursor.setPosition(self._text_completing_pos)
1807 1810 if pos < self._text_completing_pos or \
1808 1811 cursor.blockNumber() > text_cursor.blockNumber():
1809 1812 self._clear_temporary_buffer()
1810 1813 self._text_completing_pos = 0
1811 1814 else:
1812 1815 self._clear_temporary_buffer()
1813 1816 self._text_completing_pos = 0
1814 1817
1815 1818 def _custom_context_menu_requested(self, pos):
1816 1819 """ Shows a context menu at the given QPoint (in widget coordinates).
1817 1820 """
1818 1821 menu = self._context_menu_make(pos)
1819 1822 menu.exec_(self._control.mapToGlobal(pos))
General Comments 0
You need to be logged in to leave comments. Login now