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