From bdc030c664640db727ea21a1e854bb62032bf705 Mon Sep 17 00:00:00 2001 From: Ian Thompson Date: Tue, 24 Jun 2008 15:25:25 +0000 Subject: Text plugin basis with plugin for suggestions/completions. The suggest plugin works for imported global variables, methods, modules and module members. For example typing: import Blender from Blender import * | <- cursor here suggests globals Blender.Draw.gl| <- cursor here suggests all Draw members starting gl Currently suggestions are listed in the console when the space is redrawn but will be presented as a menu-style list soon. Also to add are shortcut/activation keys to allow plugins to respond to certain key strokes. --- release/scripts/textplugin_suggest.py | 234 +++++++++++++++++++++++++ source/blender/blenkernel/BKE_suggestions.h | 77 ++++++++ source/blender/blenkernel/intern/suggestions.c | 125 +++++++++++++ source/blender/python/BPY_interface.c | 1 + source/blender/python/BPY_menus.c | 5 + source/blender/python/BPY_menus.h | 1 + source/blender/python/api2_2x/Text.c | 57 ++++++ source/blender/python/api2_2x/doc/Text.py | 12 +- source/blender/src/drawtext.c | 15 ++ source/blender/src/header_text.c | 32 ++++ source/blender/src/usiblender.c | 3 + 11 files changed, 561 insertions(+), 1 deletion(-) create mode 100644 release/scripts/textplugin_suggest.py create mode 100644 source/blender/blenkernel/BKE_suggestions.h create mode 100644 source/blender/blenkernel/intern/suggestions.c diff --git a/release/scripts/textplugin_suggest.py b/release/scripts/textplugin_suggest.py new file mode 100644 index 00000000000..77ae0488b1c --- /dev/null +++ b/release/scripts/textplugin_suggest.py @@ -0,0 +1,234 @@ +#!BPY +""" +Name: 'Suggest' +Blender: 243 +Group: 'TextPlugin' +Tooltip: 'Suggests completions for the word at the cursor in a python script' +""" + +import bpy +from Blender import Text +from StringIO import StringIO +from inspect import * +from tokenize import generate_tokens +import token + +TK_TYPE = 0 +TK_TOKEN = 1 +TK_START = 2 #(srow, scol) +TK_END = 3 #(erow, ecol) +TK_LINE = 4 +TK_ROW = 0 +TK_COL = 1 + +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' ] + +execs = [] # Used to establish the same import context across defs (import is scope sensitive) + +def getTokens(txt): + global tokens_cached + if tokens_cached==None: + lines = txt.asLines() + str = '\n'.join(lines) + readline = StringIO(str).readline + g = generate_tokens(readline) + tokens = [] + for t in g: tokens.append(t) + tokens_cached = tokens + return tokens_cached +tokens_cached = None + +def isNameChar(s): + return s.isalnum() or s in ['_'] + +# Returns words preceding the cursor that are separated by periods as a list in the +# same order +def getCompletionSymbols(txt): + (l, c)= txt.getCursorPos() + lines = txt.asLines() + line = lines[l] + a=0 + for a in range(1, c+1): + if not isNameChar(line[c-a]) and line[c-a]!='.': + a -= 1 + break + return line[c-a:c].split('.') + + +# Returns a list of tuples of symbol names and their types (name, type) where +# type is one of: +# m (module/class) Has its own members (includes classes) +# v (variable) Has a type which may have its own members +# f (function) Callable and may have a return type (with its own members) +# It also updates the global import context (via execs) +def getGlobals(txt): + global execs + + tokens = getTokens(txt) + globals = dict() + for i in range(len(tokens)): + + # Handle all import statements + if i>=1 and tokens[i-1][TK_TOKEN]=='import': + + # Find 'from' if it exists + fr= -1 + for a in range(1, i): + if tokens[i-a][TK_TYPE]==token.NEWLINE: break + if tokens[i-a][TK_TOKEN]=='from': + fr=i-a + break + + # Handle: import ___[,___] + if fr<0: + + while True: + if tokens[i][TK_TYPE]==token.NAME: + # Add the import to the execs list + x = tokens[i][TK_LINE].strip() + k = tokens[i][TK_TOKEN] + execs.append(x) + + # Add the symbol name to the return list + globals[k] = 'm' + elif tokens[i][TK_TOKEN]!=',': + break + i += 1 + + # Handle statement: from ___[.___] import ___[,___] + else: # fr>=0: + + # Add the import to the execs list + x = tokens[i][TK_LINE].strip() + execs.append(x) + + # Import parent module so we can process it for sub modules + parent = ''.join([t[TK_TOKEN] for t in tokens[fr+1:i-1]]) + exec "import "+parent + + # All submodules, functions, etc. + if tokens[i][TK_TOKEN]=='*': + + # Add each symbol name to the return list + exec "d="+parent+".__dict__.items()" + for k,v in d: + if not globals.has_key(k) or not globals[k]: + t='v' + if ismodule(v): t='m' + elif callable(v): t='f' + globals[k] = t + + # Specific function, submodule, etc. + else: + while True: + if tokens[i][TK_TYPE]==token.NAME: + k = tokens[i][TK_TOKEN] + if not globals.has_key(k) or not globals[k]: + t='v' + try: + exec 'v='+parent+'.'+k + if ismodule(v): t='m' + elif callable(v): t='f' + except: pass + globals[k] = t + elif tokens[i][TK_TOKEN]!=',': + break + i += 1 + + elif tokens[i][TK_TYPE]==token.NAME and tokens[i][TK_TOKEN] not in keywords and (i==0 or tokens[i-1][TK_TOKEN]!='.'): + k = tokens[i][TK_TOKEN] + if not globals.has_key(k) or not globals[k]: + t=None + if (i>0 and tokens[i-1][TK_TOKEN]=='def'): + t='f' + else: + t='v' + globals[k] = t + + return globals + +def cmpi0(x, y): + return cmp(x[0].lower(), y[0].lower()) + +def globalSuggest(txt, cs): + global execs + + suggestions = dict() + (row, col) = txt.getCursorPos() + globals = getGlobals(txt) + + # Sometimes we have conditional includes which will fail if the module + # cannot be found. So we protect outselves in a try block + for x in execs: + exec 'try: '+x+'\nexcept: pass' + + if len(cs)==0: + sub = '' + else: + sub = cs[0].lower() + print 'Search:', sub + + for k,t in globals.items(): + if k.lower().startswith(sub): + suggestions[k] = t + + l = list(suggestions.items()) + return sorted (l, cmp=cmpi0) + +# Only works for 'static' members (eg. Text.Get) +def memberSuggest(txt, cs): + global execs + + # Populate the execs for imports + getGlobals(txt) + + # Sometimes we have conditional includes which will fail if the module + # cannot be found. So we protect outselves in a try block + for x in execs: + exec 'try: '+x+'\nexcept: pass' + + suggestions = dict() + (row, col) = txt.getCursorPos() + + sub = cs[len(cs)-1].lower() + print 'Search:', sub + + t=None + pre='.'.join(cs[:-1]) + try: + exec "t="+pre + except: + print 'Failed to assign '+pre + print execs + print cs + + if t!=None: + for k,v in t.__dict__.items(): + if ismodule(v): t='m' + elif callable(v): t='f' + else: t='v' + if k.lower().startswith(sub): + suggestions[k] = t + + l = list(suggestions.items()) + return sorted (l, cmp=cmpi0) + +def main(): + txt = bpy.data.texts.active + if txt==None: return + + cs = getCompletionSymbols(txt) + + if len(cs)<=1: + l = globalSuggest(txt, cs) + txt.suggest(l, cs[len(cs)-1]) + + else: + l = memberSuggest(txt, cs) + txt.suggest(l, cs[len(cs)-1]) + +main() diff --git a/source/blender/blenkernel/BKE_suggestions.h b/source/blender/blenkernel/BKE_suggestions.h new file mode 100644 index 00000000000..bc4e18f5a67 --- /dev/null +++ b/source/blender/blenkernel/BKE_suggestions.h @@ -0,0 +1,77 @@ +/** + * $Id: $ + * + * ***** BEGIN GPL LICENSE BLOCK ***** + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * The Original Code is Copyright (C) 2008, Blender Foundation + * All rights reserved. + * + * The Original Code is: all of this file. + * + * Contributor(s): none yet. + * + * ***** END GPL LICENSE BLOCK ***** + */ +#ifndef BKE_SUGGESTIONS_H +#define BKE_SUGGESTIONS_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* **************************************************************************** +Suggestions must be added in sorted order (no attempt is made to sort the list) +The list is then divided up based on the prefix provided by update_suggestions: +Example: + Prefix: ab + aaa <-- first + aab + aba <-- firstmatch + abb <-- lastmatch + baa + bab <-- last +**************************************************************************** */ + +struct Text; + +typedef struct SuggItem { + struct SuggItem *prev, *next; + char *name; + char type; +} SuggItem; + +typedef struct SuggList { + SuggItem *first, *last; + SuggItem *firstmatch, *lastmatch; +} SuggList; + +void free_suggestions(); + +void add_suggestion(const char *name, char type); +void update_suggestions(const char *prefix); +SuggItem *suggest_first(); +SuggItem *suggest_last(); + +void set_suggest_text(Text *text); +void clear_suggest_text(); +short is_suggest_active(Text *text); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/source/blender/blenkernel/intern/suggestions.c b/source/blender/blenkernel/intern/suggestions.c new file mode 100644 index 00000000000..3842146376d --- /dev/null +++ b/source/blender/blenkernel/intern/suggestions.c @@ -0,0 +1,125 @@ +/** + * $Id: $ + * + * ***** BEGIN GPL LICENSE BLOCK ***** + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * + * The Original Code is Copyright (C) 2008, Blender Foundation + * All rights reserved. + * + * The Original Code is: all of this file. + * + * Contributor(s): none yet. + * + * ***** END GPL LICENSE BLOCK ***** + */ + +#include +#include + +#include "MEM_guardedalloc.h" +#include "BLI_blenlib.h" +#include "DNA_text_types.h" +#include "BKE_text.h" +#include "BKE_suggestions.h" + +static SuggList suggestions= {NULL, NULL, NULL, NULL}; +static Text *suggText = NULL; + +void free_suggestions() { + SuggItem *item; + for (item = suggestions.last; item; item=item->prev) + MEM_freeN(item); + suggestions.first = suggestions.last = NULL; + suggestions.firstmatch = suggestions.lastmatch = NULL; +} + +void add_suggestion(const char *name, char type) { + SuggItem *newitem; + + newitem = MEM_mallocN(sizeof(SuggItem) + strlen(name) + 1, "SuggestionItem"); + if (!newitem) { + printf("Failed to allocate memory for suggestion.\n"); + return; + } + + newitem->name = (char *) (newitem + 1); + strcpy(newitem->name, name); + newitem->type = type; + newitem->prev = newitem->next = NULL; + + if (!suggestions.first) { + suggestions.first = suggestions.last = newitem; + } else { + newitem->prev = suggestions.last; + suggestions.last->next = newitem; + suggestions.last = newitem; + } +} + +void update_suggestions(const char *prefix) { + SuggItem *match, *first, *last; + int cmp, len = strlen(prefix); + + if (!suggestions.first) return; + if (len==0) { + suggestions.firstmatch = suggestions.first; + suggestions.lastmatch = suggestions.last; + return; + } + + first = last = NULL; + for (match=suggestions.first; match; match=match->next) { + cmp = strncmp(prefix, match->name, len); + if (cmp==0) { + if (!first) + first = match; + } else if (cmp<0) { + if (!last) { + last = match->prev; + break; + } + } + } + if (first) { + if (!last) last = suggestions.last; + suggestions.firstmatch = first; + suggestions.lastmatch = last; + } else { + suggestions.firstmatch = suggestions.lastmatch = NULL; + } +} + +SuggItem *suggest_first() { + return suggestions.firstmatch; +} + +SuggItem *suggest_last() { + return suggestions.lastmatch; +} + +void set_suggest_text(Text *text) { + suggText = text; +} + +void clear_suggest_text() { + free_suggestions(); + suggText = NULL; +} + +short is_suggest_active(Text *text) { + return suggText==text ? 1 : 0; +} diff --git a/source/blender/python/BPY_interface.c b/source/blender/python/BPY_interface.c index 7c23c86d9ba..360c8fd7f04 100644 --- a/source/blender/python/BPY_interface.c +++ b/source/blender/python/BPY_interface.c @@ -1066,6 +1066,7 @@ int BPY_menu_do_python( short menutype, int event ) case PYMENU_RENDER: case PYMENU_WIZARDS: case PYMENU_SCRIPTTEMPLATE: + case PYMENU_TEXTPLUGIN: case PYMENU_MESHFACEKEY: break; diff --git a/source/blender/python/BPY_menus.c b/source/blender/python/BPY_menus.c index 82da9edbee6..08691973a92 100644 --- a/source/blender/python/BPY_menus.c +++ b/source/blender/python/BPY_menus.c @@ -106,6 +106,8 @@ static int bpymenu_group_atoi( char *str ) return PYMENU_ARMATURE; else if( !strcmp( str, "ScriptTemplate" ) ) return PYMENU_SCRIPTTEMPLATE; + else if( !strcmp( str, "TextPlugin" ) ) + return PYMENU_TEXTPLUGIN; else if( !strcmp( str, "MeshFaceKey" ) ) return PYMENU_MESHFACEKEY; else if( !strcmp( str, "AddMesh" ) ) @@ -184,6 +186,9 @@ char *BPyMenu_group_itoa( short menugroup ) case PYMENU_SCRIPTTEMPLATE: return "ScriptTemplate"; break; + case PYMENU_TEXTPLUGIN: + return "TextPlugin"; + break; case PYMENU_MESHFACEKEY: return "MeshFaceKey"; break; diff --git a/source/blender/python/BPY_menus.h b/source/blender/python/BPY_menus.h index 1b557f79286..e8bca09d50e 100644 --- a/source/blender/python/BPY_menus.h +++ b/source/blender/python/BPY_menus.h @@ -99,6 +99,7 @@ typedef enum { PYMENU_UVCALCULATION, PYMENU_ARMATURE, PYMENU_SCRIPTTEMPLATE, + PYMENU_TEXTPLUGIN, PYMENU_HELP,/*Main Help menu items - prob best to leave for 'official' ones*/ PYMENU_HELPSYSTEM,/* Resources, troubleshooting, system tools */ PYMENU_HELPWEBSITES,/* Help -> Websites submenu */ diff --git a/source/blender/python/api2_2x/Text.c b/source/blender/python/api2_2x/Text.c index 603deb768ad..63c77c0bb3e 100644 --- a/source/blender/python/api2_2x/Text.c +++ b/source/blender/python/api2_2x/Text.c @@ -34,8 +34,11 @@ #include "BKE_global.h" #include "BKE_main.h" #include "BIF_drawtext.h" +#include "BIF_screen.h" #include "BKE_text.h" +#include "BKE_suggestions.h" #include "BLI_blenlib.h" +#include "DNA_screen_types.h" #include "DNA_space_types.h" #include "gen_utils.h" #include "gen_library.h" @@ -96,6 +99,7 @@ static PyObject *Text_set( BPy_Text * self, PyObject * args ); static PyObject *Text_asLines( BPy_Text * self ); static PyObject *Text_getCursorPos( BPy_Text * self ); static PyObject *Text_setCursorPos( BPy_Text * self, PyObject * args ); +static PyObject *Text_suggest( BPy_Text * self, PyObject * args ); /*****************************************************************************/ /* Python BPy_Text methods table: */ @@ -124,6 +128,8 @@ static PyMethodDef BPy_Text_methods[] = { "() - Return cursor position as (row, col) tuple"}, {"setCursorPos", ( PyCFunction ) Text_setCursorPos, METH_VARARGS, "(row, col) - Set the cursor position to (row, col)"}, + {"suggest", ( PyCFunction ) Text_suggest, METH_VARARGS, + "(list) - List of tuples of the form (name, type) where type is one of 'm', 'v', 'f' for module, variable and function respectively"}, {NULL, NULL, 0, NULL} }; @@ -511,6 +517,57 @@ static PyObject *Text_setCursorPos( BPy_Text * self, PyObject * args ) Py_RETURN_NONE; } +static PyObject *Text_suggest( BPy_Text * self, PyObject * args ) +{ + PyObject *item = NULL; + PyObject *list = NULL, *resl = NULL; + int list_len, i; + char *prefix, *name, type; + SpaceText *st; + + if(!self->text) + return EXPP_ReturnPyObjError(PyExc_RuntimeError, + "This object isn't linked to a Blender Text Object"); + + /* Parse args for a list of tuples */ + if(!PyArg_ParseTuple(args, "O!s", &PyList_Type, &list, &prefix)) + return EXPP_ReturnPyObjError(PyExc_TypeError, + "expected list of tuples followed by a string"); + + if (curarea->spacetype != SPACE_TEXT) + return EXPP_ReturnPyObjError(PyExc_RuntimeError, + "Active space type is not text"); + + st = curarea->spacedata.first; + if (!st || !st->text) + return EXPP_ReturnPyObjError(PyExc_RuntimeError, + "Active text area has no Text object"); + + list_len = PyList_Size(list); + clear_suggest_text(); + + for (i = 0; i < list_len; i++) { + item = PyList_GetItem(list, i); + if (!PyTuple_Check(item) || PyTuple_GET_SIZE(item) != 2) + return EXPP_ReturnPyObjError(PyExc_AttributeError, + "list must contain only tuples of size 2" ); + + name = PyString_AsString(PyTuple_GetItem(item, 0)); + type = PyString_AsString(PyTuple_GetItem(item, 1))[0]; + + if (!strlen(name) || (type!='m' && type!='v' && type!='f')) + return EXPP_ReturnPyObjError(PyExc_AttributeError, + "layer values must be in the range [1, 20]" ); + + add_suggestion(name, type); + } + update_suggestions(prefix); + set_suggest_text(st->text); + scrarea_queue_redraw(curarea); + + Py_RETURN_NONE; +} + /*****************************************************************************/ /* Function: Text_compare */ /* Description: This is a callback function for the BPy_Text type. It */ diff --git a/source/blender/python/api2_2x/doc/Text.py b/source/blender/python/api2_2x/doc/Text.py index 4099b13828d..920908eef81 100644 --- a/source/blender/python/api2_2x/doc/Text.py +++ b/source/blender/python/api2_2x/doc/Text.py @@ -150,5 +150,15 @@ class Text: cursor. """ + def suggest(list): + """ + Set the suggestion list to the given list of tuples. This list *must* be + sorted by its first element, name. + @type list: list of tuples + @param list: List of pair-tuples of the form (name, type) where name is + the suggested name and type is one of 'm' (module or class), 'f' + (function or method), 'v' (variable). + """ + import id_generics -Text.__doc__ += id_generics.attributes \ No newline at end of file +Text.__doc__ += id_generics.attributes diff --git a/source/blender/src/drawtext.c b/source/blender/src/drawtext.c index 882baa90c65..227d1f08c20 100644 --- a/source/blender/src/drawtext.c +++ b/source/blender/src/drawtext.c @@ -60,6 +60,7 @@ #include "BKE_global.h" #include "BKE_main.h" #include "BKE_node.h" +#include "BKE_suggestions.h" #include "BIF_gl.h" #include "BIF_glutil.h" @@ -999,6 +1000,19 @@ static void do_selection(SpaceText *st, int selecting) txt_undo_add_toop(st->text, UNDO_STO, sell, selc, linep2, charp2); } +void draw_suggestion_list(SpaceText *st) { + SuggItem *item, *last; + + if (!is_suggest_active(st->text)) return; + + for (item=suggest_first(), last=suggest_last(); item; item=item->next) { + /* Useful for testing but soon to be replaced by UI list */ + printf("Suggest: %c %s\n", item->type, item->name); + if (item == last) + break; + } +} + void drawtextspace(ScrArea *sa, void *spacedata) { SpaceText *st= curarea->spacedata.first; @@ -1072,6 +1086,7 @@ void drawtextspace(ScrArea *sa, void *spacedata) } draw_textscroll(st); + draw_suggestion_list(st); curarea->win_swap= WIN_BACK_OK; } diff --git a/source/blender/src/header_text.c b/source/blender/src/header_text.c index 7f281096479..e371bd56160 100644 --- a/source/blender/src/header_text.c +++ b/source/blender/src/header_text.c @@ -240,6 +240,37 @@ static uiBlock *text_template_scriptsmenu (void *args_unused) return block; } +static void do_text_plugin_scriptsmenu(void *arg, int event) +{ + BPY_menu_do_python(PYMENU_TEXTPLUGIN, event); + + allqueue(REDRAWIMAGE, 0); +} + +static uiBlock *text_plugin_scriptsmenu (void *args_unused) +{ + uiBlock *block; + BPyMenu *pym; + int i= 0; + short yco = 20, menuwidth = 120; + + block= uiNewBlock(&curarea->uiblocks, "text_plugin_scriptsmenu", UI_EMBOSSP, UI_HELV, G.curscreen->mainwin); + uiBlockSetButmFunc(block, do_text_plugin_scriptsmenu, NULL); + + /* note that we acount for the N previous entries with i+20: */ + for (pym = BPyMenuTable[PYMENU_TEXTPLUGIN]; pym; pym = pym->next, i++) { + + uiDefIconTextBut(block, BUTM, 1, ICON_PYTHON, pym->name, 0, yco-=20, menuwidth, 19, + NULL, 0.0, 0.0, 1, i, + pym->tooltip?pym->tooltip:pym->filename); + } + + uiBlockSetDirection(block, UI_RIGHT); + uiTextBoundsBlock(block, 60); + + return block; +} + /* action executed after clicking in File menu */ static void do_text_filemenu(void *arg, int event) { @@ -726,6 +757,7 @@ static uiBlock *text_filemenu(void *arg_unused) } uiDefIconTextBlockBut(block, text_template_scriptsmenu, NULL, ICON_RIGHTARROW_THIN, "Script Templates", 0, yco-=20, 120, 19, ""); + uiDefIconTextBlockBut(block, text_plugin_scriptsmenu, NULL, ICON_RIGHTARROW_THIN, "Text Plugins", 0, yco-=20, 120, 19, ""); if(curarea->headertype==HEADERTOP) { uiBlockSetDirection(block, UI_DOWN); diff --git a/source/blender/src/usiblender.c b/source/blender/src/usiblender.c index 6c0838288b8..2e55f8cdbc2 100644 --- a/source/blender/src/usiblender.c +++ b/source/blender/src/usiblender.c @@ -67,6 +67,7 @@ #include "DNA_sound_types.h" #include "DNA_scene_types.h" #include "DNA_screen_types.h" +#include "DNA_text_types.h" #include "BKE_blender.h" #include "BKE_curve.h" @@ -79,6 +80,7 @@ #include "BKE_mball.h" #include "BKE_node.h" #include "BKE_packedFile.h" +#include "BKE_suggestions.h" #include "BKE_texture.h" #include "BKE_utildefines.h" #include "BKE_pointcache.h" @@ -1091,6 +1093,7 @@ void exit_usiblender(void) free_actcopybuf(); free_vertexpaint(); free_imagepaint(); + free_suggestions(); /* editnurb can remain to exist outside editmode */ freeNurblist(&editNurb); -- cgit v1.2.3