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;