##// END OF EJS Templates
Faster refresh on write. Avoid recursive Yields.
gvaroquaux -
Show More
@@ -1,435 +1,436
1 1 # encoding: utf-8
2 2 """
3 3 A Wx widget to act as a console and input commands.
4 4
5 5 This widget deals with prompts and provides an edit buffer
6 6 restricted to after the last prompt.
7 7 """
8 8
9 9 __docformat__ = "restructuredtext en"
10 10
11 11 #-------------------------------------------------------------------------------
12 12 # Copyright (C) 2008 The IPython Development Team
13 13 #
14 14 # Distributed under the terms of the BSD License. The full license is
15 15 # in the file COPYING, distributed as part of this software.
16 16 #-------------------------------------------------------------------------------
17 17
18 18 #-------------------------------------------------------------------------------
19 19 # Imports
20 20 #-------------------------------------------------------------------------------
21 21
22 22 import wx
23 23 import wx.stc as stc
24 24
25 25 from wx.py import editwindow
26 26 import time
27 27 import sys
28 28 LINESEP = '\n'
29 29 if sys.platform == 'win32':
30 30 LINESEP = '\n\r'
31 31
32 32 import re
33 33
34 34 # FIXME: Need to provide an API for non user-generated display on the
35 35 # screen: this should not be editable by the user.
36 36
37 37 _DEFAULT_SIZE = 10
38 38 if sys.platform == 'darwin':
39 39 _DEFAULT_STYLE = 12
40 40
41 41 _DEFAULT_STYLE = {
42 42 'stdout' : 'fore:#0000FF',
43 43 'stderr' : 'fore:#007f00',
44 44 'trace' : 'fore:#FF0000',
45 45
46 46 'default' : 'size:%d' % _DEFAULT_SIZE,
47 47 'bracegood' : 'fore:#00AA00,back:#000000,bold',
48 48 'bracebad' : 'fore:#FF0000,back:#000000,bold',
49 49
50 50 # properties for the various Python lexer styles
51 51 'comment' : 'fore:#007F00',
52 52 'number' : 'fore:#007F7F',
53 53 'string' : 'fore:#7F007F,italic',
54 54 'char' : 'fore:#7F007F,italic',
55 55 'keyword' : 'fore:#00007F,bold',
56 56 'triple' : 'fore:#7F0000',
57 57 'tripledouble' : 'fore:#7F0000',
58 58 'class' : 'fore:#0000FF,bold,underline',
59 59 'def' : 'fore:#007F7F,bold',
60 60 'operator' : 'bold'
61 61 }
62 62
63 63 # new style numbers
64 64 _STDOUT_STYLE = 15
65 65 _STDERR_STYLE = 16
66 66 _TRACE_STYLE = 17
67 67
68 68
69 69 # system colors
70 70 #SYS_COLOUR_BACKGROUND = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BACKGROUND)
71 71
72 72 #-------------------------------------------------------------------------------
73 73 # The console widget class
74 74 #-------------------------------------------------------------------------------
75 75 class ConsoleWidget(editwindow.EditWindow):
76 76 """ Specialized styled text control view for console-like workflow.
77 77
78 78 This widget is mainly interested in dealing with the prompt and
79 79 keeping the cursor inside the editing line.
80 80 """
81 81
82 82 # This is where the title captured from the ANSI escape sequences are
83 83 # stored.
84 84 title = 'Console'
85 85
86 86 # The buffer being edited.
87 87 def _set_input_buffer(self, string):
88 88 self.SetSelection(self.current_prompt_pos, self.GetLength())
89 89 self.ReplaceSelection(string)
90 90 self.GotoPos(self.GetLength())
91 91
92 92 def _get_input_buffer(self):
93 93 """ Returns the text in current edit buffer.
94 94 """
95 95 input_buffer = self.GetTextRange(self.current_prompt_pos,
96 96 self.GetLength())
97 97 input_buffer = input_buffer.replace(LINESEP, '\n')
98 98 return input_buffer
99 99
100 100 input_buffer = property(_get_input_buffer, _set_input_buffer)
101 101
102 102 style = _DEFAULT_STYLE.copy()
103 103
104 104 # Translation table from ANSI escape sequences to color. Override
105 105 # this to specify your colors.
106 106 ANSI_STYLES = {'0;30': [0, 'BLACK'], '0;31': [1, 'RED'],
107 107 '0;32': [2, 'GREEN'], '0;33': [3, 'BROWN'],
108 108 '0;34': [4, 'BLUE'], '0;35': [5, 'PURPLE'],
109 109 '0;36': [6, 'CYAN'], '0;37': [7, 'LIGHT GREY'],
110 110 '1;30': [8, 'DARK GREY'], '1;31': [9, 'RED'],
111 111 '1;32': [10, 'SEA GREEN'], '1;33': [11, 'YELLOW'],
112 112 '1;34': [12, 'LIGHT BLUE'], '1;35':
113 113 [13, 'MEDIUM VIOLET RED'],
114 114 '1;36': [14, 'LIGHT STEEL BLUE'], '1;37': [15, 'YELLOW']}
115 115
116 116 # The color of the carret (call _apply_style() after setting)
117 117 carret_color = 'BLACK'
118 118
119 119 # Store the last time a refresh was done
120 120 _last_refresh_time = 0
121 121
122 122 #--------------------------------------------------------------------------
123 123 # Public API
124 124 #--------------------------------------------------------------------------
125 125
126 126 def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition,
127 127 size=wx.DefaultSize, style=wx.WANTS_CHARS, ):
128 128 editwindow.EditWindow.__init__(self, parent, id, pos, size, style)
129 129 self._configure_scintilla()
130 130
131 131 self.Bind(wx.EVT_KEY_DOWN, self._on_key_down)
132 132 self.Bind(wx.EVT_KEY_UP, self._on_key_up)
133 133
134 134
135 135 def write(self, text, refresh=True):
136 136 """ Write given text to buffer, while translating the ansi escape
137 137 sequences.
138 138 """
139 139 # XXX: do not put print statements to sys.stdout/sys.stderr in
140 140 # this method, the print statements will call this method, as
141 141 # you will end up with an infinit loop
142 142 title = self.title_pat.split(text)
143 143 if len(title)>1:
144 144 self.title = title[-2]
145 145
146 146 text = self.title_pat.sub('', text)
147 147 segments = self.color_pat.split(text)
148 148 segment = segments.pop(0)
149 149 self.GotoPos(self.GetLength())
150 150 self.StartStyling(self.GetLength(), 0xFF)
151 151 try:
152 152 self.AppendText(segment)
153 153 except UnicodeDecodeError:
154 154 # XXX: Do I really want to skip the exception?
155 155 pass
156 156
157 157 if segments:
158 158 for ansi_tag, text in zip(segments[::2], segments[1::2]):
159 159 self.StartStyling(self.GetLength(), 0xFF)
160 160 try:
161 161 self.AppendText(text)
162 162 except UnicodeDecodeError:
163 163 # XXX: Do I really want to skip the exception?
164 164 pass
165 165
166 166 if ansi_tag not in self.ANSI_STYLES:
167 167 style = 0
168 168 else:
169 169 style = self.ANSI_STYLES[ansi_tag][0]
170 170
171 171 self.SetStyling(len(text), style)
172 172
173 173 self.GotoPos(self.GetLength())
174 174 if refresh:
175 # Maybe this is faster than wx.Yield()
176 self.ProcessEvent(wx.PaintEvent())
177 175 current_time = time.time()
178 176 if current_time - self._last_refresh_time > 0.03:
179 wx.Yield()
177 # Maybe this is faster than wx.Yield(), this is certainly
178 # more robust under windows, as it avoids recursive
179 # Yields.
180 self.ProcessEvent(wx.PaintEvent())
180 181 self._last_refresh_time = current_time
181 182
182 183
183 184 def new_prompt(self, prompt):
184 185 """ Prints a prompt at start of line, and move the start of the
185 186 current block there.
186 187
187 188 The prompt can be given with ascii escape sequences.
188 189 """
189 190 self.write(prompt, refresh=False)
190 191 # now we update our cursor giving end of prompt
191 192 self.current_prompt_pos = self.GetLength()
192 193 self.current_prompt_line = self.GetCurrentLine()
193 194 wx.Yield()
194 195 self.EnsureCaretVisible()
195 196
196 197
197 198 def scroll_to_bottom(self):
198 199 maxrange = self.GetScrollRange(wx.VERTICAL)
199 200 self.ScrollLines(maxrange)
200 201
201 202
202 203 def pop_completion(self, possibilities, offset=0):
203 204 """ Pops up an autocompletion menu. Offset is the offset
204 205 in characters of the position at which the menu should
205 206 appear, relativ to the cursor.
206 207 """
207 208 self.AutoCompSetIgnoreCase(False)
208 209 self.AutoCompSetAutoHide(False)
209 210 self.AutoCompSetMaxHeight(len(possibilities))
210 211 self.AutoCompShow(offset, " ".join(possibilities))
211 212
212 213
213 214 def get_line_width(self):
214 215 """ Return the width of the line in characters.
215 216 """
216 217 return self.GetSize()[0]/self.GetCharWidth()
217 218
218 219 #--------------------------------------------------------------------------
219 220 # EditWindow API
220 221 #--------------------------------------------------------------------------
221 222
222 223 def OnUpdateUI(self, event):
223 224 """ Override the OnUpdateUI of the EditWindow class, to prevent
224 225 syntax highlighting both for faster redraw, and for more
225 226 consistent look and feel.
226 227 """
227 228
228 229 #--------------------------------------------------------------------------
229 230 # Private API
230 231 #--------------------------------------------------------------------------
231 232
232 233 def _apply_style(self):
233 234 """ Applies the colors for the different text elements and the
234 235 carret.
235 236 """
236 237 self.SetCaretForeground(self.carret_color)
237 238
238 239 #self.StyleClearAll()
239 240 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT,
240 241 "fore:#FF0000,back:#0000FF,bold")
241 242 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD,
242 243 "fore:#000000,back:#FF0000,bold")
243 244
244 245 for style in self.ANSI_STYLES.values():
245 246 self.StyleSetSpec(style[0], "bold,fore:%s" % style[1])
246 247
247 248
248 249 def _configure_scintilla(self):
249 250 self.SetEOLMode(stc.STC_EOL_LF)
250 251
251 252 # Ctrl"+" or Ctrl "-" can be used to zoomin/zoomout the text inside
252 253 # the widget
253 254 self.CmdKeyAssign(ord('+'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMIN)
254 255 self.CmdKeyAssign(ord('-'), stc.STC_SCMOD_CTRL, stc.STC_CMD_ZOOMOUT)
255 256 # Also allow Ctrl Shift "=" for poor non US keyboard users.
256 257 self.CmdKeyAssign(ord('='), stc.STC_SCMOD_CTRL|stc.STC_SCMOD_SHIFT,
257 258 stc.STC_CMD_ZOOMIN)
258 259
259 260 # Keys: we need to clear some of the keys the that don't play
260 261 # well with a console.
261 262 self.CmdKeyClear(ord('D'), stc.STC_SCMOD_CTRL)
262 263 self.CmdKeyClear(ord('L'), stc.STC_SCMOD_CTRL)
263 264 self.CmdKeyClear(ord('T'), stc.STC_SCMOD_CTRL)
264 265 self.CmdKeyClear(ord('A'), stc.STC_SCMOD_CTRL)
265 266
266 267 self.SetEOLMode(stc.STC_EOL_CRLF)
267 268 self.SetWrapMode(stc.STC_WRAP_CHAR)
268 269 self.SetWrapMode(stc.STC_WRAP_WORD)
269 270 self.SetBufferedDraw(True)
270 271 self.SetUseAntiAliasing(True)
271 272 self.SetLayoutCache(stc.STC_CACHE_PAGE)
272 273 self.SetUndoCollection(False)
273 274 self.SetUseTabs(True)
274 275 self.SetIndent(4)
275 276 self.SetTabWidth(4)
276 277
277 278 # we don't want scintilla's autocompletion to choose
278 279 # automaticaly out of a single choice list, as we pop it up
279 280 # automaticaly
280 281 self.AutoCompSetChooseSingle(False)
281 282 self.AutoCompSetMaxHeight(10)
282 283 # XXX: this doesn't seem to have an effect.
283 284 self.AutoCompSetFillUps('\n')
284 285
285 286 self.SetMargins(3, 3) #text is moved away from border with 3px
286 287 # Suppressing Scintilla margins
287 288 self.SetMarginWidth(0, 0)
288 289 self.SetMarginWidth(1, 0)
289 290 self.SetMarginWidth(2, 0)
290 291
291 292 self._apply_style()
292 293
293 294 # Xterm escape sequences
294 295 self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
295 296 self.title_pat = re.compile('\x1b]0;(.*?)\x07')
296 297
297 298 #self.SetEdgeMode(stc.STC_EDGE_LINE)
298 299 #self.SetEdgeColumn(80)
299 300
300 301 # styles
301 302 p = self.style
302 303 self.StyleSetSpec(stc.STC_STYLE_DEFAULT, p['default'])
303 304 self.StyleClearAll()
304 305 self.StyleSetSpec(_STDOUT_STYLE, p['stdout'])
305 306 self.StyleSetSpec(_STDERR_STYLE, p['stderr'])
306 307 self.StyleSetSpec(_TRACE_STYLE, p['trace'])
307 308
308 309 self.StyleSetSpec(stc.STC_STYLE_BRACELIGHT, p['bracegood'])
309 310 self.StyleSetSpec(stc.STC_STYLE_BRACEBAD, p['bracebad'])
310 311 self.StyleSetSpec(stc.STC_P_COMMENTLINE, p['comment'])
311 312 self.StyleSetSpec(stc.STC_P_NUMBER, p['number'])
312 313 self.StyleSetSpec(stc.STC_P_STRING, p['string'])
313 314 self.StyleSetSpec(stc.STC_P_CHARACTER, p['char'])
314 315 self.StyleSetSpec(stc.STC_P_WORD, p['keyword'])
315 316 self.StyleSetSpec(stc.STC_P_WORD2, p['keyword'])
316 317 self.StyleSetSpec(stc.STC_P_TRIPLE, p['triple'])
317 318 self.StyleSetSpec(stc.STC_P_TRIPLEDOUBLE, p['tripledouble'])
318 319 self.StyleSetSpec(stc.STC_P_CLASSNAME, p['class'])
319 320 self.StyleSetSpec(stc.STC_P_DEFNAME, p['def'])
320 321 self.StyleSetSpec(stc.STC_P_OPERATOR, p['operator'])
321 322 self.StyleSetSpec(stc.STC_P_COMMENTBLOCK, p['comment'])
322 323
323 324 def _on_key_down(self, event, skip=True):
324 325 """ Key press callback used for correcting behavior for
325 326 console-like interfaces: the cursor is constraint to be after
326 327 the last prompt.
327 328
328 329 Return True if event as been catched.
329 330 """
330 331 catched = True
331 332 # Intercept some specific keys.
332 333 if event.KeyCode == ord('L') and event.ControlDown() :
333 334 self.scroll_to_bottom()
334 335 elif event.KeyCode == ord('K') and event.ControlDown() :
335 336 self.input_buffer = ''
336 337 elif event.KeyCode == ord('A') and event.ControlDown() :
337 338 self.GotoPos(self.GetLength())
338 339 self.SetSelectionStart(self.current_prompt_pos)
339 340 self.SetSelectionEnd(self.GetCurrentPos())
340 341 catched = True
341 342 elif event.KeyCode == ord('E') and event.ControlDown() :
342 343 self.GotoPos(self.GetLength())
343 344 catched = True
344 345 elif event.KeyCode == wx.WXK_PAGEUP:
345 346 self.ScrollPages(-1)
346 347 elif event.KeyCode == wx.WXK_PAGEDOWN:
347 348 self.ScrollPages(1)
348 349 elif event.KeyCode == wx.WXK_UP and event.ShiftDown():
349 350 self.ScrollLines(-1)
350 351 elif event.KeyCode == wx.WXK_DOWN and event.ShiftDown():
351 352 self.ScrollLines(1)
352 353 else:
353 354 catched = False
354 355
355 356 if self.AutoCompActive():
356 357 event.Skip()
357 358 else:
358 359 if event.KeyCode in (13, wx.WXK_NUMPAD_ENTER) and \
359 360 event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
360 361 catched = True
361 362 self.CallTipCancel()
362 363 self.write('\n', refresh=False)
363 364 # Under windows scintilla seems to be doing funny stuff to the
364 365 # line returns here, but the getter for input_buffer filters
365 366 # this out.
366 367 if sys.platform == 'win32':
367 368 self.input_buffer = self.input_buffer
368 369 self._on_enter()
369 370
370 371 elif event.KeyCode == wx.WXK_HOME:
371 372 if event.Modifiers in (wx.MOD_NONE, wx.MOD_WIN):
372 373 self.GotoPos(self.current_prompt_pos)
373 374 catched = True
374 375
375 376 elif event.Modifiers == wx.MOD_SHIFT:
376 377 # FIXME: This behavior is not ideal: if the selection
377 378 # is already started, it will jump.
378 379 self.SetSelectionStart(self.current_prompt_pos)
379 380 self.SetSelectionEnd(self.GetCurrentPos())
380 381 catched = True
381 382
382 383 elif event.KeyCode == wx.WXK_UP:
383 384 if self.GetCurrentLine() > self.current_prompt_line:
384 385 if self.GetCurrentLine() == self.current_prompt_line + 1 \
385 386 and self.GetColumn(self.GetCurrentPos()) < \
386 387 self.GetColumn(self.current_prompt_pos):
387 388 self.GotoPos(self.current_prompt_pos)
388 389 else:
389 390 event.Skip()
390 391 catched = True
391 392
392 393 elif event.KeyCode in (wx.WXK_LEFT, wx.WXK_BACK):
393 394 if self.GetCurrentPos() > self.current_prompt_pos:
394 395 event.Skip()
395 396 catched = True
396 397
397 398 if skip and not catched:
398 399 # Put the cursor back in the edit region
399 400 if self.GetCurrentPos() < self.current_prompt_pos:
400 401 self.GotoPos(self.current_prompt_pos)
401 402 else:
402 403 event.Skip()
403 404
404 405 return catched
405 406
406 407
407 408 def _on_key_up(self, event, skip=True):
408 409 """ If cursor is outside the editing region, put it back.
409 410 """
410 411 event.Skip()
411 412 if self.GetCurrentPos() < self.current_prompt_pos:
412 413 self.GotoPos(self.current_prompt_pos)
413 414
414 415
415 416
416 417 if __name__ == '__main__':
417 418 # Some simple code to test the console widget.
418 419 class MainWindow(wx.Frame):
419 420 def __init__(self, parent, id, title):
420 421 wx.Frame.__init__(self, parent, id, title, size=(300,250))
421 422 self._sizer = wx.BoxSizer(wx.VERTICAL)
422 423 self.console_widget = ConsoleWidget(self)
423 424 self._sizer.Add(self.console_widget, 1, wx.EXPAND)
424 425 self.SetSizer(self._sizer)
425 426 self.SetAutoLayout(1)
426 427 self.Show(True)
427 428
428 429 app = wx.PySimpleApp()
429 430 w = MainWindow(None, wx.ID_ANY, 'ConsoleWidget')
430 431 w.SetSize((780, 460))
431 432 w.Show()
432 433
433 434 app.MainLoop()
434 435
435 436
General Comments 0
You need to be logged in to leave comments. Login now