diff --git a/pd/nw/dialog_search.html b/pd/nw/dialog_search.html index 6727ce498cfce3e7b2d64eaf6b3a77e6833bcd35..5861f992be87cc19bfaefe042d55eeb2e5ccf89c 100644 --- a/pd/nw/dialog_search.html +++ b/pd/nw/dialog_search.html @@ -135,6 +135,154 @@ var toc = [ }, ]; +// ag: Some functions to add bookmarks to the toc, and save them in the user's +// personal config file. + +function toc_add_bookmarks() +{ + toc[toc.length] = { + title: "Bookmarks", + }; +} + +function toc_bookmarks() +{ + for (var i = 0, len = toc.length; i < len; i++) { + if (!toc[i].id && !toc[i].description && + toc[i].title == "Bookmarks") { + return i; + } + } + return -1; +} + +function expand_tilde(filepath) { + if (filepath[0] === '~') { + var home = pdgui.check_os("win32") ? process.env.HOMEPATH : + process.env.HOME; + return path.join(home, filepath.slice(1)); + } + return filepath; +} + +const toc_config = expand_tilde( + pdgui.check_os("win32") + ? "~/AppData/Roaming/Purr-Data/bookmarks.json" + : "~/.purr-data/bookmarks.json" +); + +function toc_save() +{ + var i = toc_bookmarks(); + if (i >= 0) { + // the actual bookmarks start at index i+1, i is the section title + var data = JSON.stringify(toc.slice(i+1), null, 2); + fs.writeFileSync(toc_config, data); + } else { + // no bookmarks, get rid of the bookmarks file if present + try { + fs.unlinkSync(toc_config); + } catch (err) { + // ignore + } + } +} + +// this should be executed just once, when the browser is first shown +function toc_load() +{ + function toc_valid(doc) { + // validate the bookmark entries + if (doc.id && doc.title) { + try { + fs.accessSync(check_dir(doc.id), fs.F_OK); + return true; + } catch (e) { + return false; + } + } else { + return false; + } + }; + var bookmarks; + try { + bookmarks = fs.readFileSync(toc_config); + try { + bookmarks = JSON.parse(bookmarks); + } catch (err) { + // might be a syntax error, if the user edited the file manually, + // so lets provide some (hopefully useful) diagnostic in the + // console + pdgui.post("error reading bookmarks: "+toc_config, "error"); + pdgui.post(err); + return; + } + } catch (err) { + // no bookmarks, just bail out + return; + } + try { + // this might still fail if the JSON we read is some (syntactically + // correct) random garbage, in this case give some diagnostic below + bookmarks = bookmarks.filter(toc_valid); + if (bookmarks && bookmarks.length > 0) { + toc_add_bookmarks(); + toc = toc.concat(bookmarks); + // this message would be shown each time the browser is opened, + // which would create a lot of noise; commented, but we leave it + // in here since it might be useful for debugging purposes + //pdgui.post("loaded bookmarks: "+toc_config); + } + } catch (err) { + pdgui.post("error reading bookmarks: "+toc_config, "error"); + pdgui.post("format error (expected an array, got " + + (typeof bookmarks === "object" ? "an " : + typeof bookmarks === "undefined" ? "" : "a ") + + typeof bookmarks + ")"); + } +} + +function toc_add_bookmark(id, title, descr) +{ + if (toc_bookmarks() < 0) { + toc_add_bookmarks(); + } + toc[toc.length] = { + id: id, + title: title, + description: descr + }; + pdgui.post("add bookmark: "+title+" ("+id+")"); + toc_save(); +} + +function toc_delete_bookmark(id, title) +{ + var i = toc_bookmarks(); + if (i >= 0) { + var l = toc.length; + while (i < l && + (toc[i].id !== id || toc[i].title !== title)) { + i++; + } + if (i < l) { + toc.splice(i, 1); + pdgui.post("delete bookmark: "+title+" ("+id+")"); + } else { + // not found, we're done + return; + } + } else { + // no bookmarks, we're done + return; + } + if (toc_bookmarks() == toc.length-1) { + // empty bookmark section, remove the section title + toc.pop(); + } + toc_save(); +} + // Stop-gap translator function translate_form() { var elements = document.querySelectorAll("[data-i18n]"), @@ -157,6 +305,22 @@ function click_toc(dir) { var current_dir; +function check_dir(dirname) +{ + var absname = dirname; + if (!path.isAbsolute(dirname)) { + // A relative path is taken relative to libdir, so that, e.g., doc/* + // and extra/* can be used to access documentation in the doc and + // extra hierarchies. + absname = path.join(pdgui.get_lib_dir(), dirname); + } + try { + if (fs.lstatSync(absname).isDirectory()) return absname; + } catch (err) { + } + return null; +} + function display_toc() { var results_elem = document.getElementById("results"), div, @@ -169,7 +333,7 @@ function display_toc() { div = document.createElement("div"); if (doc.id) { try { - fs.accessSync(path.join(pdgui.get_lib_dir(), doc.id), fs.F_OK); + fs.accessSync(check_dir(doc.id), fs.F_OK); a = document.createElement("a"); a.href = "javascript: click_toc('" + doc.id + "');"; a.textContent = doc.title; @@ -202,6 +366,7 @@ function finish_build(idx) { index = idx; document.getElementById("search_text").disabled = false; clear_results(); + toc_load(); display_toc(); } @@ -389,6 +554,12 @@ function window_close_shortcut(evt) { return (evt.keyCode === 87 && evt[modifier]) // <ctrl-w> } +function bookmark_shortcut(evt) { + var osx = process.platform === "darwin", + modifier = osx ? "metaKey" : "ctrlKey"; + return (evt.keyCode === 68 && evt[modifier]) // <ctrl-d> adds, <ctrl-shift-d> deletes a bookmark +} + function toggle_find_bar() { // this is copied from index.js m.edit.find... var find_div = document.getElementById("console_find"), @@ -412,6 +583,46 @@ function toggle_find_bar() { } } +// Adds (or deletes, if del is true) a bookmark to the toc. +function do_bookmark(dirname, del) +{ + /* id (dirname) for the bookmark. We take this relative to the libdir if + the relative designation is shorter than the absolute path, which gives + prettier ids for documents in the doc and extra hierarchies and + siblings. Note that these ids only ever appear in the toc, never in + documents in the search database. */ + var relname = path.relative(pdgui.get_lib_dir(), dirname); + var id = dirname.length <= relname.length ? + dirname : relname; + // Default name for the bookmark. + var name = path.basename(dirname); + // Let's check whether the directory contains a meta file from which we + // may get name and description of the external. + var meta = path.join(dirname, name+"-meta.pd"); + var meta_name, meta_descr; + try { + var data = fs.readFileSync(meta, 'utf8').replace("\n", " "); + meta_name = data.match + (/#X text \-?[0-9]+ \-?[0-9]+ NAME ([\s\S]*?);/i); + meta_descr = data.match + (/#X text \-?[0-9]+ \-?[0-9]+ DESCRIPTION ([\s\S]*?);/i); + meta_name = meta_name && meta_name.length > 1 ? + meta_name[1].trim() : null; + meta_descr = meta_descr && meta_descr.length > 1 ? + meta_descr[1].trim() : null; + // Remove the Pd escapes for commas + meta_descr = meta_descr ? + meta_descr.replace(" \\,", ",") : null; + } catch (err) { + // ignore + } + name = meta_name ? meta_name : name; + if (del) + toc_delete_bookmark(id, name); + else + toc_add_bookmark(id, name, meta_descr); +} + function add_events() { // closing the Window nw.Window.get().on("close", function() { @@ -460,6 +671,13 @@ function add_events() { evt.keyCode === 10 || evt.keyCode === 13) { } else if (evt.target !== input_elem) { input_elem.focus(); + } else if (bookmark_shortcut(evt)) { + // We assume here that current_dir is set and points to the + // directory to be bookmarked. + if (current_dir) { + evt.stopPropagation(); + do_bookmark(current_dir, evt.shiftKey); + } } else if (window_close_shortcut(evt)) { evt.stopPropagation(); pdgui.remove_dialogwin("search"); @@ -532,12 +750,11 @@ function doc_search() { display_toc(); return; } - // if the search term is doc/* or extra/* then short circuit + // if the search term is a directory then short circuit // the search and just list the docs in that directory - if ((search_text.slice(0, 4) === "doc/" || - search_text.slice(0, 6) === "extra/") && - search_text.indexOf(" ") === -1) { - display_directory(path.join(pdgui.get_lib_dir(), search_text)); + var dirname = check_dir(search_text); + if (dirname) { + display_directory(dirname); return; } clear_results();