diff --git a/pd/nw/bookmark.svg b/pd/nw/bookmark.svg new file mode 100644 index 0000000000000000000000000000000000000000..ddc28c52f08b5dfeaf68b9e85eb319a140c37700 --- /dev/null +++ b/pd/nw/bookmark.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 16 16"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="m4 2v12l4-1.594 4 1.594v-12h-7zm1 1h6v9.594l-3-1.188-3 1.188v-2.594z" + class="ColorScheme-Text" + /> +</svg> diff --git a/pd/nw/bookmark2.svg b/pd/nw/bookmark2.svg new file mode 100644 index 0000000000000000000000000000000000000000..4bfd3facacfb8941d06192b991d75c9514c7515f --- /dev/null +++ b/pd/nw/bookmark2.svg @@ -0,0 +1,22 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 16 16"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + .ColorScheme-NegativeText { + color:#da4453; + } + </style> + </defs> + <g transform="translate(-421.71-531.79)"> + <path + style="fill:currentColor;fill-opacity:1;stroke:none" + d="m425.71 533.79v12l4-1.594v-1l-3 1.188v-9.594h6v5h1v-6h-7z" + class="ColorScheme-Text"/> +<path style="fill:currentColor;fill-opacity:1;stroke:none" + class="ColorScheme-NegativeText" + d="m431.42 540.82l-.707.707 1.793 1.793-1.793 1.793.707.707 1.793-1.793 1.793 1.793.707-.707-1.793-1.793 1.793-1.793-.707-.707-1.793 1.793z" + /> +</g> +</svg> diff --git a/pd/nw/dialog_search.html b/pd/nw/dialog_search.html index 6727ce498cfce3e7b2d64eaf6b3a77e6833bcd35..4c72e35fcb9c2212abe8425bbd12ef575a3fd18d 100644 --- a/pd/nw/dialog_search.html +++ b/pd/nw/dialog_search.html @@ -135,6 +135,172 @@ 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] = descr ? { + id: id, + title: title, + description: descr + } : { + id: id, + title: title + }; + 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(); +} + +function toc_is_bookmarked(id) +{ + var i = toc_bookmarks(); + if (i >= 0) { + var l = toc.length; + while (i < l && toc[i].id !== id) { + i++; + } + return i<l; + } else { + // no bookmarks + return false; + } +} + // Stop-gap translator function translate_form() { var elements = document.querySelectorAll("[data-i18n]"), @@ -157,6 +323,45 @@ function click_toc(dir) { var current_dir; +function canonical_path(dir) +{ + // normalize + dir = path.normalize(dir); + // get rid of Windows' '\', '/' works just as well and is more portable + return pdgui.defunkify_windows_path(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 canonical_path(absname); + } catch (err) { + } + return null; +} + +function toc_bookmark_update(dir) +{ + var bookmark = document.getElementById("bookmark_indicator"); + var rel = canonical_path(path.relative(pdgui.get_lib_dir(), dir)); + dir = canonical_path(dir); + var id = dir.length <= rel.length ? dir : rel; + bookmark.src = toc_is_bookmarked(id) ? "bookmark2.svg" : "bookmark.svg"; +} + +function toc_bookmark_status(enabled) { + document.getElementById("bookmark_indicator").style. + setProperty("opacity", enabled?"1.0":"0.5"); +} + function display_toc() { var results_elem = document.getElementById("results"), div, @@ -165,13 +370,16 @@ function display_toc() { text_node; // reset current_dir to doc current_dir = path.join(pdgui.get_lib_dir(), "doc"); + toc_bookmark_update(current_dir); + toc_bookmark_status(false); toc.forEach(function(doc, i, a) { 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 + "');"; + // We need to properly stringify click_toc's argument here. + a.href = "javascript: click_toc(" + JSON.stringify(doc.id) + ");"; a.textContent = doc.title; // set title to path for tooltip a.title = doc.id; @@ -202,6 +410,7 @@ function finish_build(idx) { index = idx; document.getElementById("search_text").disabled = false; clear_results(); + toc_load(); display_toc(); } @@ -230,8 +439,11 @@ function display_directory_callback(err, files) { function display_directory(dir) { current_dir = dir; + var is_doc = current_dir === path.join(pdgui.get_lib_dir(), "doc"); clear_results(); fs.readdir(dir, display_directory_callback); + toc_bookmark_update(dir); + toc_bookmark_status(!is_doc); } function file_browser_click() { @@ -242,12 +454,23 @@ function file_browser_click() { document.getElementById("file_browser").click(); } +function bookmark_indicator_click() { + toggle_bookmark(current_dir); +} + function file_browser_callback(elem) { var doc = elem.value; if (doc !== "") { - pdgui.doc_open(pdgui.defunkify_windows_path(path.dirname(doc)), - pdgui.defunkify_windows_path(path.basename(doc))); - display_directory(pdgui.defunkify_windows_path(path.dirname(doc))); + var defunkify = pdgui.defunkify_windows_path; + var dir = path.dirname(doc); + pdgui.doc_open(defunkify(dir), defunkify(path.basename(doc))); + display_directory(defunkify(dir)); + // update the search field accordingly; use a relative path if that + // makes sense, and canonicalize + var rel = canonical_path(path.relative(pdgui.get_lib_dir(), dir)); + dir = canonical_path(dir); + dir = dir.length <= rel.length ? dir : rel; + document.getElementById("search_text").value = dir; } } @@ -389,6 +612,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 +641,65 @@ function toggle_find_bar() { } } +// Adds (or deletes, if del is true) a bookmark to the toc. +function do_bookmark(dirname, del) +{ + if (current_dir === path.join(pdgui.get_lib_dir(), "doc")) { + /* We don't want to bookmark the doc directory. There's nothing + interesting to see there anyway, and, since the toc also lives + there, just bailing out at this point we prevent an interesting + race condition which arises if we try to update the toc while we're + displaying it. */ + return; + } + /* 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 = canonical_path(path.relative(pdgui.get_lib_dir(), dirname)); + dirname = canonical_path(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); + toc_bookmark_update(dirname); +} + +// Toggle bookmark for the given directory. This is invoked by clicking on +// the bookmark indicator to the right of the search field. +function toggle_bookmark(dir) +{ + var rel = canonical_path(path.relative(pdgui.get_lib_dir(), dir)); + dir = canonical_path(dir); + var id = dir.length <= rel.length ? dir : rel; + do_bookmark(dir, toc_is_bookmarked(id)); +} + function add_events() { // closing the Window nw.Window.get().on("close", function() { @@ -450,16 +738,32 @@ function add_events() { } }); + document.getElementById("bookmark_indicator").addEventListener("click", + function(evt) { + if (evt.currentTarget === document.activeElement) { + bookmark_indicator_click(); + } + }); + // Keydown in the document document.body.addEventListener("keydown", function(evt) { var input_elem = document.getElementById("search_text"), - button_elem = document.getElementById("file_browser_button"); + button_elem = document.getElementById("file_browser_button"), + button_elem2 = document.getElementById("bookmark_indicator"); if (find_bar_shortcut(evt)) { toggle_find_bar(); - } else if (evt.target === button_elem && + } else if ((evt.target === button_elem || + evt.target === button_elem2) && 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"); @@ -494,6 +798,7 @@ function register_window_id(id, attrs) { function display_no_results() { document.getElementById("results").textContent = l("search.no_results"); + toc_bookmark_update(current_dir); } function display_doc(doc) { @@ -516,6 +821,7 @@ function display_doc(doc) { div.appendChild(text_node); } results_elem.appendChild(div); + toc_bookmark_update(current_dir); } function doc_search() { @@ -532,16 +838,16 @@ 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(); text_elem.blur(); + toc_bookmark_status(false); results = index.search(search_text); for (i = 0; i < results.length; i++) { doc = index.documentStore.getDoc(results[i].ref); @@ -573,10 +879,14 @@ function doc_search() { name="search_text" id="search_text" data-i18n="[title]search.search"> - <input type="image" + <input type="image" style="vertical-align:middle;" src="folder.svg" id="file_browser_button" data-i18n="[title]search.browse"> + <input type="image" style="vertical-align:middle;" + src="bookmark.svg" + id="bookmark_indicator" + data-i18n="[title]search.bookmark"> </form> <div id="results"> </div> diff --git a/pd/nw/folder.svg b/pd/nw/folder.svg index 6d9ea6e9f4feaf79f5bbb42ae2b8377a37e6f25c..337ef4b71e45ea78255edfd64ce02901e4c4df6d 100644 --- a/pd/nw/folder.svg +++ b/pd/nw/folder.svg @@ -1 +1,13 @@ -<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 32 32"><defs><clipPath><path d="m69.63 12.145h-.052c-22.727-.292-46.47 4.077-46.709 4.122-2.424.451-4.946 2.974-5.397 5.397-.044.237-4.414 23.983-4.122 46.71-.292 22.777 4.078 46.523 4.122 46.761.451 2.423 2.974 4.945 5.398 5.398.237.044 23.982 4.413 46.709 4.121 22.779.292 46.524-4.077 46.761-4.121 2.423-.452 4.946-2.976 5.398-5.399.044-.236 4.413-23.981 4.121-46.709.292-22.777-4.077-46.523-4.121-46.761-.453-2.423-2.976-4.946-5.398-5.397-.238-.045-23.984-4.414-46.71-4.122"/></clipPath><linearGradient gradientUnits="userSpaceOnUse" y2="352.98" x2="-601.15" y1="663.95" x1="-591.02" id="2"><stop stop-color="#a0a0a0"/><stop offset="1" stop-color="#aaa"/></linearGradient><linearGradient gradientUnits="userSpaceOnUse" y2="354.29" x2="-704.05" y1="647.77" x1="-701.19" id="1"><stop stop-color="#acabab"/><stop offset="1" stop-color="#d4d4d4"/></linearGradient><linearGradient id="0" x1="59.12" y1="-19.888" x2="59.15" y2="-37.783" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.17478 0 0 4.16765-1069.7 447.73)"><stop stop-color="#a0a0a0"/><stop offset="1" stop-color="#bdbdbd"/></linearGradient></defs><g transform="matrix(.07089 0 0 .07017 23.295-40.67)" fill="#60aae5"><path transform="matrix(.7872 0 0 .79524 415.34 430.11)" d="m-884.1 294.78c-4.626 0-8.349 3.718-8.349 8.335v161.41l468.19 1v-121.2c0-4.618-3.724-8.335-8.35-8.335h-272.65c-8.51.751-9.607-.377-13.812-5.981-5.964-7.968-14.969-21.443-20.84-29.21-4.712-6.805-5.477-6.02-13.292-6.02z" fill="url(#0)" color="#000"/><rect transform="matrix(.7872 0 0 .79524 415.34 430.11)" y="356.85" x="-890.28" height="295.13" width="463.85" fill="url(#1)" stroke="url(#1)" stroke-width="2.378" rx="9.63"/><rect width="463.85" height="295.13" x="-890.28" y="356.85" transform="matrix(.7872 0 0 .79524 415.34 430.11)" fill="none" stroke="url(#2)" stroke-linejoin="round" stroke-linecap="round" stroke-width="5.376" rx="9.63"/></g></svg> +<svg xmlns="http://www.w3.org/2000/svg" height="20" width="20" viewBox="0 0 16 16"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#4d4d4d; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 2 2 L 2 3 L 2 6 L 2 7 L 2 13 L 2 14 L 14 14 L 14 13 L 14 6 L 14 5 L 14 4 L 9.0078125 4 L 7.0078125 2 L 7 2.0078125 L 7 2 L 3 2 L 2 2 z M 3 3 L 6.5917969 3 L 7.59375 4 L 7 4 L 7 4.0078125 L 6.9921875 4 L 4.9921875 6 L 3 6 L 3 3 z M 3 7 L 13 7 L 13 13 L 3 13 L 3 7 z " + class="ColorScheme-Text" + /> +</svg> diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json index 798fc5cb9852ab30480f068447d0c7e3eb5868ab..6a43d138c9d81288f3996f6ae8460fa0df4de399 100644 --- a/pd/nw/locales/de/translation.json +++ b/pd/nw/locales/de/translation.json @@ -499,6 +499,7 @@ }, "search": { "browse": "Durchsuche die Dokumentation", + "bookmark": "Lesezeichen hinzufügen oder entfernen", "search": "Suche", "building_index": "Erstelle Index...", "no_results": "Keine Resultate gefunden.", diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json index 2a24c5494589e9f1628308087fe25ff875d08527..b54d90c993b803af05c4e412ae7f566a661a9a12 100644 --- a/pd/nw/locales/en/translation.json +++ b/pd/nw/locales/en/translation.json @@ -499,6 +499,7 @@ }, "search": { "browse": "browse the documentation", + "bookmark": "add or remove a bookmark", "search": "search", "building_index": "Building index...", "no_results": "No results found.", diff --git a/pd/nw/locales/fr/translation.json b/pd/nw/locales/fr/translation.json index 6e476f33beca113d02f129c80f7236862c11a8fc..59099097fa6e7b616d36514515c99bd0efc622d5 100644 --- a/pd/nw/locales/fr/translation.json +++ b/pd/nw/locales/fr/translation.json @@ -499,6 +499,7 @@ }, "search": { "browse": "Parcourir la documentation", + "bookmark": "Ajouter ou enlever un favori", "search": "Chercher", "building_index": "Construction de l'index...", "no_results": "Aucun résultat trouvé !",