diff options
Diffstat (limited to 'release')
-rw-r--r-- | release/scripts/bpymodules/BPyTextPlugin.py | 675 | ||||
-rw-r--r-- | release/scripts/reeb.py | 110 | ||||
-rw-r--r-- | release/scripts/scripttemplate_text_plugin.py | 69 | ||||
-rw-r--r-- | release/scripts/textplugin_functiondocs.py | 77 | ||||
-rw-r--r-- | release/scripts/textplugin_imports.py | 91 | ||||
-rw-r--r-- | release/scripts/textplugin_membersuggest.py | 101 | ||||
-rw-r--r-- | release/scripts/textplugin_outliner.py | 142 | ||||
-rw-r--r-- | release/scripts/textplugin_suggest.py | 94 | ||||
-rw-r--r-- | release/scripts/textplugin_templates.py | 115 |
9 files changed, 1364 insertions, 110 deletions
diff --git a/release/scripts/bpymodules/BPyTextPlugin.py b/release/scripts/bpymodules/BPyTextPlugin.py new file mode 100644 index 00000000000..9d33eca9de7 --- /dev/null +++ b/release/scripts/bpymodules/BPyTextPlugin.py @@ -0,0 +1,675 @@ +import bpy, sys, os +import __builtin__, tokenize +from Blender.sys import time +from tokenize import generate_tokens, TokenError, \ + COMMENT, DEDENT, INDENT, NAME, NEWLINE, NL, STRING + +class ScriptDesc(): + """Describes a script through lists of further descriptor objects (classes, + defs, vars) and dictionaries to built-in types (imports). If a script has + not been fully parsed, its incomplete flag will be set. The time of the last + parse is held by the time field and the name of the text object from which + it was parsed, the name field. + """ + + def __init__(self, name, imports, classes, defs, vars, incomplete=False): + self.name = name + self.imports = imports + self.classes = classes + self.defs = defs + self.vars = vars + self.incomplete = incomplete + self.time = 0 + + def set_time(self): + self.time = time() + +class ClassDesc(): + """Describes a class through lists of further descriptor objects (defs and + vars). The name of the class is held by the name field and the line on + which it is defined is held in lineno. + """ + + def __init__(self, name, defs, vars, lineno): + self.name = name + self.defs = defs + self.vars = vars + self.lineno = lineno + +class FunctionDesc(): + """Describes a function through its name and list of parameters (name, + params) and the line on which it is defined (lineno). + """ + + def __init__(self, name, params, lineno): + self.name = name + self.params = params + self.lineno = lineno + +class VarDesc(): + """Describes a variable through its name and type (if ascertainable) and the + line on which it is defined (lineno). If no type can be determined, type + will equal None. + """ + + def __init__(self, name, type, lineno): + self.name = name + self.type = type # None for unknown (supports: dict/list/str) + self.lineno = lineno + +# Context types +CTX_UNSET = -1 +CTX_NORMAL = 0 +CTX_SINGLE_QUOTE = 1 +CTX_DOUBLE_QUOTE = 2 +CTX_COMMENT = 3 + +# Special time period constants +TP_AUTO = -1 + +# Python keywords +KEYWORDS = ['and', 'del', 'from', 'not', 'while', 'as', 'elif', 'global', + 'or', 'with', 'assert', 'else', 'if', 'pass', 'yield', + 'break', 'except', 'import', 'print', 'class', 'exec', 'in', + 'raise', 'continue', 'finally', 'is', 'return', 'def', 'for', + 'lambda', 'try' ] + +# Module file extensions +MODULE_EXTS = ['.py', '.pyc', '.pyo', '.pyw', '.pyd'] + +ModuleType = type(__builtin__) +NoneScriptDesc = ScriptDesc('', dict(), dict(), dict(), dict(), True) + +_modules = {} +_modules_updated = 0 +_parse_cache = dict() + +def _load_module_names(): + """Searches the sys.path for module files and lists them, along with + sys.builtin_module_names, in the global dict _modules. + """ + + global _modules + + for n in sys.builtin_module_names: + _modules[n] = None + for p in sys.path: + if p == '': p = os.curdir + if not os.path.isdir(p): continue + for f in os.listdir(p): + for ext in MODULE_EXTS: + if f.endswith(ext): + _modules[f[:-len(ext)]] = None + break + +_load_module_names() + + +def get_cached_descriptor(txt, period=TP_AUTO): + """Returns the cached ScriptDesc for the specified Text object 'txt'. If the + script has not been parsed in the last 'period' seconds it will be reparsed + to obtain this descriptor. + + Specifying TP_AUTO for the period (default) will choose a period based on the + size of the Text object. Larger texts are parsed less often. + """ + + global _parse_cache + + if period == TP_AUTO: + m = txt.nlines + r = 1 + while True: + m = m >> 2 + if not m: break + r = r << 1 + period = r + + parse = True + key = hash(txt) + if _parse_cache.has_key(key): + desc = _parse_cache[key] + if desc.time >= time() - period: + parse = desc.incomplete + + if parse: + desc = parse_text(txt) + + return desc + +def parse_text(txt): + """Parses an entire script's text and returns a ScriptDesc instance + containing information about the script. + + If the text is not a valid Python script (for example if brackets are left + open), parsing may fail to complete. However, if this occurs, no exception + is thrown. Instead the returned ScriptDesc instance will have its incomplete + flag set and information processed up to this point will still be accessible. + """ + + txt.reset() + tokens = generate_tokens(txt.readline) # Throws TokenError + + curl, cursor = txt.getCursorPos() + linen = curl + 1 # Token line numbers are one-based + + imports = dict() + imp_step = 0 + + classes = dict() + cls_step = 0 + + defs = dict() + def_step = 0 + + vars = dict() + var1_step = 0 + var2_step = 0 + var3_step = 0 + var_accum = dict() + var_forflag = False + + indent = 0 + prev_type = -1 + prev_string = '' + incomplete = False + + while True: + try: + type, string, start, end, line = tokens.next() + except StopIteration: + break + except TokenError, IndentationError: + incomplete = True + break + + # Skip all comments and line joining characters + if type == COMMENT or type == NL: + continue + + ################# + ## Indentation ## + ################# + + if type == INDENT: + indent += 1 + elif type == DEDENT: + indent -= 1 + + ######################### + ## Module importing... ## + ######################### + + imp_store = False + + # Default, look for 'from' or 'import' to start + if imp_step == 0: + if string == 'from': + imp_tmp = [] + imp_step = 1 + elif string == 'import': + imp_from = None + imp_tmp = [] + imp_step = 2 + + # Found a 'from', create imp_from in form '???.???...' + elif imp_step == 1: + if string == 'import': + imp_from = '.'.join(imp_tmp) + imp_tmp = [] + imp_step = 2 + elif type == NAME: + imp_tmp.append(string) + elif string != '.': + imp_step = 0 # Invalid syntax + + # Found 'import', imp_from is populated or None, create imp_name + elif imp_step == 2: + if string == 'as': + imp_name = '.'.join(imp_tmp) + imp_step = 3 + elif type == NAME or string == '*': + imp_tmp.append(string) + elif string != '.': + imp_name = '.'.join(imp_tmp) + imp_symb = imp_name + imp_store = True + + # Found 'as', change imp_symb to this value and go back to step 2 + elif imp_step == 3: + if type == NAME: + imp_symb = string + else: + imp_store = True + + # Both imp_name and imp_symb have now been populated so we can import + if imp_store: + + # Handle special case of 'import *' + if imp_name == '*': + parent = get_module(imp_from) + imports.update(parent.__dict__) + + else: + # Try importing the name as a module + try: + if imp_from: + module = get_module(imp_from +'.'+ imp_name) + else: + module = get_module(imp_name) + except (ImportError, ValueError, AttributeError, TypeError): + # Try importing name as an attribute of the parent + try: + module = __import__(imp_from, globals(), locals(), [imp_name]) + imports[imp_symb] = getattr(module, imp_name) + except (ImportError, ValueError, AttributeError, TypeError): + pass + else: + imports[imp_symb] = module + + # More to import from the same module? + if string == ',': + imp_tmp = [] + imp_step = 2 + else: + imp_step = 0 + + ################### + ## Class parsing ## + ################### + + # If we are inside a class then def and variable parsing should be done + # for the class. Otherwise the definitions are considered global + + # Look for 'class' + if cls_step == 0: + if string == 'class': + cls_name = None + cls_lineno = start[0] + cls_indent = indent + cls_step = 1 + + # Found 'class', look for cls_name followed by '(' + elif cls_step == 1: + if not cls_name: + if type == NAME: + cls_name = string + cls_sline = False + cls_defs = dict() + cls_vars = dict() + elif string == ':': + cls_step = 2 + + # Found 'class' name ... ':', now check if it's a single line statement + elif cls_step == 2: + if type == NEWLINE: + cls_sline = False + cls_step = 3 + else: + cls_sline = True + cls_step = 3 + + elif cls_step == 3: + if cls_sline: + if type == NEWLINE: + classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno) + cls_step = 0 + else: + if type == DEDENT and indent <= cls_indent: + classes[cls_name] = ClassDesc(cls_name, cls_defs, cls_vars, cls_lineno) + cls_step = 0 + + ################# + ## Def parsing ## + ################# + + # Look for 'def' + if def_step == 0: + if string == 'def': + def_name = None + def_lineno = start[0] + def_step = 1 + + # Found 'def', look for def_name followed by '(' + elif def_step == 1: + if type == NAME: + def_name = string + def_params = [] + elif def_name and string == '(': + def_step = 2 + + # Found 'def' name '(', now identify the parameters upto ')' + # TODO: Handle ellipsis '...' + elif def_step == 2: + if type == NAME: + def_params.append(string) + elif string == ')': + if cls_step > 0: # Parsing a class + cls_defs[def_name] = FunctionDesc(def_name, def_params, def_lineno) + else: + defs[def_name] = FunctionDesc(def_name, def_params, def_lineno) + def_step = 0 + + ########################## + ## Variable assignation ## + ########################## + + if cls_step > 0: # Parsing a class + # Look for 'self.???' + if var1_step == 0: + if string == 'self': + var1_step = 1 + elif var1_step == 1: + if string == '.': + var_name = None + var1_step = 2 + else: + var1_step = 0 + elif var1_step == 2: + if type == NAME: + var_name = string + if cls_vars.has_key(var_name): + var_step = 0 + else: + var1_step = 3 + elif var1_step == 3: + if string == '=': + var1_step = 4 + elif var1_step == 4: + var_type = None + if string == '[': + close = line.find(']', end[1]) + var_type = list + elif type == STRING: + close = end[1] + var_type = str + elif string == '(': + close = line.find(')', end[1]) + var_type = tuple + elif string == '{': + close = line.find('}', end[1]) + var_type = dict + elif string == 'dict': + close = line.find(')', end[1]) + var_type = dict + if var_type and close+1 < len(line): + if line[close+1] != ' ' and line[close+1] != '\t': + var_type = None + cls_vars[var_name] = VarDesc(var_name, var_type, start[0]) + var1_step = 0 + + elif def_step > 0: # Parsing a def + # Look for 'global ???[,???]' + if var2_step == 0: + if string == 'global': + var2_step = 1 + elif var2_step == 1: + if type == NAME: + vars[string] = True + elif string != ',' and type != NL: + var2_step == 0 + + else: # In global scope + if var3_step == 0: + # Look for names + if string == 'for': + var_accum = dict() + var_forflag = True + elif string == '=' or (var_forflag and string == 'in'): + var_forflag = False + var3_step = 1 + elif type == NAME: + if prev_string != '.' and not vars.has_key(string): + var_accum[string] = VarDesc(string, None, start[0]) + elif not string in [',', '(', ')', '[', ']']: + var_accum = dict() + var_forflag = False + elif var3_step == 1: + if len(var_accum) != 1: + var_type = None + vars.update(var_accum) + else: + var_name = var_accum.keys()[0] + var_type = None + if string == '[': var_type = list + elif type == STRING: var_type = str + elif string == '(': var_type = tuple + elif string == '{': var_type = dict + vars[var_name] = VarDesc(var_name, var_type, start[0]) + var3_step = 0 + + ####################### + ## General utilities ## + ####################### + + prev_type = type + prev_string = string + + desc = ScriptDesc(txt.name, imports, classes, defs, vars, incomplete) + desc.set_time() + + global _parse_cache + _parse_cache[hash(txt)] = desc + return desc + +def get_modules(since=1): + """Returns the set of built-in modules and any modules that have been + imported into the system upto 'since' seconds ago. + """ + + global _modules, _modules_updated + + t = time() + if _modules_updated < t - since: + _modules.update(sys.modules) + _modules_updated = t + return _modules.keys() + +def suggest_cmp(x, y): + """Use this method when sorting a list of suggestions. + """ + + return cmp(x[0].upper(), y[0].upper()) + +def get_module(name): + """Returns the module specified by its name. The module itself is imported + by this method and, as such, any initialization code will be executed. + """ + + mod = __import__(name) + components = name.split('.') + for comp in components[1:]: + mod = getattr(mod, comp) + return mod + +def type_char(v): + """Returns the character used to signify the type of a variable. Use this + method to identify the type character for an item in a suggestion list. + + The following values are returned: + 'm' if the parameter is a module + 'f' if the parameter is callable + 'v' if the parameter is variable or otherwise indeterminable + + """ + + if isinstance(v, ModuleType): + return 'm' + elif callable(v): + return 'f' + else: + return 'v' + +def get_context(txt): + """Establishes the context of the cursor in the given Blender Text object + + Returns one of: + CTX_NORMAL - Cursor is in a normal context + CTX_SINGLE_QUOTE - Cursor is inside a single quoted string + CTX_DOUBLE_QUOTE - Cursor is inside a double quoted string + CTX_COMMENT - Cursor is inside a comment + + """ + + l, cursor = txt.getCursorPos() + lines = txt.asLines(0, l+1) + + # FIXME: This method is too slow in large files for it to be called as often + # as it is. So for lines below the 1000th line we do this... (quorn) + if l > 1000: return CTX_NORMAL + + # Detect context (in string or comment) + in_str = CTX_NORMAL + for line in lines: + if l == 0: + end = cursor + else: + end = len(line) + l -= 1 + + # Comments end at new lines + if in_str == CTX_COMMENT: + in_str = CTX_NORMAL + + for i in range(end): + if in_str == 0: + if line[i] == "'": in_str = CTX_SINGLE_QUOTE + elif line[i] == '"': in_str = CTX_DOUBLE_QUOTE + elif line[i] == '#': in_str = CTX_COMMENT + else: + if in_str == CTX_SINGLE_QUOTE: + if line[i] == "'": + in_str = CTX_NORMAL + # In again if ' escaped, out again if \ escaped, and so on + for a in range(i-1, -1, -1): + if line[a] == '\\': in_str = 1-in_str + else: break + elif in_str == CTX_DOUBLE_QUOTE: + if line[i] == '"': + in_str = CTX_NORMAL + # In again if " escaped, out again if \ escaped, and so on + for a in range(i-1, -1, -1): + if line[i-a] == '\\': in_str = 2-in_str + else: break + + return in_str + +def current_line(txt): + """Extracts the Python script line at the cursor in the Blender Text object + provided and cursor position within this line as the tuple pair (line, + cursor). + """ + + lineindex, cursor = txt.getCursorPos() + lines = txt.asLines() + line = lines[lineindex] + + # Join previous lines to this line if spanning + i = lineindex - 1 + while i > 0: + earlier = lines[i].rstrip() + if earlier.endswith('\\'): + line = earlier[:-1] + ' ' + line + cursor += len(earlier) + i -= 1 + + # Join later lines while there is an explicit joining character + i = lineindex + while i < len(lines)-1 and lines[i].rstrip().endswith('\\'): + later = lines[i+1].strip() + line = line + ' ' + later[:-1] + i += 1 + + return line, cursor + +def get_targets(line, cursor): + """Parses a period separated string of valid names preceding the cursor and + returns them as a list in the same order. + """ + + targets = [] + i = cursor - 1 + while i >= 0 and (line[i].isalnum() or line[i] == '_' or line[i] == '.'): + i -= 1 + + pre = line[i+1:cursor] + return pre.split('.') + +def get_defs(txt): + """Returns a dictionary which maps definition names in the source code to + a list of their parameter names. + + The line 'def doit(one, two, three): print one' for example, results in the + mapping 'doit' : [ 'one', 'two', 'three' ] + """ + + return get_cached_descriptor(txt).defs + +def get_vars(txt): + """Returns a dictionary of variable names found in the specified Text + object. This method locates all names followed directly by an equal sign: + 'a = ???' or indirectly as part of a tuple/list assignment or inside a + 'for ??? in ???:' block. + """ + + return get_cached_descriptor(txt).vars + +def get_imports(txt): + """Returns a dictionary which maps symbol names in the source code to their + respective modules. + + The line 'from Blender import Text as BText' for example, results in the + mapping 'BText' : <module 'Blender.Text' (built-in)> + + Note that this method imports the modules to provide this mapping as as such + will execute any initilization code found within. + """ + + return get_cached_descriptor(txt).imports + +def get_builtins(): + """Returns a dictionary of built-in modules, functions and variables.""" + + return __builtin__.__dict__ + + +################################# +## Debugging utility functions ## +################################# + +def print_cache_for(txt, period=sys.maxint): + """Prints out the data cached for a given Text object. If no period is + given the text will not be reparsed and the cached version will be returned. + Otherwise if the period has expired the text will be reparsed. + """ + + desc = get_cached_descriptor(txt, period) + print '================================================' + print 'Name:', desc.name, '('+str(hash(txt))+')' + print '------------------------------------------------' + print 'Defs:' + for name, ddesc in desc.defs.items(): + print ' ', name, ddesc.params, ddesc.lineno + print '------------------------------------------------' + print 'Vars:' + for name, vdesc in desc.vars.items(): + print ' ', name, vdesc.type, vdesc.lineno + print '------------------------------------------------' + print 'Imports:' + for name, item in desc.imports.items(): + print ' ', name.ljust(15), item + print '------------------------------------------------' + print 'Classes:' + for clsnme, clsdsc in desc.classes.items(): + print ' *********************************' + print ' Name:', clsnme + print ' ---------------------------------' + print ' Defs:' + for name, ddesc in clsdsc.defs.items(): + print ' ', name, ddesc.params, ddesc.lineno + print ' ---------------------------------' + print ' Vars:' + for name, vdesc in clsdsc.vars.items(): + print ' ', name, vdesc.type, vdesc.lineno + print ' *********************************' + print '================================================' diff --git a/release/scripts/reeb.py b/release/scripts/reeb.py deleted file mode 100644 index 63ab1203708..00000000000 --- a/release/scripts/reeb.py +++ /dev/null @@ -1,110 +0,0 @@ -#!BPY - -""" -Name: 'Reeb graph import' -Blender: 245 -Group: 'Import' -Tooltip: 'Imports a reeb graph saved after skeleton generation' -""" -import Blender - -def name(count): - if count == -1: - return "" - else: - return "%05" % count - -def importGraph(count): - bNode = Blender.Draw.Create(1) - bSize = Blender.Draw.Create(0.01) - - Block = [] - - Block.append(("Size: ", bSize, 0.01, 10.0, "Size of the nodes")) - Block.append(("Nodes", bNode, "Import nodes as tetras")) - - retval = Blender.Draw.PupBlock("Reeb Graph Import", Block) - - if not retval: - return - - - me = Blender.Mesh.New("graph%s" % name(count)) - scn = Blender.Scene.GetCurrent() - - f = open("test%s.txt" % name(count), "r") - - verts = [] - edges = [] - faces = [] - - i = 0 - first = False - - SIZE = float(bSize.val) - WITH_NODE = bool(bNode.val) - - def addNode(v, s, verts, faces): - if WITH_NODE: - v1 = [v[0], v[1], v[2] + s] - i1 = len(verts) - verts.append(v1) - v2 = [v[0], v[1] + 0.959 * s, v[2] - 0.283 * s] - i2 = len(verts) - verts.append(v2) - v3 = [v[0] - 0.830 * s, v[1] - 0.479 * s, v[2] - 0.283 * s] - i3 = len(verts) - verts.append(v3) - v4 = [v[0] + 0.830 * s, v[1] - 0.479 * s, v[2] - 0.283 * s] - i4 = len(verts) - verts.append(v4) - - faces.append([i1,i2,i3]) - faces.append([i1,i3,i4]) - faces.append([i2,i3,i4]) - faces.append([i1,i2,i4]) - - return 4 - else: - return 0 - - for line in f: - data = line.strip().split(" ") - if data[0] == "v1": - v = [float(x) for x in data[-3:]] - i += addNode(v, SIZE, verts, faces) - verts.append(v) - i += 1 - elif data[0] == "v2": - pass - v = [float(x) for x in data[-3:]] - verts.append(v) - edges.append((i-1, i)) - i += 1 - i += addNode(v, SIZE, verts, faces) - elif data[0] == "b": - verts.append([float(x) for x in data[-3:]]) - edges.append((i-1, i)) - i += 1 -# elif data[0] == "angle": -# obj = scn.objects.new('Empty') -# obj.loc = (float(data[1]), float(data[2]), float(data[3])) -# obj.properties["angle"] = data[4] -# del obj - - - me.verts.extend(verts) - me.edges.extend(edges) - me.faces.extend(faces) - - - ob = scn.objects.new(me, "graph%s" % name(count)) - del ob - del scn - - -#for i in range(16): -# importGraph(i) - -if __name__=='__main__': - importGraph(-1) diff --git a/release/scripts/scripttemplate_text_plugin.py b/release/scripts/scripttemplate_text_plugin.py new file mode 100644 index 00000000000..4ae562736d3 --- /dev/null +++ b/release/scripts/scripttemplate_text_plugin.py @@ -0,0 +1,69 @@ +#!BPY +""" +Name: 'Text Plugin' +Blender: 246 +Group: 'ScriptTemplate' +Tooltip: 'Add a new text for writing a text plugin' +""" + +from Blender import Window +import bpy + +script_data = \ +'''#!BPY +""" +Name: 'My Plugin Script' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+Alt+U' +Tooltip: 'Put some useful info here' +""" + +# Add a licence here if you wish to re-distribute, we recommend the GPL + +from Blender import Window, sys +import BPyTextPlugin, bpy + +def my_script_util(txt): + # This function prints out statistical information about a script + + desc = BPyTextPlugin.get_cached_descriptor(txt) + print '---------------------------------------' + print 'Script Name:', desc.name + print 'Classes:', len(desc.classes) + print ' ', desc.classes.keys() + print 'Functions:', len(desc.defs) + print ' ', desc.defs.keys() + print 'Variables:', len(desc.vars) + print ' ', desc.vars.keys() + +def main(): + + # Gets the active text object, there can be many in one blend file. + txt = bpy.data.texts.active + + # Silently return if the script has been run with no active text + if not txt: + return + + # Text plug-ins should run quickly so we time it here + Window.WaitCursor(1) + t = sys.time() + + # Run our utility function + my_script_util(txt) + + # Timing the script is a good way to be aware on any speed hits when scripting + print 'Plugin script finished in %.2f seconds' % (sys.time()-t) + Window.WaitCursor(0) + + +# This lets you import the script without running it +if __name__ == '__main__': + main() +''' + +new_text = bpy.data.texts.new('textplugin_template.py') +new_text.write(script_data) +bpy.data.texts.active = new_text +Window.RedrawAll() diff --git a/release/scripts/textplugin_functiondocs.py b/release/scripts/textplugin_functiondocs.py new file mode 100644 index 00000000000..d9cf6657a25 --- /dev/null +++ b/release/scripts/textplugin_functiondocs.py @@ -0,0 +1,77 @@ +#!BPY +""" +Name: 'Function Documentation | Ctrl I' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+I' +Tooltip: 'Attempts to display documentation about the function preceding the cursor.' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + (line, c) = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + # Look backwards for first '(' without ')' + b = 0 + found = False + for i in range(c-1, -1, -1): + if line[i] == ')': b += 1 + elif line[i] == '(': + b -= 1 + if b < 0: + found = True + c = i + break + + # Otherwise identify the name under the cursor + if not found: + llen = len(line) + while c<llen and (line[c].isalnum() or line[c]=='_'): + c += 1 + + pre = get_targets(line, c) + + if len(pre) == 0: + return + + imports = get_imports(txt) + builtins = get_builtins() + + # Identify the root (root.sub.sub.) + if imports.has_key(pre[0]): + obj = imports[pre[0]] + elif builtins.has_key(pre[0]): + obj = builtins[pre[0]] + else: + return + + # Step through sub-attributes + try: + for name in pre[1:]: + obj = getattr(obj, name) + except AttributeError: + print "Attribute not found '%s' in '%s'" % (name, '.'.join(pre)) + return + + if hasattr(obj, '__doc__') and obj.__doc__: + txt.showDocs(obj.__doc__) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_imports.py b/release/scripts/textplugin_imports.py new file mode 100644 index 00000000000..ec608243c2b --- /dev/null +++ b/release/scripts/textplugin_imports.py @@ -0,0 +1,91 @@ +#!BPY +""" +Name: 'Import Complete|Space' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Space' +Tooltip: 'Lists modules when import or from is typed' +""" + +# Only run if we have the required modules +try: + import bpy, sys + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + line, c = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + pos = line.rfind('from ', 0, c) + + # No 'from' found + if pos == -1: + # Check instead for straight 'import xxxx' + pos2 = line.rfind('import ', 0, c) + if pos2 != -1: + pos2 += 7 + for i in range(pos2, c): + if line[i]==',' or (line[i]==' ' and line[i-1]==','): + pos2 = i+1 + elif not line[i].isalnum() and line[i] != '_': + return + items = [(m, 'm') for m in get_modules()] + items.sort(cmp = suggest_cmp) + txt.suggest(items, line[pos2:c].strip()) + return + + # Found 'from xxxxx' before cursor + immediate = True + pos += 5 + for i in range(pos, c): + if not line[i].isalnum() and line[i] != '_' and line[i] != '.': + immediate = False + break + + # Immediate 'from' followed by at most a module name + if immediate: + items = [(m, 'm') for m in get_modules()] + items.sort(cmp = suggest_cmp) + txt.suggest(items, line[pos:c]) + return + + # Found 'from' earlier, suggest import if not already there + pos2 = line.rfind('import ', pos, c) + + # No 'import' found after 'from' so suggest it + if pos2 == -1: + txt.suggest([('import', 'k')], '') + return + + # Immediate 'import' before cursor and after 'from...' + for i in range(pos2+7, c): + if line[i]==',' or (line[i]==' ' and line[i-1]==','): + pass + elif not line[i].isalnum() and line[i] != '_': + return + between = line[pos:pos2-1].strip() + try: + mod = get_module(between) + except ImportError: + return + + items = [('*', 'k')] + for (k,v) in mod.__dict__.items(): + items.append((k, type_char(v))) + items.sort(cmp = suggest_cmp) + txt.suggest(items, '') + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_membersuggest.py b/release/scripts/textplugin_membersuggest.py new file mode 100644 index 00000000000..2b261703e19 --- /dev/null +++ b/release/scripts/textplugin_membersuggest.py @@ -0,0 +1,101 @@ +#!BPY +""" +Name: 'Member Suggest | .' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Period' +Tooltip: 'Lists members of the object preceding the cursor in the current text space' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + (line, c) = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + pre = get_targets(line, c) + + if len(pre) <= 1: + return + + imports = get_imports(txt) + builtins = get_builtins() + + # Identify the root (root.sub.sub.) + obj = None + if pre[0] == '': + i = c - len('.'.join(pre)) - 1 + if i >= 0: + if line[i] == '"' or line[i] == "'": + obj = str + elif line[i] == '}': + obj = dict + elif line[i] == ']': # Could be array elem x[y] or list [y] + i = line.rfind('[', 0, i) - 1 + while i >= 0: + if line[i].isalnum() or line[i] == '_': + break + elif line[i] != ' ' and line[i] != '\t': + i = -1 + break + i -= 1 + if i < 0: + obj = list + elif imports.has_key(pre[0]): + obj = imports[pre[0]] + elif builtins.has_key(pre[0]): + obj = builtins[pre[0]] + else: + desc = get_cached_descriptor(txt) + if desc.vars.has_key(pre[0]): + obj = desc.vars[pre[0]].type + + if not obj: + return + + # Step through sub-attributes + try: + for name in pre[1:-1]: + obj = getattr(obj, name) + except AttributeError: + print "Attribute not found '%s' in '%s'" % (name, '.'.join(pre)) + return + + try: + attr = obj.__dict__.keys() + except AttributeError: + attr = dir(obj) + else: + if not attr: + attr = dir(obj) + + items = [] + for k in attr: + try: + v = getattr(obj, k) + except (AttributeError, TypeError): # Some attributes are not readable + pass + else: + items.append((k, type_char(v))) + + if items != []: + items.sort(cmp = suggest_cmp) + txt.suggest(items, pre[-1]) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_outliner.py b/release/scripts/textplugin_outliner.py new file mode 100644 index 00000000000..3879a2819a5 --- /dev/null +++ b/release/scripts/textplugin_outliner.py @@ -0,0 +1,142 @@ +#!BPY +""" +Name: 'Code Outline | Ctrl T' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+T' +Tooltip: 'Provides a menu for jumping to class and functions definitions.' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * + from Blender import Draw +except ImportError: + OK = False +else: + OK = True + +def make_menu(items, eventoffs): + n = len(items) + if n < 20: + return [(items[i], i+1+eventoffs) for i in range(len(items))] + + letters = [] + check = 'abcdefghijklmnopqrstuvwxyz_' # Names cannot start 0-9 + for c in check: + for item in items: + if item[0].lower() == c: + letters.append(c) + break + + entries = {} + i = 0 + for item in items: + i += 1 + c = item[0].lower() + entries.setdefault(c, []).append((item, i+eventoffs)) + + subs = [] + for c in letters: + subs.append((c, entries[c])) + + return subs + +def find_word(txt, word): + i = 0 + txt.reset() + while True: + try: + line = txt.readline() + except StopIteration: + break + c = line.find(word) + if c != -1: + txt.setCursorPos(i, c) + break + i += 1 + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + # Identify word under cursor + if get_context(txt) == CTX_NORMAL: + line, c = current_line(txt) + start = c-1 + end = c + while start >= 0: + if not line[start].lower() in 'abcdefghijklmnopqrstuvwxyz0123456789_': + break + start -= 1 + while end < len(line): + if not line[end].lower() in 'abcdefghijklmnopqrstuvwxyz0123456789_': + break + end += 1 + word = line[start+1:end] + if word in KEYWORDS: + word = None + else: + word = None + + script = get_cached_descriptor(txt) + items = [] + desc = None + + tmp = script.classes.keys() + tmp.sort(cmp = suggest_cmp) + class_menu = make_menu(tmp, len(items)) + class_menu_length = len(tmp) + items.extend(tmp) + + tmp = script.defs.keys() + tmp.sort(cmp = suggest_cmp) + defs_menu = make_menu(tmp, len(items)) + defs_menu_length = len(tmp) + items.extend(tmp) + + tmp = script.vars.keys() + tmp.sort(cmp = suggest_cmp) + vars_menu = make_menu(tmp, len(items)) + vars_menu_length = len(tmp) + items.extend(tmp) + + menu = [('Script %t', 0), + ('Classes', class_menu), + ('Functions', defs_menu), + ('Variables', vars_menu)] + if word: + menu.extend([None, ('Locate', [(word, -10)])]) + + i = Draw.PupTreeMenu(menu) + if i == -1: + return + + # Chosen to search for word under cursor + if i == -10: + if script.classes.has_key(word): + desc = script.classes[word] + elif script.defs.has_key(word): + desc = script.defs[word] + elif script.vars.has_key(word): + desc = script.vars[word] + else: + find_word(txt, word) + return + else: + i -= 1 + if i < class_menu_length: + desc = script.classes[items[i]] + elif i < class_menu_length + defs_menu_length: + desc = script.defs[items[i]] + elif i < class_menu_length + defs_menu_length + vars_menu_length: + desc = script.vars[items[i]] + + if desc: + txt.setCursorPos(desc.lineno-1, 0) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_suggest.py b/release/scripts/textplugin_suggest.py new file mode 100644 index 00000000000..1e011a2e82d --- /dev/null +++ b/release/scripts/textplugin_suggest.py @@ -0,0 +1,94 @@ +#!BPY +""" +Name: 'Suggest All | Ctrl Space' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Ctrl+Space' +Tooltip: 'Performs suggestions based on the context of the cursor' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * +except ImportError: + OK = False +else: + OK = True + +def check_membersuggest(line, c): + pos = line.rfind('.', 0, c) + if pos == -1: + return False + for s in line[pos+1:c]: + if not s.isalnum() and s != '_': + return False + return True + +def check_imports(line, c): + pos = line.rfind('import ', 0, c) + if pos > -1: + for s in line[pos+7:c]: + if not s.isalnum() and s != '_': + return False + return True + pos = line.rfind('from ', 0, c) + if pos > -1: + for s in line[pos+5:c]: + if not s.isalnum() and s != '_': + return False + return True + return False + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + (line, c) = current_line(txt) + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + # Check the character preceding the cursor and execute the corresponding script + + if check_membersuggest(line, c): + import textplugin_membersuggest + textplugin_membersuggest.main() + return + + elif check_imports(line, c): + import textplugin_imports + textplugin_imports.main() + return + + # Otherwise we suggest globals, keywords, etc. + list = [] + pre = get_targets(line, c) + desc = get_cached_descriptor(txt) + + for k in KEYWORDS: + list.append((k, 'k')) + + for k, v in get_builtins().items(): + list.append((k, type_char(v))) + + for k, v in desc.imports.items(): + list.append((k, type_char(v))) + + for k, v in desc.classes.items(): + list.append((k, 'f')) + + for k, v in desc.defs.items(): + list.append((k, 'f')) + + for k, v in desc.vars.items(): + list.append((k, 'v')) + + list.sort(cmp = suggest_cmp) + txt.suggest(list, pre[-1]) + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() diff --git a/release/scripts/textplugin_templates.py b/release/scripts/textplugin_templates.py new file mode 100644 index 00000000000..25dadf4de54 --- /dev/null +++ b/release/scripts/textplugin_templates.py @@ -0,0 +1,115 @@ +#!BPY +""" +Name: 'Template Completion | Tab' +Blender: 246 +Group: 'TextPlugin' +Shortcut: 'Tab' +Tooltip: 'Completes templates based on the text preceding the cursor' +""" + +# Only run if we have the required modules +try: + import bpy + from BPyTextPlugin import * + from Blender import Text +except ImportError: + OK = False +else: + OK = True + +templates = { + 'ie': + 'if ${1:cond}:\n' + '\t${2}\n' + 'else:\n' + '\t${3}\n', + 'iei': + 'if ${1:cond}:\n' + '\t${2}\n' + 'elif:\n' + '\t${3}\n' + 'else:\n' + '\t${4}\n', + 'def': + 'def ${1:name}(${2:params}):\n' + '\t"""(${2}) - ${3:comment}"""\n' + '\t${4}', + 'cls': + 'class ${1:name}(${2:parent}):\n' + '\t"""${3:docs}"""\n' + '\t\n' + '\tdef __init__(self, ${4:params}):\n' + '\t\t"""Creates a new ${1}"""\n' + '\t\t${5}' +} + +def main(): + txt = bpy.data.texts.active + if not txt: + return + + row, c = txt.getCursorPos() + line = txt.asLines(row, row+1)[0] + indent=0 + while indent<c and (line[indent]==' ' or line[indent]=='\t'): + indent += 1 + + # Check we are in a normal context + if get_context(txt) != CTX_NORMAL: + return + + targets = get_targets(line, c-1); + if len(targets) != 1: return + + color = (0, 192, 32) + + for trigger, template in templates.items(): + if trigger != targets[0]: continue + inserts = {} + txt.delete(-len(trigger)-1) + y, x = txt.getCursorPos() + first = None + + # Insert template text and parse for insertion points + count = len(template); i = 0 + while i < count: + if i<count-1 and template[i]=='$' and template[i+1]=='{': + i += 2 + e = template.find('}', i) + item = template[i:e].split(':') + if len(item)<2: item.append('') + if not inserts.has_key(item[0]): + inserts[item[0]] = (item[1], [(x, y)]) + else: + inserts[item[0]][1].append((x, y)) + item[1] = inserts[item[0]][0] + if not first: first = (item[1], x, y) + txt.insert(item[1]) + x += len(item[1]) + i = e + else: + txt.insert(template[i]) + if template[i] == '\n': + txt.insert(line[:indent]) + y += 1 + x = indent + else: + x += 1 + i += 1 + + # Insert markers at insertion points + for id, (text, points) in inserts.items(): + for x, y in points: + txt.setCursorPos(y, x) + txt.setSelectPos(y, x+len(text)) + txt.markSelection(hash(text)+int(id), color, Text.TMARK_TEMP | Text.TMARK_EDITALL) + if first: + text, x, y = first + txt.setCursorPos(y, x) + txt.setSelectPos(y, x+len(text)) + break + + +# Check we are running as a script and not imported as a module +if __name__ == "__main__" and OK: + main() |