- Scrollbars for the console (use View2D functions)
[blender-staging.git] / release / ui / space_console.py
1
2 import bpy
3
4 class CONSOLE_HT_header(bpy.types.Header):
5         __space_type__ = "CONSOLE"
6         __idname__ = "CONSOLE_HT_header"
7
8         def draw(self, context):
9                 sc = context.space_data
10                 # text = sc.text
11                 layout = self.layout
12
13                 layout.template_header()
14
15                 if context.area.show_menus:
16                         row = layout.row()
17                         row.itemM("CONSOLE_MT_console")
18                 
19                 row = layout.row()
20                 row.scale_x = 0.9
21                 row.itemR(sc, "type", expand=True)
22                 if sc.type == 'REPORT':
23                         row.itemR(sc, "show_report_debug")
24                         row.itemR(sc, "show_report_info")
25                         row.itemR(sc, "show_report_operator")
26                         row.itemR(sc, "show_report_warn")
27                         row.itemR(sc, "show_report_error")
28
29
30 class CONSOLE_MT_console(bpy.types.Menu):
31         __space_type__ = "CONSOLE"
32         __label__ = "Console"
33
34         def draw(self, context):
35                 layout = self.layout
36                 sc = context.space_data
37
38                 layout.column()
39                 layout.itemO("CONSOLE_OT_clear")
40
41 def add_scrollback(text, text_type):
42         for l in text.split('\n'):
43                 bpy.ops.CONSOLE_OT_scrollback_append(text=l.replace('\t', '    '), type=text_type)
44
45 def get_console(console_id):
46         '''
47         helper function for console operators
48         currently each text datablock gets its own console - code.InteractiveConsole()
49         ...which is stored in this function.
50         
51         console_id can be any hashable type
52         '''
53         import sys, code
54         
55         try:    consoles = get_console.consoles
56         except:consoles = get_console.consoles = {}
57         
58         # clear all dead consoles, use text names as IDs
59         # TODO, find a way to clear IDs
60         '''
61         for console_id in list(consoles.keys()):
62                 if console_id not in bpy.data.texts:
63                         del consoles[id]
64         '''
65         
66         try:
67                 namespace, console, stdout, stderr = consoles[console_id]
68         except:
69                 namespace = {'__builtins__':__builtins__} # locals()
70                 namespace['bpy'] = bpy
71                 
72                 console = code.InteractiveConsole(namespace)
73                 
74                 if sys.version.startswith('2'):
75                         import cStringIO
76                         stdout = cStringIO.BytesIO()  # Py2x support
77                         stderr = cStringIO.BytesIO()
78                 else:
79                         import io
80                         stdout = io.StringIO()
81                         stderr = io.StringIO()
82         
83                 consoles[console_id]= namespace, console, stdout, stderr
84                 
85         return namespace, console, stdout, stderr
86
87 class CONSOLE_OT_exec(bpy.types.Operator):
88         '''
89         Operator documentatuon text, will be used for the operator tooltip and python docs.
90         '''
91         __label__ = "Console Execute"
92         __register__ = True
93         
94         # Both prompts must be the same length
95         PROMPT = '>>> ' 
96         PROMPT_MULTI = '... '
97         
98         # is this working???
99         '''
100         def poll(self, context):
101                 return (context.space_data.type == 'PYTHON')
102         ''' # its not :|
103         
104         def execute(self, context):
105                 import sys
106                 
107                 sc = context.space_data
108                 
109                 try:
110                         line = sc.history[-1].line
111                 except:
112                         return ('CANCELLED',)
113                 
114                 if sc.type != 'PYTHON':
115                         return ('CANCELLED',)
116                 
117                 namespace, console, stdout, stderr = get_console(hash(context.region))
118                 
119                 # redirect output
120                 sys.stdout = stdout
121                 sys.stderr = stderr
122                 
123                 # run the console
124                 if not line.strip():
125                         line_exec = '\n' # executes a multiline statement
126                 else:
127                         line_exec = line
128                 
129                 is_multiline = console.push(line_exec)
130                 
131                 stdout.seek(0)
132                 stderr.seek(0)
133                 
134                 output = stdout.read()
135                 output_err = stderr.read()
136         
137                 # cleanup
138                 sys.stdout = sys.__stdout__
139                 sys.stderr = sys.__stderr__
140                 sys.last_traceback = None
141                 
142                 # So we can reuse, clear all data
143                 stdout.truncate(0)
144                 stderr.truncate(0)
145                 
146                 bpy.ops.CONSOLE_OT_scrollback_append(text = sc.prompt+line, type='INPUT')
147                 
148                 if is_multiline:        sc.prompt = self.PROMPT_MULTI
149                 else:                           sc.prompt = self.PROMPT
150                 
151                 # insert a new blank line
152                 bpy.ops.CONSOLE_OT_history_append(text="", current_character=0)
153                 
154                 # Insert the output into the editor
155                 # not quite correct because the order might have changed, but ok 99% of the time.
156                 if output:                      add_scrollback(output, 'OUTPUT')
157                 if output_err:          add_scrollback(output_err, 'ERROR')
158                 
159                 
160                 return ('FINISHED',)
161
162
163 def autocomp(bcon):
164         '''
165         This function has been taken from a BGE console autocomp I wrote a while ago
166         the dictionaty bcon is not needed but it means I can copy and paste from the old func
167         which works ok for now.
168         
169         could be moved into its own module.
170         '''
171         
172         
173         def is_delimiter(ch):
174                 '''
175                 For skipping words
176                 '''
177                 if ch == '_':
178                         return False
179                 if ch.isalnum():
180                         return False
181                 
182                 return True
183         
184         def is_delimiter_autocomp(ch):
185                 '''
186                 When autocompleteing will earch back and 
187                 '''
188                 if ch in '._[] "\'':
189                         return False
190                 if ch.isalnum():
191                         return False
192                 
193                 return True
194
195         
196         def do_autocomp(autocomp_prefix, autocomp_members):
197                 '''
198                 return text to insert and a list of options
199                 '''
200                 autocomp_members = [v for v in autocomp_members if v.startswith(autocomp_prefix)]
201                 
202                 print("AUTO: '%s'" % autocomp_prefix)
203                 print("MEMBERS: '%s'" % str(autocomp_members))
204                 
205                 if not autocomp_prefix:
206                         return '', autocomp_members
207                 elif len(autocomp_members) > 1:
208                         # find a common string between all members after the prefix 
209                         # 'ge' [getA, getB, getC] --> 'get'
210                         
211                         # get the shortest member
212                         min_len = min([len(v) for v in autocomp_members])
213                         
214                         autocomp_prefix_ret = ''
215                         
216                         for i in range(len(autocomp_prefix), min_len):
217                                 char_soup = set()
218                                 for v in autocomp_members:
219                                         char_soup.add(v[i])
220                                 
221                                 if len(char_soup) > 1:
222                                         break
223                                 else:
224                                         autocomp_prefix_ret += char_soup.pop()
225                                 
226                         print(autocomp_prefix_ret)
227                         return autocomp_prefix_ret, autocomp_members
228                 elif len(autocomp_members) == 1:
229                         return autocomp_members[0][len(autocomp_prefix):], []
230                 else:
231                         return '', []
232         
233
234         def BCon_PrevChar(bcon):
235                 cursor = bcon['cursor']-1
236                 if cursor<0:
237                         return None
238                         
239                 try:
240                         return bcon['edit_text'][cursor]
241                 except:
242                         return None
243                 
244                 
245         def BCon_NextChar(bcon):
246                 try:
247                         return bcon['edit_text'][bcon['cursor']]
248                 except:
249                         return None
250         
251         def BCon_cursorLeft(bcon):
252                 bcon['cursor'] -= 1
253                 if bcon['cursor'] < 0:
254                         bcon['cursor'] = 0
255
256         def BCon_cursorRight(bcon):
257                         bcon['cursor'] += 1
258                         if bcon['cursor'] > len(bcon['edit_text']):
259                                 bcon['cursor'] = len(bcon['edit_text'])
260         
261         def BCon_AddScrollback(bcon, text):
262                 
263                 bcon['scrollback'] = bcon['scrollback'] + text
264                 
265         
266         def BCon_cursorInsertChar(bcon, ch):
267                 if bcon['cursor']==0:
268                         bcon['edit_text'] = ch + bcon['edit_text']
269                 elif bcon['cursor']==len(bcon['edit_text']):
270                         bcon['edit_text'] = bcon['edit_text'] + ch
271                 else:
272                         bcon['edit_text'] = bcon['edit_text'][:bcon['cursor']] + ch + bcon['edit_text'][bcon['cursor']:]
273                         
274                 bcon['cursor'] 
275                 if bcon['cursor'] > len(bcon['edit_text']):
276                         bcon['cursor'] = len(bcon['edit_text'])
277                 BCon_cursorRight(bcon)
278         
279         
280         TEMP_NAME = '___tempname___'
281         
282         cursor_orig = bcon['cursor']
283         
284         ch = BCon_PrevChar(bcon)
285         while ch != None and (not is_delimiter(ch)):
286                 ch = BCon_PrevChar(bcon)
287                 BCon_cursorLeft(bcon)
288         
289         if ch != None:
290                 BCon_cursorRight(bcon)
291         
292         #print (cursor_orig, bcon['cursor'])
293         
294         cursor_base = bcon['cursor']
295         
296         autocomp_prefix = bcon['edit_text'][cursor_base:cursor_orig]
297         
298         print("PREFIX:'%s'" % autocomp_prefix)
299         
300         # Get the previous word
301         if BCon_PrevChar(bcon)=='.':
302                 BCon_cursorLeft(bcon)
303                 ch = BCon_PrevChar(bcon)
304                 while ch != None and is_delimiter_autocomp(ch)==False:
305                         ch = BCon_PrevChar(bcon)
306                         BCon_cursorLeft(bcon)
307                 
308                 cursor_new = bcon['cursor']
309                 
310                 if ch != None:
311                         cursor_new+=1
312                 
313                 pytxt = bcon['edit_text'][cursor_new:cursor_base-1].strip()
314                 print("AUTOCOMP EVAL: '%s'" % pytxt)
315                 #try:
316                 if pytxt:
317                         bcon['console'].runsource(TEMP_NAME + '=' + pytxt, '<input>', 'single')
318                         # print val
319                 else: ##except:
320                         val = None
321                 
322                 try:
323                         val = bcon['namespace'][TEMP_NAME]
324                         del bcon['namespace'][TEMP_NAME]
325                 except:
326                         val = None
327                 
328                 if val:
329                         autocomp_members = dir(val)
330                         
331                         autocomp_prefix_ret, autocomp_members = do_autocomp(autocomp_prefix, autocomp_members)
332                         
333                         bcon['cursor'] = cursor_orig
334                         for v in autocomp_prefix_ret:
335                                 BCon_cursorInsertChar(bcon, v)
336                         cursor_orig = bcon['cursor']
337                         
338                         if autocomp_members:
339                                 BCon_AddScrollback(bcon, ', '.join(autocomp_members))
340                 
341                 del val
342                 
343         else:
344                 # Autocomp global namespace
345                 autocomp_members = bcon['namespace'].keys()
346                 
347                 if autocomp_prefix:
348                         autocomp_members = [v for v in autocomp_members if v.startswith(autocomp_prefix)]
349                 
350                 autocomp_prefix_ret, autocomp_members = do_autocomp(autocomp_prefix, autocomp_members)
351                 
352                 bcon['cursor'] = cursor_orig
353                 for v in autocomp_prefix_ret:
354                         BCon_cursorInsertChar(bcon, v)
355                 cursor_orig = bcon['cursor']
356                 
357                 if autocomp_members:
358                         BCon_AddScrollback(bcon, ', '.join(autocomp_members))
359         
360         bcon['cursor'] = cursor_orig
361
362
363 class CONSOLE_OT_autocomplete(bpy.types.Operator):
364         '''
365         Operator documentatuon text, will be used for the operator tooltip and python docs.
366         '''
367         __label__ = "Console Autocomplete"
368         __register__ = True
369         
370         def poll(self, context):
371                 return context.space_data.type == 'PYTHON'
372         
373         def execute(self, context):
374                 
375                 sc = context.space_data
376                 
377                 namespace, console, stdout, stderr = get_console(hash(context.region))
378                 
379                 current_line = sc.history[-1]
380                 line = current_line.line
381                 
382                 if not console:
383                         return ('CANCELLED',)
384                 
385                 if sc.type != 'PYTHON':
386                         return ('CANCELLED',)
387                 
388                 # fake cursor, use for autocomp func.
389                 bcon = {}
390                 bcon['cursor'] = current_line.current_character
391                 bcon['console'] = console
392                 bcon['edit_text'] = line
393                 bcon['namespace'] = namespace
394                 bcon['scrollback'] = '' # nor from the BGE console
395                 
396                 
397                 # This function isnt aware of the text editor or being an operator
398                 # just does the autocomp then copy its results back
399                 autocomp(bcon)
400                 
401                 # Now we need to copy back the line from blender back into the text editor.
402                 # This will change when we dont use the text editor anymore
403                 if bcon['scrollback']:
404                         add_scrollback(bcon['scrollback'], 'INFO')
405                 
406                 # copy back
407                 current_line.line = bcon['edit_text']
408                 current_line.current_character = bcon['cursor']
409                 
410                 context.area.tag_redraw()
411                 
412                 return ('FINISHED',)
413
414
415
416 bpy.types.register(CONSOLE_HT_header)
417 bpy.types.register(CONSOLE_MT_console)
418
419 bpy.ops.add(CONSOLE_OT_exec)
420 bpy.ops.add(CONSOLE_OT_autocomplete)
421