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