From e68834c75bc3e69641a6332dab071ebaa2d9d1d3 Mon Sep 17 00:00:00 2001 From: Ian Thompson Date: Wed, 25 Jun 2008 13:51:54 +0000 Subject: Added UI for suggestions list. Works with arrow-keys and mouse wheel, accept with Enter, reject with Esc or click elsewhere. Mouse selection not yet supported. The script is called from the File->Text Plugins menu. Tidied python script, the C suggestions functions and fixed some bugs including suggestions not being freed properly. --- release/scripts/textplugin_suggest.py | 128 ++++++++-------- source/blender/blenkernel/BKE_suggestions.h | 14 +- source/blender/blenkernel/intern/suggestions.c | 32 ++-- source/blender/python/api2_2x/Text.c | 14 +- source/blender/src/drawtext.c | 200 +++++++++++++++++++++++-- 5 files changed, 293 insertions(+), 95 deletions(-) diff --git a/release/scripts/textplugin_suggest.py b/release/scripts/textplugin_suggest.py index 77ae0488b1c..9861298209c 100644 --- a/release/scripts/textplugin_suggest.py +++ b/release/scripts/textplugin_suggest.py @@ -6,12 +6,11 @@ Group: 'TextPlugin' Tooltip: 'Suggests completions for the word at the cursor in a python script' """ -import bpy +import bpy, __builtin__, token from Blender import Text from StringIO import StringIO from inspect import * from tokenize import generate_tokens -import token TK_TYPE = 0 TK_TOKEN = 1 @@ -21,32 +20,48 @@ TK_LINE = 4 TK_ROW = 0 TK_COL = 1 +execs = [] # Used to establish the same import context across defs + 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 getBuiltins(): + builtins = [] + bi = dir(__builtin__) + for k in bi: + v = eval(k) + if ismodule(v): t='m' + elif callable(v): t='f' + else: t='v' + builtins.append((k, t)) + return builtins + + +def getKeywords(): + global keywords + return [(k, 'k') for k in keywords] + 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 + lines = txt.asLines() + str = '\n'.join(lines) + readline = StringIO(str).readline + g = generate_tokens(readline) + tokens = [] + for t in g: tokens.append(t) + return tokens + def isNameChar(s): - return s.isalnum() or s in ['_'] + return (s.isalnum() or s == '_') -# Returns words preceding the cursor that are separated by periods as a list in the -# same order + +# 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() @@ -68,7 +83,14 @@ def getCompletionSymbols(txt): def getGlobals(txt): global execs - tokens = getTokens(txt) + # Unfortunately, tokenize may fail if the script leaves brackets or strings + # open. For now we return an empty list, leaving builtins and keywords as + # the only globals. (on the TODO list) + try: + tokens = getTokens(txt) + except: + return [] + globals = dict() for i in range(len(tokens)): @@ -92,6 +114,7 @@ def getGlobals(txt): x = tokens[i][TK_LINE].strip() k = tokens[i][TK_TOKEN] execs.append(x) + exec 'try: '+x+'\nexcept: pass' # Add the symbol name to the return list globals[k] = 'm' @@ -105,16 +128,17 @@ def getGlobals(txt): # Add the import to the execs list x = tokens[i][TK_LINE].strip() execs.append(x) + exec 'try: '+x+'\nexcept: pass' # 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 + exec 'try: import '+parent+'\nexcept: pass' # All submodules, functions, etc. if tokens[i][TK_TOKEN]=='*': # Add each symbol name to the return list - exec "d="+parent+".__dict__.items()" + d = eval(parent).__dict__.items() for k,v in d: if not globals.has_key(k) or not globals[k]: t='v' @@ -130,7 +154,7 @@ def getGlobals(txt): if not globals.has_key(k) or not globals[k]: t='v' try: - exec 'v='+parent+'.'+k + v = eval(parent+'.'+k) if ismodule(v): t='m' elif callable(v): t='f' except: pass @@ -149,35 +173,13 @@ def getGlobals(txt): t='v' globals[k] = t - return globals + return globals.items() -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) + return globals + # Only works for 'static' members (eg. Text.Get) def memberSuggest(txt, cs): @@ -194,28 +196,28 @@ def memberSuggest(txt, cs): suggestions = dict() (row, col) = txt.getCursorPos() - sub = cs[len(cs)-1].lower() - print 'Search:', sub + sub = cs[len(cs)-1] - t=None + m=None pre='.'.join(cs[:-1]) try: - exec "t="+pre + m = eval(pre) except: - print 'Failed to assign '+pre - print execs - print cs + print pre+ ' not found or not imported.' - if t!=None: - for k,v in t.__dict__.items(): + if m!=None: + for k,v in m.__dict__.items(): if ismodule(v): t='m' elif callable(v): t='f' else: t='v' - if k.lower().startswith(sub): - suggestions[k] = t + suggestions[k] = t - l = list(suggestions.items()) - return sorted (l, cmp=cmpi0) + return suggestions.items() + + +def cmp0(x, y): + return cmp(x[0], y[0]) + def main(): txt = bpy.data.texts.active @@ -225,10 +227,12 @@ def main(): if len(cs)<=1: l = globalSuggest(txt, cs) - txt.suggest(l, cs[len(cs)-1]) - + l.extend(getBuiltins()) + l.extend(getKeywords()) else: l = memberSuggest(txt, cs) - txt.suggest(l, cs[len(cs)-1]) + + l.sort(cmp=cmp0) + txt.suggest(l, cs[len(cs)-1]) main() diff --git a/source/blender/blenkernel/BKE_suggestions.h b/source/blender/blenkernel/BKE_suggestions.h index bc4e18f5a67..d0f982263c0 100644 --- a/source/blender/blenkernel/BKE_suggestions.h +++ b/source/blender/blenkernel/BKE_suggestions.h @@ -57,18 +57,22 @@ typedef struct SuggItem { typedef struct SuggList { SuggItem *first, *last; SuggItem *firstmatch, *lastmatch; + SuggItem *selected; } SuggList; void free_suggestions(); -void add_suggestion(const char *name, char type); -void update_suggestions(const char *prefix); +void suggest_add(const char *name, char type); +void suggest_prefix(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); +void suggest_set_text(Text *text); +void suggest_clear_text(); +short suggest_is_active(Text *text); + +void suggest_set_selected(SuggItem *sel); +SuggItem *suggest_get_selected(); #ifdef __cplusplus } diff --git a/source/blender/blenkernel/intern/suggestions.c b/source/blender/blenkernel/intern/suggestions.c index 3842146376d..ae0c7baa146 100644 --- a/source/blender/blenkernel/intern/suggestions.c +++ b/source/blender/blenkernel/intern/suggestions.c @@ -40,14 +40,17 @@ static SuggList suggestions= {NULL, NULL, NULL, NULL}; static Text *suggText = NULL; void free_suggestions() { - SuggItem *item; - for (item = suggestions.last; item; item=item->prev) + SuggItem *item, *prev; + for (item = suggestions.last; item; item=prev) { + prev = item->prev; MEM_freeN(item); + } suggestions.first = suggestions.last = NULL; suggestions.firstmatch = suggestions.lastmatch = NULL; + suggestions.selected = NULL; } -void add_suggestion(const char *name, char type) { +void suggest_add(const char *name, char type) { SuggItem *newitem; newitem = MEM_mallocN(sizeof(SuggItem) + strlen(name) + 1, "SuggestionItem"); @@ -63,6 +66,7 @@ void add_suggestion(const char *name, char type) { if (!suggestions.first) { suggestions.first = suggestions.last = newitem; + suggestions.selected = newitem; } else { newitem->prev = suggestions.last; suggestions.last->next = newitem; @@ -70,13 +74,13 @@ void add_suggestion(const char *name, char type) { } } -void update_suggestions(const char *prefix) { +void suggest_prefix(const char *prefix) { SuggItem *match, *first, *last; int cmp, len = strlen(prefix); if (!suggestions.first) return; if (len==0) { - suggestions.firstmatch = suggestions.first; + suggestions.selected = suggestions.firstmatch = suggestions.first; suggestions.lastmatch = suggestions.last; return; } @@ -96,10 +100,10 @@ void update_suggestions(const char *prefix) { } if (first) { if (!last) last = suggestions.last; - suggestions.firstmatch = first; + suggestions.selected = suggestions.firstmatch = first; suggestions.lastmatch = last; } else { - suggestions.firstmatch = suggestions.lastmatch = NULL; + suggestions.selected = suggestions.firstmatch = suggestions.lastmatch = NULL; } } @@ -111,15 +115,23 @@ SuggItem *suggest_last() { return suggestions.lastmatch; } -void set_suggest_text(Text *text) { +void suggest_set_text(Text *text) { suggText = text; } -void clear_suggest_text() { +void suggest_clear_text() { free_suggestions(); suggText = NULL; } -short is_suggest_active(Text *text) { +short suggest_is_active(Text *text) { return suggText==text ? 1 : 0; } + +void suggest_set_selected(SuggItem *sel) { + suggestions.selected = sel; +} + +SuggItem *suggest_get_selected() { + return suggestions.selected; +} diff --git a/source/blender/python/api2_2x/Text.c b/source/blender/python/api2_2x/Text.c index 63c77c0bb3e..0c612a95f3a 100644 --- a/source/blender/python/api2_2x/Text.c +++ b/source/blender/python/api2_2x/Text.c @@ -129,7 +129,7 @@ static PyMethodDef BPy_Text_methods[] = { {"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"}, + "(list) - List of tuples of the form (name, type) where type is one of 'm', 'v', 'f', 'k' for module, variable, function and keyword respectively"}, {NULL, NULL, 0, NULL} }; @@ -544,7 +544,7 @@ static PyObject *Text_suggest( BPy_Text * self, PyObject * args ) "Active text area has no Text object"); list_len = PyList_Size(list); - clear_suggest_text(); + suggest_clear_text(); for (i = 0; i < list_len; i++) { item = PyList_GetItem(list, i); @@ -555,14 +555,14 @@ static PyObject *Text_suggest( BPy_Text * self, PyObject * args ) name = PyString_AsString(PyTuple_GetItem(item, 0)); type = PyString_AsString(PyTuple_GetItem(item, 1))[0]; - if (!strlen(name) || (type!='m' && type!='v' && type!='f')) + if (!strlen(name) || (type!='m' && type!='v' && type!='f' && type!='k')) return EXPP_ReturnPyObjError(PyExc_AttributeError, - "layer values must be in the range [1, 20]" ); + "names must be non-empty and types in ['m', 'v', 'f', 'k']" ); - add_suggestion(name, type); + suggest_add(name, type); } - update_suggestions(prefix); - set_suggest_text(st->text); + suggest_prefix(prefix); + suggest_set_text(st->text); scrarea_queue_redraw(curarea); Py_RETURN_NONE; diff --git a/source/blender/src/drawtext.c b/source/blender/src/drawtext.c index 227d1f08c20..5cececa40a7 100644 --- a/source/blender/src/drawtext.c +++ b/source/blender/src/drawtext.c @@ -99,6 +99,10 @@ static int check_delim(char *string); static int check_numbers(char *string); static int check_builtinfuncs(char *string); static int check_specialvars(char *string); +static int check_identifier(char ch); + +static void get_suggest_prefix(Text *text); +static void confirm_suggestion(Text *text); static void *last_txt_find_string= NULL; @@ -1000,16 +1004,63 @@ static void do_selection(SpaceText *st, int selecting) txt_undo_add_toop(st->text, UNDO_STO, sell, selc, linep2, charp2); } +#define SUGG_LIST_SIZE 7 +#define SUGG_LIST_WIDTH 20 + void draw_suggestion_list(SpaceText *st) { - SuggItem *item, *last; + SuggItem *item, *first, *last, *sel; + TextLine *tmp; + char str[SUGG_LIST_WIDTH+1]; + int w, boxw=0, boxh, i, l, x, y, b; - if (!is_suggest_active(st->text)) return; + if (!st || !st->text) return; + if (!suggest_is_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; + first = suggest_first(); + last = suggest_last(); + sel = suggest_get_selected(); + //if (!first || !last || !sel) return; + + for (tmp=st->text->curl, l=-st->top; tmp; tmp=tmp->prev, l++); + boxw = SUGG_LIST_WIDTH*spacetext_get_fontwidth(st) + 20; + boxh = SUGG_LIST_SIZE*st->lheight + 8; + x = spacetext_get_fontwidth(st)*st->text->curc + 50; // TODO: Replace + 50 + y = curarea->winy - st->lheight*l - 2; + + BIF_ThemeColor(TH_SHADE1); + glRecti(x-1, y+1, x+boxw+1, y-boxh-1); + BIF_ThemeColor(TH_BACK); + glRecti(x, y, x+boxw, y-boxh); + + for (i=0, item=sel; i<3 && item && item!=first; i++, item=item->prev); + + for (i=0; inext) { + + y -= st->lheight; + + strncpy(str, item->name, SUGG_LIST_WIDTH); + str[SUGG_LIST_WIDTH] = '\0'; + + w = BMF_GetStringWidth(spacetext_get_font(st), str); + + if (item == sel) { + BIF_ThemeColor(TH_SHADE2); + glRecti(x+16, y-3, x+16+w, y+st->lheight-3); + } + b=1; /* b=1 colour block, text is default. b=0 no block, colour text */ + switch (item->type) { + case 'k': BIF_ThemeColor(TH_SYNTAX_B); b=0; break; + case 'm': BIF_ThemeColor(TH_TEXT); break; + case 'f': BIF_ThemeColor(TH_SYNTAX_L); break; + case 'v': BIF_ThemeColor(TH_SYNTAX_N); break; + } + if (b) { + glRecti(x+8, y+2, x+11, y+5); + BIF_ThemeColor(TH_TEXT); + } + text_draw(st, str, 0, 0, 1, x+16, y-1, NULL); + + if (item == last) break; } } @@ -1484,6 +1535,48 @@ static void set_tabs(Text *text) st->currtab_set = setcurr_tab(text); } +static void get_suggest_prefix(Text *text) { + int i, len; + char *line, tmp[256]; + + if (!text) return; + if (!suggest_is_active(text)) return; + + line= text->curl->line; + for (i=text->curc-1; i>=0; i--) + if (!check_identifier(line[i])) + break; + i++; + len= text->curc-i; + if (len > 255) { + printf("Suggestion prefix too long\n"); + return; + } + strncpy(tmp, line+i, len); + tmp[len]= '\0'; + suggest_prefix(tmp); +} + +static void confirm_suggestion(Text *text) { + int i, len; + char *line; + SuggItem *sel; + + if (!text) return; + if (!suggest_is_active(text)) return; + + sel = suggest_get_selected(); + if (!sel) return; + + line= text->curl->line; + for (i=text->curc-1; i>=0; i--) + if (!check_identifier(line[i])) + break; + i++; + len= text->curc-i; + txt_insert_buf(text, sel->name+len); +} + void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) { unsigned short event= evt->event; @@ -1492,6 +1585,7 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) SpaceText *st= curarea->spacedata.first; Text *text; int do_draw=0, p; + int suggesting=0, do_suggest=0; /* 0:just redraw, -1:clear, 1:update prefix */ if (st==NULL || st->spacetype != SPACE_TEXT) return; @@ -1554,6 +1648,8 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) } return; } + + suggesting = suggest_is_active(text); if (event==LEFTMOUSE) { if (val) { @@ -1573,6 +1669,7 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) } do_draw= 1; } + do_suggest= -1; } } else if (event==MIDDLEMOUSE) { if (val) { @@ -1586,6 +1683,7 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) { do_textscroll(st, 1); } + do_suggest= -1; } } else if (event==RIGHTMOUSE) { if (val) { @@ -1618,6 +1716,7 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) default: break; } + do_suggest= -1; } } else if (ascii) { if (text && text->id.lib) { @@ -1627,8 +1726,10 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) if (st->showsyntax) get_format_string(st); pop_space_text(st); do_draw= 1; + do_suggest= 1; } } else if (val) { + do_suggest= -1; switch (event) { case AKEY: if (G.qual & LR_ALTKEY) { @@ -1891,6 +1992,9 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) do_draw= 1; } break; + case ESCKEY: + do_suggest= -1; + break; case TABKEY: if (text && text->id.lib) { error_libdata(); @@ -1920,6 +2024,11 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) error_libdata(); break; } + if (suggesting) { + confirm_suggestion(text); + if (st->showsyntax) get_format_string(st); + break; + } //double check tabs before splitting the line st->currtab_set = setcurr_tab(text); txt_split_curline(text); @@ -1951,6 +2060,7 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) if (st->showsyntax) get_format_string(st); do_draw= 1; pop_space_text(st); + do_suggest= 1; break; case DELKEY: if (text && text->id.lib) { @@ -1970,8 +2080,16 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) case INSERTKEY: st->overwrite= !st->overwrite; do_draw= 1; + do_suggest= 0; break; case DOWNARROWKEY: + if (suggesting) { + SuggItem *sel = suggest_get_selected(); + if (sel && sel!=suggest_last() && sel->next) + suggest_set_selected(sel->next); + do_suggest= 0; + break; + } txt_move_down(text, G.qual & LR_SHIFTKEY); set_tabs(text); do_draw= 1; @@ -2000,17 +2118,42 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) pop_space_text(st); break; case UPARROWKEY: + if (suggesting) { + SuggItem *sel = suggest_get_selected(); + if (sel && sel!=suggest_first() && sel->prev) + suggest_set_selected(sel->prev); + do_suggest= 0; + break; + } txt_move_up(text, G.qual & LR_SHIFTKEY); set_tabs(text); do_draw= 1; pop_space_text(st); break; case PAGEDOWNKEY: - screen_skip(st, st->viewlines); + if (suggesting) { + int i; + SuggItem *sel = suggest_get_selected(); + for (i=0; inext; i++, sel=sel->next) + suggest_set_selected(sel->next); + do_suggest= 0; + break; + } else { + screen_skip(st, st->viewlines); + } do_draw= 1; break; case PAGEUPKEY: - screen_skip(st, -st->viewlines); + if (suggesting) { + int i; + SuggItem *sel = suggest_get_selected(); + for (i=0; iprev; i++, sel=sel->prev) + suggest_set_selected(sel->prev); + do_suggest= 0; + break; + } else { + screen_skip(st, -st->viewlines); + } do_draw= 1; break; case HOMEKEY: @@ -2024,14 +2167,39 @@ void winqreadtextspace(ScrArea *sa, void *spacedata, BWinEvent *evt) pop_space_text(st); break; case WHEELUPMOUSE: - screen_skip(st, -U.wheellinescroll); + if (suggesting) { + SuggItem *sel = suggest_get_selected(); + if (sel && sel!=suggest_first() && sel->prev) + suggest_set_selected(sel->prev); + do_suggest= 0; + } else { + screen_skip(st, -U.wheellinescroll); + } do_draw= 1; break; case WHEELDOWNMOUSE: - screen_skip(st, U.wheellinescroll); + if (suggesting) { + SuggItem *sel = suggest_get_selected(); + if (sel && sel!=suggest_last() && sel->next) + suggest_set_selected(sel->next); + do_suggest= 0; + } else { + screen_skip(st, U.wheellinescroll); + } do_draw= 1; break; + default: + do_suggest= 0; + } + } + + if (suggesting) { + if (do_suggest == -1) { + suggest_clear_text(); + } else if (do_suggest == 1) { + get_suggest_prefix(text); } + do_draw= 1; } if (do_draw) { @@ -2223,6 +2391,16 @@ static int check_numbers(char *string) return 0; } +static int check_identifier(char ch) { + if (ch < '0') return 0; + if (ch <= '9') return 1; + if (ch < 'A') return 0; + if (ch <= 'Z' || ch == '_') return 1; + if (ch < 'a') return 0; + if (ch <= 'z') return 1; + return 0; +} + void convert_tabs (struct SpaceText *st, int tab) { Text *text = st->text; -- cgit v1.2.3