diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json index 794b0874b58efed353ea8369d9ad1c18908dcddf..1427ac9040be5410da688473fc989e3673a1ea08 100644 --- a/pd/nw/locales/en/translation.json +++ b/pd/nw/locales/en/translation.json @@ -287,6 +287,16 @@ "cancel": "Cancel", "cancel_tt": "Don't save any changes, and don't close the patch" }, + "abstract_dialog": { + "prompt": "Would you like to turn the identical subpatches into abstractions?\nCandidates found in the subpatch tree, from root canvas: ", + "note": "Note: this can't be undone all at once – any changes made in subpatches can be undone from that subpatch", + "single": "Only this one", + "single_tt": "Replace the subpatch you just saved with the corresponding abstraction", + "all": "All candidates", + "all_tt": "Replace all subpatches in the subpatch tree identical to the one you just saved with the corresponding abstraction", + "none": "None", + "none_tt": "Do not replace any subpatch" + }, "find": { "placeholder": "Search in Canvas", "search": "Search", @@ -296,6 +306,7 @@ "menu": { "props": "Properties", "open": "Open", + "saveas": "Save as", "help": "Help", "front": "Bring to Front", "back": "Send to Back" diff --git a/pd/nw/pd_canvas.html b/pd/nw/pd_canvas.html index 27bdbd0ac1fe5cf2a5f90ed1b77f9995be23116e..cfd6665b715e65ee5b85d1e226082ad3ad2f4e46 100644 --- a/pd/nw/pd_canvas.html +++ b/pd/nw/pd_canvas.html @@ -77,6 +77,30 @@ <span data-i18n="canvas.save_dialog.cancel"></span> </button> </div> + </dialog> + <dialog id="abstract_dialog"> + <h4><span style="white-space: pre-line" + data-i18n="canvas.abstract_dialog.prompt"></span> + <span id="abstract_dialog_candidates"></span> + </h4> + <h5> <span data-i18n="canvas.abstract_dialog.note"></span> </h5> + <div class="abstract_submit_buttons"> + <button type="button" + id="abstract_single_button" + data-i18n="[title]canvas.abstract_dialog.single_tt"> + <span data-i18n="canvas.abstract_dialog.single"></span> + </button> + <button type="button" + id="abstract_all_button" + data-i18n="[title]canvas.abstract_dialog.all_tt"> + <span data-i18n="canvas.abstract_dialog.all"></span> + </button> + <button type="button" + id="abstract_none_button" + data-i18n="[title]canvas.abstract_dialog.none_tt"> + <span data-i18n="canvas.abstract_dialog.none"></span> + </button> + </div> </dialog> <div id="hscroll" style="background-color: rgba(0, 0, 0, 0.267); position: fixed; left: 0px; bottom: 0px; border-radius: 0px; width: 10px; height: 5px; visibility: hidden;"></div> <div id="vscroll" style="background-color: rgba(0, 0, 0, 0.267); position: fixed; right: 0px; top: 0px; border-radius: 0px; width: 5px; height: 10px; visibility: hidden;"></div> diff --git a/pd/nw/pd_canvas.js b/pd/nw/pd_canvas.js index c3374b9e1c97de61883565b30888002578d49ba2..1cf5fa04c4bbfe7e11cd6ccc2c93b7861ddf6136 100644 --- a/pd/nw/pd_canvas.js +++ b/pd/nw/pd_canvas.js @@ -1357,6 +1357,12 @@ function create_popup_menu(name) { pdgui.popup_action(name, 1); } })); + popup_menu.append(new gui.MenuItem({ + label: l("canvas.menu.saveas"), + click: function() { + pdgui.popup_action(name, 5); + } + })); popup_menu.append(new gui.MenuItem({ label: l("canvas.menu.help"), click: function() { diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js index 880525bab66ae4e0d19f03ab5eedc61dd9f8c786..6f4f9f38f5939da9898b618598172bccb98ef7be 100644 --- a/pd/nw/pdgui.js +++ b/pd/nw/pdgui.js @@ -1185,6 +1185,48 @@ function gui_canvas_menuclose(cid_for_dialog, cid, force) { }, 450); } +function canvas_abstract_callback(cid_for_dialog, cid, matches) { + var nw = patchwin[cid_for_dialog], + w = nw.window, + doc = w.document, + dialog = doc.getElementById("abstract_dialog"), + dialog_candidates = doc.getElementById("abstract_dialog_candidates"), + single_button = doc.getElementById("abstract_single_button"), + all_button = doc.getElementById("abstract_all_button"), + none_button = doc.getElementById("abstract_none_button"); + + dialog_candidates.textContent = matches.toString(); + dialog_candidates.title = matches.toString() + all_button.disabled = (matches === 1); + + single_button.onclick = function() { + dialog.close(); + w.canvas_events[w.canvas_events.get_previous_state()](); + pdsend(cid, "dialog", 0); + }; + all_button.onclick = function() { + dialog.close(); + w.canvas_events[w.canvas_events.get_previous_state()](); + pdsend(cid, "dialog", 1); + }; + none_button.onclick = function() { + dialog.close(); + w.canvas_events[w.canvas_events.get_previous_state()](); + } + + w.canvas_events.none(); + + w.setTimeout(function() { + dialog.showModal(); + }, 150); +} + +function gui_canvas_abstract(cid_for_dialog, cid, matches) { + setTimeout(function() { + canvas_abstract_callback(cid_for_dialog, cid, matches); + }, 450); +} + function gui_quit_dialog() { gui_raise_pd_window(); var reply = pd_window.window.confirm("Really quit?"); @@ -4885,7 +4927,7 @@ function zoom_kludge(zoom_level) { return zfactor; } -function gui_canvas_popup(cid, xpos, ypos, canprop, canopen, isobject) { +function gui_canvas_popup(cid, xpos, ypos, canprop, canopen, cansaveas, isobject) { // Get page coords for top of window, in case we're scrolled gui(cid).get_nw_window(function(nw_win) { // ico@vt.edu updated win_left and win_top for the 0.46.2 @@ -4908,6 +4950,7 @@ function gui_canvas_popup(cid, xpos, ypos, canprop, canopen, isobject) { //popup_coords[1] = ypos; popup_menu[cid].items[0].enabled = canprop; popup_menu[cid].items[1].enabled = canopen; + popup_menu[cid].items[2].enabled = cansaveas; // We'll use "isobject" to enable/disable "To Front" and "To Back" //isobject; diff --git a/pd/src/g_editor.c b/pd/src/g_editor.c index 021f964e5026e6fc3877f1f995b1390f4f1169fa..ecec351057873f5664ce0de70d38f7569c7e406e 100644 --- a/pd/src/g_editor.c +++ b/pd/src/g_editor.c @@ -2179,6 +2179,250 @@ void canvas_undo_font(t_canvas *x, void *z, int action) } } +/* ------------------------------- abstract feature --------------------- */ + +static t_class *abstracthandler_class; + +typedef struct abstracthandler +{ + t_object x_obj; + t_symbol *sym; /* symbol bound to the object */ + t_canvas *dialog; /* canvas where the dialog will be displayed */ + t_canvas *tarjet; /* tarjet subpatch */ + char *path; /* abstraction path */ + t_binbuf *subpatch; /* subpatch contents */ +} t_abstracthandler; + +static void *abstracthandler_new(void) +{ + t_abstracthandler *x = (t_abstracthandler *)pd_new(abstracthandler_class); + char namebuf[80]; + sprintf(namebuf, "ah%lx", (t_int)x); + x->sym = gensym(namebuf); + pd_bind(&x->x_obj.ob_pd, x->sym); + return (x); +} + +static void abstracthandler_free(t_abstracthandler *x) +{ + freebytes(x->path, MAXPDSTRING); + binbuf_free(x->subpatch); + pd_unbind(&x->x_obj.ob_pd, x->sym); +} + +/* gobj_activate, canvas_addtobuf and canvas_buftotex toghether and simplified*/ +static void do_rename_light(t_gobj *z, t_glist *glist, const char *text) +{ + t_rtext *y = glist_findrtext(glist, (t_text *)z); + glist->gl_editor->e_textedfor = y; + char *buf = getbytes(strlen(text)+1); + strcpy(buf, text); + rtext_settext(y, buf, strlen(text)+1); + glist->gl_editor->e_textdirty = 1; + canvas_dirty(glist, 1); + glist->gl_editor->e_onmotion = MA_NONE; //necessary? +} + +/* traverses the whole subtree of the given canvas/patch, replacing all subpatches identical to the + given one with an abstraction */ +static int do_replace_subpatches(t_canvas *x, const char* label, t_binbuf *original) +{ + t_selection *list = 0; + int edi = 0, num = 0; + /* editor is needed in order to do the operations below */ + if(label && !x->gl_editor) { canvas_create_editor(x); edi = 1; } + t_gobj *y, *yn; + canvas_undo_add(x, UNDO_SEQUENCE_START, "replace", "subpatch replacements have been redone. " + "All other possible replacements within other (sub)patches have not been affected"); + for(y = x->gl_list; y; y = yn) + { + yn = y->g_next; /* dirty hack, + 'canvas_stowconnections' inside 'glist_deselect' + rearranges the glist */ + if(pd_class(&y->g_pd) == canvas_class && + !canvas_isabstraction((t_canvas *)y)) + { + t_binbuf *tmp = binbuf_new(), *tmps = binbuf_new(); + gobj_save((t_gobj *)y, tmp); + int i = 0, j = binbuf_getnatom(tmp)-2; + t_atom *v = binbuf_getvec(tmp); + while(v[i].a_type != A_SEMI) i++; + i++; + while(v[j].a_type != A_SEMI) j--; + binbuf_restore(tmps, j-i+1, v+i); + binbuf_free(tmp); + if(binbuf_match(original, tmps, 0) && + binbuf_getnatom(original) == binbuf_getnatom(tmps)) + { + if(label) + { + binbuf_free(tmps); + glist_noselect(x); + glist_select(x, y); + do_rename_light(y, x, label); + glist_deselect(x, y); + } + else num++; + } + else + { + binbuf_free(tmps); + /* all the non-matching subpatches are stored in a list, */ + t_selection *new = (t_selection *)getbytes(sizeof(t_selection)); + new->sel_what = y; + new->sel_next = list; + list = new; + } + } + } + canvas_undo_add(x, UNDO_SEQUENCE_END, "replace", "subpatch replacements have been undone. " + "All other possible replacements within other (sub)patches have not been affected"); + if(edi) canvas_destroy_editor(x); + t_selection *z, *zn; + /* the function is called recursively on each non-matching canvas. + this has been left for last due to a visual bug that happens if + they are explored as soon as they are traversed*/ + for(z = list; z; z = zn) + { + zn = z->sel_next; + num += do_replace_subpatches((t_canvas *)z->sel_what, label, original); + freebytes(z, sizeof(t_selection)); + } + return num; +} + +static void abstracthandler_callback(t_abstracthandler *x, t_symbol *s) +{ + char fullpath[MAXPDSTRING], label[MAXPDSTRING], *dir, *filename, *o = s->s_name; + memset(fullpath, '\0', MAXPDSTRING); memset(label, '\0', MAXPDSTRING); + sys_unbashfilename(s->s_name, fullpath); + if(strlen(fullpath) < 3 || strcmp(fullpath+strlen(fullpath)-3, ".pd")) + strcat(fullpath, ".pd"); + filename = strrchr(fullpath, '/')+1; + fullpath[(int)filename-(int)fullpath-1] = '\0'; + dir = fullpath; + + int flag, prefix = 0; + if(flag = sys_relativizepath(canvas_getdir(canvas_getrootfor(x->tarjet))->s_name, dir, label)) + { + int len = strlen(label), creator, fd = -1; + if(len && label[len-1] != '/') label[len] = '/'; + strncat(label, filename, strlen(filename)-3); + /* check if there is a creator with the same name or if it's one of the built-in methods */ + t_symbol *sym = gensym(label); + creator = (sym == &s_bang || sym == &s_float || sym == &s_symbol || sym == &s_blob + || sym == &s_list || sym == &s_anything); + if(!len && creator) + { + prefix = (!len && creator); + creator = 0; + } + creator = (creator || zgetfn(&pd_objectmaker, sym)); + + /* check if there in an abstraction with the same name in the search path */ + if(!creator) + { + char opendir[MAXPDSTRING], *filenameptr; + fd = canvas_open(canvas_getrootfor(x->tarjet), label, ".pd", opendir, + &filenameptr, MAXPDSTRING, 0); //high load + if(fd > 0) + { + sys_close(fd); + /* check if we are overwriting the file */ + if(!strncmp(dir, opendir, (int)filenameptr-(int)opendir-1)) fd = -1; + } + } + flag = !(creator || (fd > 0)); + + if(flag && prefix) + { + strcpy(label, "./"); + strncat(label, filename, strlen(filename)-3); + } + else if(!flag) + error("warning: couldn't use relative path, there is a coincidence in the creator list or the search path"); + } + if(!flag) /* absolute path is required */ + { + memset(label, '\0', MAXPDSTRING); + strcpy(label, dir); + strcat(label, "/"); + strncat(label, filename, strlen(filename)-3); + /* should check if 'filename' is one of the built-in special methods + in order to inform the user about the nameclash problem */ + } + x->path = (char *)getbytes(MAXPDSTRING); + strcpy(x->path, label); + + /* save the subpatch into a separated pd file */ + t_atom at[3]; + SETSYMBOL(at, gensym(filename)); SETSYMBOL(at+1, gensym(dir)); SETFLOAT(at+2, 0.f); + x->tarjet->gl_env = 0xF1A6; /* gl_env is set to non-zero in order to save the subcanvas as a root canvas */ + typedmess(&x->tarjet->gl_pd, gensym("savetofile"), 3, at); + x->tarjet->gl_env = 0; + + t_binbuf *tmp = binbuf_new(), *tmps = binbuf_new(); + gobj_save((t_gobj *)x->tarjet, tmp); + /* only the internals are kept, the position on the parent canvas may differ */ + int i = 0, j = binbuf_getnatom(tmp)-2, matches; + t_atom *v = binbuf_getvec(tmp); + while(v[i].a_type != A_SEMI) i++; + i++; + while(v[j].a_type != A_SEMI) j--; + binbuf_restore(tmps, j-i+1, v+i); + binbuf_free(tmp); + matches = do_replace_subpatches(canvas_getrootfor(x->tarjet), 0, tmps); + x->subpatch = tmps; + + /* trigger the frontend dialog for replacing options */ + gui_vmess("gui_canvas_abstract", "xsi", + x->dialog, + x->sym->s_name, + matches); +} + +static void abstracthandler_dialog(t_abstracthandler *x, t_floatarg val) +{ + if(x->tarjet == x->dialog) canvas_vis(x->dialog, 0); + t_canvas *owner = x->tarjet->gl_owner, *root = canvas_getrootfor(x->tarjet); + int all = val; + if(!all) + { + /* change the text of the subpatch object to create the abstraction, + emulating the procedure done by the user. could be simplified */ + int edi = 0; + if(!owner->gl_editor) { canvas_create_editor(owner); edi = 1; } + glist_noselect(owner); + glist_select(owner, x->tarjet); + do_rename_light(x->tarjet, owner, x->path); + glist_deselect(owner, x->tarjet); + if(edi) canvas_destroy_editor(owner); + + /* select '[args]' slice + canvas_editmode(owner, 1); + t_gobj *abst = glist_nth(owner, glist_getindex(owner, 0)-1); + int len = strlen(x->path); + glist_select(owner, abst); + gobj_activate(abst, owner, (0b1 << 31) | (((len+1) & 0x7FFF) << 16) | ((len+7) & 0xFFFF)); */ + } + else + { + do_replace_subpatches(root, x->path, x->subpatch); + } + pd_free(&x->x_obj.ob_pd); +} + +void abstracthandler_setup(void) +{ + abstracthandler_class = class_new(gensym("abstracthandler"), 0, + abstracthandler_free, sizeof(t_abstracthandler), + CLASS_NOINLET, 0); + class_addmethod(abstracthandler_class, (t_method)abstracthandler_callback, + gensym("callback"), A_SYMBOL, 0); + class_addmethod(abstracthandler_class, (t_method)abstracthandler_dialog, + gensym("dialog"), A_FLOAT, 0); +} + /* ------------------------ event handling ------------------------ */ static char *cursorlist[] = { @@ -2271,7 +2515,7 @@ static void canvas_rightclick(t_canvas *x, int xpos, int ypos, t_gobj *y_sel) { //fprintf(stderr,"e_onmotion=%d\n",x->gl_editor->e_onmotion); if (x->gl_editor->e_onmotion != MA_NONE) return; - int canprop, canopen, isobject; + int canprop, canopen, isobject, cansaveas; t_gobj *y = NULL; int x1, y1, x2, y2, scalar_has_canvas = 0; if (x->gl_editor->e_selection) @@ -2328,12 +2572,19 @@ static void canvas_rightclick(t_canvas *x, int xpos, int ypos, t_gobj *y_sel) // LATER: consider enabling help and perhaps even limited properties... return; } - gui_vmess("gui_canvas_popup", "xiiiii", + /* saveas option, only if it's a canvas and it isn't an abstraction */ + cansaveas = (canopen && pd_class(&y->g_pd) == canvas_class && + !canvas_isabstraction((t_canvas *)y)); + /* or if it is the background of a subpatch */ + cansaveas = (cansaveas || (!y && canvas_getrootfor(x) != x && + !canvas_isabstraction((t_canvas *)x))); + gui_vmess("gui_canvas_popup", "xiiiiii", x, xpos, ypos, canprop, canopen, + cansaveas, isobject); } @@ -3078,6 +3329,22 @@ void canvas_done_popup(t_canvas *x, t_float which, t_float xpos, vmess(&y->g_pd, gensym("menu-open"), ""); return; } + else if(which == 5) /* saveas */ + { + t_abstracthandler *ah = abstracthandler_new(); + ah->tarjet = y; + ah->dialog = x; + + char buf[MAXPDSTRING]; + sprintf(buf, "%s/%s.pd", canvas_getdir(canvas_getrootfor(y))->s_name, ((t_canvas *)y)->gl_name->s_name); + + gui_vmess("gui_savepanel", "xss", + x, + ah->sym->s_name, + buf); + + return; + } else if (which == 2) /* help */ { char *dir; @@ -3146,6 +3413,21 @@ void canvas_done_popup(t_canvas *x, t_float which, t_float xpos, } else if (which == 2) open_via_helppath("intro.pd", canvas_getdir((t_canvas *)x)->s_name); + + if (which == 5) + { + t_abstracthandler *ah = abstracthandler_new(); + ah->tarjet = x; + ah->dialog = x; + + char buf[MAXPDSTRING]; + sprintf(buf, "%s/%s.pd", canvas_getdir(canvas_getrootfor(x))->s_name, ((t_canvas *)x)->gl_name->s_name); + + gui_vmess("gui_savepanel", "xss", + x, + ah->sym->s_name, + buf); + } } extern t_class *my_canvas_class; // for ignoring runtime clicks and resizing @@ -8082,6 +8364,8 @@ void g_editor_setup(void) gensym("disconnect"), A_FLOAT, A_FLOAT, A_FLOAT, A_FLOAT, A_NULL); /* -------------- copy buffer ------------------ */ copy_binbuf = binbuf_new(); + + abstracthandler_setup(); } void canvas_editor_for_class(t_class *c) diff --git a/pd/src/g_rtext.c b/pd/src/g_rtext.c index 87a2e463545a018a8dddfaa8b7bac39e9790a6b4..4fc08f1037f9d1f88d95c8ec153e85dcf7954de8 100644 --- a/pd/src/g_rtext.c +++ b/pd/src/g_rtext.c @@ -587,6 +587,10 @@ void rtext_activate(t_rtext *x, int state) glist->gl_editor->e_textedfor = 0; x->x_active = 0; } + + /* check if it has a window */ + if(!glist->gl_havewindow) return; + rtext_senditup(x, SEND_UPDATE, &w, &h, &indx); /* hack... state = 0 no editing diff --git a/pd/src/g_undo.c b/pd/src/g_undo.c index e314e1caed28bfc78d592ed3b8361ef404887282..2d963439b6d4da9890f598c65d63c1f4b18e7fe9 100644 --- a/pd/src/g_undo.c +++ b/pd/src/g_undo.c @@ -110,6 +110,7 @@ void canvas_undo_undo(t_canvas *x) if(UNDO_SEQUENCE_END == x->u_last->type) { int sequence_depth = 1; + if(x->u_last->data) post("undo info: %s", (char *)x->u_last->data); while((x->u_last = x->u_last->prev) && (UNDO_INIT != x->u_last->type)) { @@ -120,6 +121,7 @@ void canvas_undo_undo(t_canvas *x) break; case UNDO_SEQUENCE_END: sequence_depth++; + if(x->u_last->data) post("undo info: %s", (char *)x->u_last->data); break; default: canvas_undo_doit(x, x->u_last, UNDO_UNDO); @@ -176,6 +178,7 @@ void canvas_undo_redo(t_canvas *x) if(UNDO_SEQUENCE_START == x->u_last->type) { int sequence_depth = 1; + if(x->u_last->data) post("redo info: %s", (char *)x->u_last->data); while(x->u_last->next && (x->u_last = x->u_last->next)) { switch(x->u_last->type) @@ -185,6 +188,7 @@ void canvas_undo_redo(t_canvas *x) break; case UNDO_SEQUENCE_START: sequence_depth++; + if(x->u_last->data) post("redo info: %s", (char *)x->u_last->data); break; default: canvas_undo_doit(x, x->u_last, UNDO_REDO); diff --git a/pd/src/s_path.c b/pd/src/s_path.c index 9b2aebb8d9f95d5ddeee6bdf190f51759c25d73a..6c721c30c82de72fa2d1e0d74a3ccb30d621e6aa 100644 --- a/pd/src/s_path.c +++ b/pd/src/s_path.c @@ -195,6 +195,60 @@ int sys_isabsolutepath(const char *dir) } } +int sys_relativizepath(const char *from, const char *to, char *result) +{ + char fromext[FILENAME_MAX]; + sys_unbashfilename(from, fromext); + char toext[FILENAME_MAX]; + sys_unbashfilename(to, toext); + + int i = 0, j; + while(fromext[i] && toext[i] && fromext[i] == toext[i]) i++; + if(!i) return 0; + + j = i; + if(fromext[i]) + while(i > 0 && fromext[i] != '/') i--; + if(toext[j]) + while(j > 0 && toext[j] != '/') j--; + + if(fromext[i]) + { + int k = 0; + while(fromext[i]) + { + if(fromext[i] == '/') + { + if(k == 0) + { + strcpy(result+k, ".."); + k += 2; + } + else + { + strcpy(result+k, "/.."); + k += 3; + } + } + i++; + } + if(toext[j]) + { + result[k] = '/'; + strcpy(result+k+1, toext+j+1); + } + } + else if(!fromext[i] && toext[j]) + { + strcpy(result, toext+j+1); + } + else + { + strcpy(result, ""); + } + return 1; +} + /******************* Utility functions used below ******************/