diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json index 54e76840d26c4738532e93f7cb2c2fab61ef71da..d096882ebba4e682597d5c41d11586c28ccab4f2 100644 --- a/pd/nw/locales/de/translation.json +++ b/pd/nw/locales/de/translation.json @@ -99,6 +99,10 @@ "new_tt": "Erzeuge einen leeren Pd-Patch", "open": "Öffnen", "open_tt": "Öffne eine oder mehrere Pd-Dateien", + "recent_files": "Zuletzt geöffnete Dateien", + "recent_files_tt": "Öffne eine der zuletzt geöffneten Pd-Dateien", + "clear_recent_files": "Liste leeren", + "clear_recent_files_tt": "Leere die Liste der zuletzt geöffneten Pd-Dateien", "k12_demos": "K12-Demos", "k12_demos_tt": "Demo-Patches zur Verwendung im K12-Modus", "save": "Speichern", diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json index 60bd7ae1ac7819bdec2714ec277d5a506c2d8560..4202e39b4eb02b4617972d3751a04444d07e8c55 100644 --- a/pd/nw/locales/en/translation.json +++ b/pd/nw/locales/en/translation.json @@ -99,6 +99,10 @@ "new_tt": "Create an empty Pd patch", "open": "Open", "open_tt": "Open one or more Pd files", + "recent_files": "Recent Files", + "recent_files_tt": "Open a recently opened Pd file", + "clear_recent_files": "Clear List", + "clear_recent_files_tt": "Clear the recent files list", "k12_demos": "K12 Demos", "k12_demos_tt": "Demo patches for use with K12 Mode", "save": "Save", diff --git a/pd/nw/pd_menus.js b/pd/nw/pd_menus.js index 74700af1a49491884fe9465be52a9e2a8554e1df..829a684a423bc6b402cfc5f0599af6611cd307a8 100644 --- a/pd/nw/pd_menus.js +++ b/pd/nw/pd_menus.js @@ -3,6 +3,7 @@ var pdgui = require("./pdgui.js"); var l = pdgui.get_local_string; // For menu names var osx_menu = null; // OSX App menu -- a single one per running instance +var recent_files_submenu = null; function create_menu(gui, type) { // On OSX we create a menu only once, and then enable/disable menuitems @@ -26,6 +27,16 @@ function create_menu(gui, type) { media_menu, help_menu; + // We only maintain a single instance of the recent files submenu which + // gets updated in pdgui.js via a callback from the engine. + if (!recent_files_submenu) { + recent_files_submenu = new gui.Menu(); + // NOTE: Since we can't be sure whether the GUI or the engine runs + // first, make sure that we populate the submenu on the first run in + // either case. + pdgui.populate_recent_files(recent_files_submenu); + } + // OSX just spawns a single canvas menu and then enables/disables // the various menu items as needed. canvas_menu = osx || (type !== "console"); @@ -63,6 +74,11 @@ function create_menu(gui, type) { modifiers: cmd_or_ctrl, tooltip: l("menu.open_tt") })); + file_menu.append(m.file.recent_files = new gui.MenuItem({ + label: l("menu.recent_files"), + submenu: recent_files_submenu, + tooltip: l("menu.recent_files_tt") + })); if (pdgui.k12_mode == 1) { file_menu.append(m.file.k12 = new gui.MenuItem({ label: l("menu.k12_demos"), diff --git a/pd/nw/pdgui.js b/pd/nw/pdgui.js index 59552e3989fd42e2f7db443e46d0c50e16604c00..e3231e5039df36d3c20a7a6558fedfe7c955e277 100644 --- a/pd/nw/pdgui.js +++ b/pd/nw/pdgui.js @@ -741,6 +741,11 @@ function saveas_callback(cid, file, close_flag) { } pdsend(cid, "savetofile", enquote(basename), enquote(directory), close_flag); + // XXXREVIEW: It seems sensible that we also switch the opendir here. -ag + set_pd_opendir(directory); + // update the recent files list + var norm_path = path.normalize(directory); + pdsend("pd add-recent-file", enquote(path.join(norm_path, basename))); } exports.saveas_callback = saveas_callback; @@ -976,6 +981,9 @@ function open_file(file) { (enquote(directory))); set_pd_opendir(directory); //::pd_guiprefs::update_recentfiles "$filename" 1 + // update the recent files list + var norm_path = path.normalize(directory); + pdsend("pd add-recent-file", enquote(path.join(norm_path, basename))); } } @@ -4446,6 +4454,58 @@ function open_search() { exports.open_search= open_search; +// This is the same for all windows (initialization is in pd_menus.js). +var recent_files_submenu = null; +var recent_files = null; + +// We need to jump through some hoops here since JS closures capture variables +// by reference, which causes trouble when closures are created within a +// loop. +function recent_files_callback(i) { + return function() { + var fname = recent_files[i]; + //post("clicked recent file: "+fname); + open_file(fname); + } +} + +function populate_recent_files(submenu) { + if (submenu) recent_files_submenu = submenu; + if (recent_files && recent_files_submenu) { + //post("recent files: " + recent_files.join(" ")); + while (recent_files_submenu.items.length > 0) + recent_files_submenu.removeAt(0); + for (var i = 0; i < recent_files.length; i++) { + var item = new nw.MenuItem({ + label: path.basename(recent_files[i]), + tooltip: recent_files[i] + }); + item.click = recent_files_callback(i); + recent_files_submenu.append(item); + } + if (recent_files_submenu.items.length > 0) { + recent_files_submenu.append(new nw.MenuItem({ + type: "separator" + })); + var item = new nw.MenuItem({ + label: lang.get_local_string("menu.clear_recent_files"), + tooltip: lang.get_local_string("menu.clear_recent_files_tt") + }); + item.click = function() { + pdsend("pd clear-recent-files"); + }; + recent_files_submenu.append(item); + } + } +} + +exports.populate_recent_files = populate_recent_files; + +function gui_recent_files(dummy, recent_files_array) { + recent_files = recent_files_array; + populate_recent_files(recent_files_submenu); +} + function gui_audio_properties(gfxstub, sys_indevs, sys_outdevs, pd_indevs, pd_inchans, pd_outdevs, pd_outchans, audio_attrs) { var attrs = audio_attrs.concat([ @@ -4758,7 +4818,7 @@ function canvas_params(cid) x |= 0; y |= 0; return { svg: svg, x: x, y: y, w: width, h: height, - mw: min_width, mh: min_height }; + mw: min_width, mh: min_height }; } function do_getscroll(cid) { @@ -4771,7 +4831,7 @@ function do_getscroll(cid) { // errors wrt the rendering context disappearing. if (!patchwin[cid]) { return; } var { svg: svg, x: x, y: y, w: width, h: height, - mw: min_width, mh: min_height } = canvas_params(cid); + mw: min_width, mh: min_height } = canvas_params(cid); if (width < min_width) { width = min_width; } @@ -4816,7 +4876,7 @@ function do_optimalzoom(cid, hflag, vflag) { // the window if (!patchwin[cid]) { return; } var { x: x, y: y, w: width, h: height, mw: min_width, mh: min_height } = - canvas_params(cid); + canvas_params(cid); // Calculate the optimal horizontal and vertical zoom values, // using floor to always round down to the nearest integer. Note // that these may well be negative, if the viewport is too small @@ -4832,16 +4892,16 @@ function do_optimalzoom(cid, hflag, vflag) { // to the valid zoom level range of -8..+7. var actz = patchwin[cid].zoomLevel, z = 0; if (hflag && vflag) - z = Math.min(zx, zy); + z = Math.min(zx, zy); else if (hflag) - z = zx; + z = zx; else if (vflag) - z = zy; + z = zy; z += actz; if (z < -8) z = -8; if (z > 7) z = 7; //post("bbox: "+width+"x"+height+"+"+x+"+"+y+" window size: "+min_width+"x"+min_height+" current zoom level: "+actz+" optimal zoom level: "+z); if (z != actz) { - patchwin[cid].zoomLevel = z; + patchwin[cid].zoomLevel = z; } } diff --git a/pd/src/m_glob.c b/pd/src/m_glob.c index 734084845ebbe3d067162229679a0693632655e7..3f5e448fb0d2623899b7e2028e5a5b96675e48bc 100644 --- a/pd/src/m_glob.c +++ b/pd/src/m_glob.c @@ -37,6 +37,9 @@ void glob_ping(t_pd *dummy); void glob_watchdog(t_pd *dummy); void glob_savepreferences(t_pd *dummy); void glob_forward_files_from_secondary_instance(void); +void glob_recent_files(t_pd *dummy); +void glob_add_recent_file(t_pd *dummy, t_symbol *s); +void glob_clear_recent_files(t_pd *dummy); void alsa_resync( void); @@ -168,6 +171,12 @@ void glob_init(void) gensym("gui-preset"), A_SYMBOL, 0); class_addmethod(glob_pdobject, (t_method)glob_gui_properties, gensym("gui-properties"), 0); + class_addmethod(glob_pdobject, (t_method)glob_recent_files, + gensym("recent-files"), 0); + class_addmethod(glob_pdobject, (t_method)glob_add_recent_file, + gensym("add-recent-file"), A_SYMBOL, 0); + class_addmethod(glob_pdobject, (t_method)glob_clear_recent_files, + gensym("clear-recent-files"), 0); #ifdef UNIX class_addmethod(glob_pdobject, (t_method)glob_watchdog, gensym("watchdog"), 0); diff --git a/pd/src/s_file.c b/pd/src/s_file.c index 1af0d737956bffbc3f8a4e284f73e95f4a2f0b11..32f54c312d8d21cadd790b2ecd1abb3926181c3c 100644 --- a/pd/src/s_file.c +++ b/pd/src/s_file.c @@ -46,8 +46,9 @@ void sys_doflags( void); #ifdef UNIX +#define USER_CONFIG_DIR ".pd-l2ork" + static char *sys_prefbuf; -static int sys_prefbufsize; static void sys_initloadpreferences( void) { @@ -62,7 +63,7 @@ static void sys_initloadpreferences( void) sys_libdir->s_name); if (homedir) - snprintf(user_prefs_file, FILENAME_MAX, "%s/.pd-l2ork/user.settings", homedir); + snprintf(user_prefs_file, FILENAME_MAX, "%s/" USER_CONFIG_DIR "/user.settings", homedir); if (stat(user_prefs_file, &statbuf) == 0) strncpy(filenamebuf, user_prefs_file, FILENAME_MAX); else if (stat(default_prefs_file, &statbuf) == 0) @@ -143,16 +144,16 @@ static void sys_initsavepreferences( void) if (!homedir) return; - snprintf(filenamebuf, FILENAME_MAX, "%s/.pd-l2ork", homedir); + snprintf(filenamebuf, FILENAME_MAX, "%s/" USER_CONFIG_DIR, homedir); filenamebuf[FILENAME_MAX-1] = 0; if (stat(filenamebuf, &statbuf) || !S_ISDIR(statbuf.st_mode)) { // user config dir doesn't exist yet, try to create it if (mkdir(filenamebuf, 0755)) { pd_error(0, "%s: %s",filenamebuf, strerror(errno)); - return; + return; } } - snprintf(filenamebuf, FILENAME_MAX, "%s/.pd-l2ork/user.settings", homedir); + snprintf(filenamebuf, FILENAME_MAX, "%s/" USER_CONFIG_DIR "/user.settings", homedir); filenamebuf[FILENAME_MAX-1] = 0; if ((sys_prefsavefp = fopen(filenamebuf, "w")) == NULL) { @@ -337,7 +338,7 @@ void sys_loadpreferences( void) int nmidiindev, midiindev[MAXMIDIINDEV]; int nmidioutdev, midioutdev[MAXMIDIOUTDEV]; int i, rate = 0, advance = -1, callback = 0, blocksize = 0, - api, nolib, maxi; + api, maxi; char prefbuf[MAXPDSTRING], keybuf[80]; sys_initloadpreferences(); @@ -505,6 +506,7 @@ void sys_loadpreferences( void) if (strcmp(prefbuf, ".")) sys_flags = gensym(prefbuf); } + sys_doneloadpreferences(); sys_doflags(); if (sys_defeatrt) @@ -636,3 +638,165 @@ void glob_savepreferences(t_pd *dummy) sys_donesavepreferences(); } + +/* AG: Recent files table */ + +int sys_n_recent_files = 0; +char *sys_recent_files[MAX_RECENT_FILES]; + +static int fexists(const char *s) +{ + struct stat statbuf; + return stat(s, &statbuf) == 0; +} + +void sys_add_recent_file(const char *s) +{ + int i; + // only add the file if it actually exists + if (!fexists(s)) return; + for (i = 0; i < sys_n_recent_files && strcmp(sys_recent_files[i], s); i++) ; + if (i < sys_n_recent_files) { + // already got an existing entry, move it to the front + char *t = sys_recent_files[i]; + memmove(sys_recent_files+1, sys_recent_files, i*sizeof(char*)); + sys_recent_files[0] = t; + } else { + char *t = strdup(s); + if (!t) return; + if (sys_n_recent_files == MAX_RECENT_FILES) { + // kick out the oldest entry to make room for a new one + free(sys_recent_files[--sys_n_recent_files]); + } + // add a new entry at the beginning of the table + memmove(sys_recent_files+1, sys_recent_files, + sys_n_recent_files*sizeof(char*)); + sys_recent_files[0] = t; + sys_n_recent_files++; + } +} + +void sys_save_recent_files(void) +{ + int i; +#ifdef UNIX + // UNIX/Linux: save in recent_files file + FILE *fp; + char filenamebuf[FILENAME_MAX], *homedir = getenv("HOME"); + struct stat statbuf; + if (!homedir) return; + snprintf(filenamebuf, FILENAME_MAX, "%s/" USER_CONFIG_DIR, homedir); + filenamebuf[FILENAME_MAX-1] = 0; + if (stat(filenamebuf, &statbuf) || !S_ISDIR(statbuf.st_mode)) { + // user config dir doesn't exist yet, try to create it + if (mkdir(filenamebuf, 0755)) { + pd_error(0, "%s: %s",filenamebuf, strerror(errno)); + return; + } + } + snprintf(filenamebuf, FILENAME_MAX, "%s/" USER_CONFIG_DIR "/recent_files", homedir); + filenamebuf[FILENAME_MAX-1] = 0; + if ((fp = fopen(filenamebuf, "w")) == NULL) { + pd_error(0, "%s: %s",filenamebuf, strerror(errno)); + return; + } + for (i = 0; i < sys_n_recent_files; i++) { + fprintf(fp, "%s\n", sys_recent_files[i]); + } + fclose(fp); +#else + // Mac/Windows (use the defaults/registry) + char buf[MAXPDSTRING]; + sys_initsavepreferences(); + for (i = 0; i < sys_n_recent_files; i++) { + sprintf(buf, "recent%d", i+1); + sys_putpreference(buf, sys_recent_files[i]); + } + sprintf(buf, "%d", i); + sys_putpreference("nrecent", buf); + sys_donesavepreferences(); +#endif +} + +void sys_load_recent_files(void) +{ +#ifdef UNIX + // UNIX/Linux: load from recent_files file + FILE *fp; + char filenamebuf[FILENAME_MAX], *homedir = getenv("HOME"); + if (!homedir) return; + snprintf(filenamebuf, FILENAME_MAX, "%s/" USER_CONFIG_DIR "/recent_files", homedir); + filenamebuf[FILENAME_MAX-1] = 0; + if ((fp = fopen(filenamebuf, "r")) == NULL) return; + for (sys_n_recent_files = 0; sys_n_recent_files < MAX_RECENT_FILES && + fgets(filenamebuf, FILENAME_MAX, fp); ) { + char *s; + int l = strlen(filenamebuf); + if (l > 0 && filenamebuf[l-1] == '\n') filenamebuf[--l] = 0; + // only add files which actually exist + if (l == 0 || !fexists(filenamebuf)) continue; + s = strdup(filenamebuf); + if (s) sys_recent_files[sys_n_recent_files++] = s; + } + fclose(fp); +#else + // Mac/Windows (use the defaults/registry) + char prefbuf[MAXPDSTRING], keybuf[80]; + int i, maxi = MAX_RECENT_FILES; + sys_initloadpreferences(); + if (sys_getpreference("nrecent", prefbuf, MAXPDSTRING)) + sscanf(prefbuf, "%d", &maxi); + for (i = 0; i < maxi; i++) { + int l; + char *s; + sprintf(keybuf, "recent%d", i+1); + if (!sys_getpreference(keybuf, prefbuf, MAXPDSTRING)) + break; + l = strlen(prefbuf); + if (l == 0 || !fexists(prefbuf)) continue; + s = strdup(prefbuf); + if (s) sys_recent_files[sys_n_recent_files++] = s; + } + sys_doneloadpreferences(); +#endif +} + +void sys_clear_recent_files(void) +{ + int i; + for (i = 0; i < sys_n_recent_files; i++) { + free(sys_recent_files[i]); + } + sys_n_recent_files = 0; +} + +// send the recent files list back to the gui so that the Recent Files menu +// can be updated accordingly +void glob_recent_files(t_pd *dummy) +{ + int i; + gui_start_vmess("gui_recent_files", "x", dummy); + gui_start_array(); + for (i = 0; i < sys_n_recent_files; i++) + { + gui_s(sys_recent_files[i]); + } + gui_end_array(); + gui_end_vmess(); +} + +// add an entry to the recent files list, save the list and update the gui +void glob_add_recent_file(t_pd *dummy, t_symbol *s) +{ + sys_add_recent_file(s->s_name); + sys_save_recent_files(); + glob_recent_files(dummy); +} + +// clear the recent files list, save the list and update the gui +void glob_clear_recent_files(t_pd *dummy) +{ + sys_clear_recent_files(); + sys_save_recent_files(); + glob_recent_files(dummy); +} diff --git a/pd/src/s_main.c b/pd/src/s_main.c index bacdd8d2f88033a78f977fcb3ffe30f9cd04afa4..fb2114595a8fcf3dd600abc5a5ca97de7db65323 100644 --- a/pd/src/s_main.c +++ b/pd/src/s_main.c @@ -301,6 +301,7 @@ int sys_main(int argc, char **argv) noprefs = 1; if (!noprefs) sys_loadpreferences(); /* load default settings */ + sys_load_recent_files(); /* load recent files table */ #ifndef MSW if (!noprefs) sys_rcfile(); /* parse the startup file */ @@ -320,6 +321,8 @@ int sys_main(int argc, char **argv) gui_vmess("gui_set_lib_dir", "s", sys_libdir->s_name); /* send the name of the gui preset */ gui_vmess("gui_set_gui_preset", "s", sys_gui_preset->s_name); + /* send the recent files list */ + glob_recent_files(0); if (sys_externalschedlib) return (sys_run_scheduler(sys_externalschedlibname, diff --git a/pd/src/s_stuff.h b/pd/src/s_stuff.h index 0547f5726b2ec2d8b62d218afafb8c6413d26206..107b3b809d5838f7cea38859de86365f86bc77a9 100644 --- a/pd/src/s_stuff.h +++ b/pd/src/s_stuff.h @@ -42,6 +42,14 @@ extern int sys_defeatrt; extern t_symbol *sys_gui_preset; extern t_symbol *sys_flags; +#define MAX_RECENT_FILES 8 +void sys_load_recent_files(void); +void sys_save_recent_files(void); +void sys_add_recent_file(const char *s); +void sys_clear_recent_files(void); +extern int sys_n_recent_files; +extern char *sys_recent_files[]; + /* s_main.c */ extern int sys_debuglevel; extern int sys_verbose;