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