From c0018ac1d59a682df282db2102cafd00de70a71d Mon Sep 17 00:00:00 2001
From: Albert Graef <aggraef@gmail.com>
Date: Sat, 14 Jan 2017 10:29:26 +0100
Subject: [PATCH] Add startup configuration to the preferences dialog (fixes
 issue #89).

---
 pd/nw/css/c64.css                 |   6 +-
 pd/nw/css/default.css             |   6 +-
 pd/nw/css/extended.css            |   6 +-
 pd/nw/css/inverted.css            |   6 +-
 pd/nw/css/strongbad.css           |   6 +-
 pd/nw/css/subdued.css             |   6 +-
 pd/nw/css/vanilla.css             |   6 +-
 pd/nw/css/vanilla_inverted.css    |   6 +-
 pd/nw/dialog_prefs.html           | 354 +++++++++++++++++++++++++++++-
 pd/nw/locales/de/translation.json |  16 ++
 pd/nw/locales/en/translation.json |  16 ++
 11 files changed, 424 insertions(+), 10 deletions(-)

diff --git a/pd/nw/css/c64.css b/pd/nw/css/c64.css
index db2b8e856..dcd6bacc9 100644
--- a/pd/nw/css/c64.css
+++ b/pd/nw/css/c64.css
@@ -429,6 +429,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -539,7 +543,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/default.css b/pd/nw/css/default.css
index 74f8761f2..5d5ee68ab 100644
--- a/pd/nw/css/default.css
+++ b/pd/nw/css/default.css
@@ -560,6 +560,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -683,7 +687,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/extended.css b/pd/nw/css/extended.css
index 57f6a7926..c884ab882 100644
--- a/pd/nw/css/extended.css
+++ b/pd/nw/css/extended.css
@@ -417,6 +417,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -527,7 +531,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/inverted.css b/pd/nw/css/inverted.css
index 768742e7d..e81421bf5 100644
--- a/pd/nw/css/inverted.css
+++ b/pd/nw/css/inverted.css
@@ -443,6 +443,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -553,7 +557,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/strongbad.css b/pd/nw/css/strongbad.css
index 3363e3440..65f5d760b 100644
--- a/pd/nw/css/strongbad.css
+++ b/pd/nw/css/strongbad.css
@@ -425,6 +425,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -535,7 +539,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/subdued.css b/pd/nw/css/subdued.css
index bb2669bc5..1acea1462 100644
--- a/pd/nw/css/subdued.css
+++ b/pd/nw/css/subdued.css
@@ -424,6 +424,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -534,7 +538,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/vanilla.css b/pd/nw/css/vanilla.css
index 9ea516797..b9dd8502c 100644
--- a/pd/nw/css/vanilla.css
+++ b/pd/nw/css/vanilla.css
@@ -418,6 +418,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -528,7 +532,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/css/vanilla_inverted.css b/pd/nw/css/vanilla_inverted.css
index 943de4f46..56808b77e 100644
--- a/pd/nw/css/vanilla_inverted.css
+++ b/pd/nw/css/vanilla_inverted.css
@@ -423,6 +423,10 @@ input[name="font_size"] {
     width: 3em;
 }
 
+input[name="startup_flags"] {
+    width: 16em;
+}
+
 /* Canvas dialog */
 
 div.x-scale {
@@ -533,7 +537,7 @@ div.y2 {
 }
 
 /* This matches tabs displaying to their associated radio inputs */
-.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3 {
+.tab1:checked ~ .tab1, .tab2:checked ~ .tab2, .tab3:checked ~ .tab3, .tab4:checked ~ .tab4 {
     display: table;
     padding: 8px;
     line-height: 20px;
diff --git a/pd/nw/dialog_prefs.html b/pd/nw/dialog_prefs.html
index d1a669ceb..2b46054aa 100644
--- a/pd/nw/dialog_prefs.html
+++ b/pd/nw/dialog_prefs.html
@@ -31,6 +31,14 @@
           <span data-i18n="prefs.heading.gui"></span>
         </label>
 
+        <input type="radio"
+               name="prefs_radio_group"
+               id="startup_tab_radio"
+               class="tab4 prefs_tab"/>
+        <label for="startup_tab_radio" data-i18n="[title]prefs.heading.startup_tt">
+          <span data-i18n="prefs.heading.startup"></span>
+        </label>
+
         <div class="tab1">
           <div class="tab_settings">
             <label data-i18n="[title]prefs.audio.api_tt">
@@ -286,6 +294,53 @@
             </select>
           </div>
         </div>
+
+        <div class="tab4">
+          <div class="tab_settings">
+            <label data-i18n="[title]prefs.startup.paths_tt">
+              <span data-i18n="prefs.startup.paths"></span>
+            </label>
+            <div style=" height:92px; width:300px; background:white; border: 1px solid #bbb; overflow-y:auto; overflow-x:auto; padding:0px;">
+              <table id="startup_paths" style="width:100%; background:white;">
+              </table>
+            </div>
+            <div class="submit_buttons">
+              <button type="button" onClick="startup_path_new()" data-i18n="[title]prefs.startup.new_tt">
+                <span data-i18n="prefs.startup.new"></span>
+              </button>
+              <button type="button" id="startup_path_edit" onClick="startup_path_edit()" data-i18n="[title]prefs.startup.edit_tt">
+                <span data-i18n="prefs.startup.edit"></span>
+              </button>
+              <button type="button" id="startup_path_delete" onClick="startup_path_delete()" data-i18n="[title]prefs.startup.del_tt">
+                <span data-i18n="prefs.startup.del"></span>
+              </button>
+            </div>
+            <label data-i18n="[title]prefs.startup.libs_tt">
+              <span data-i18n="prefs.startup.libs"></span>
+            </label>
+            <div style=" height:92px; width:300px; background:white; border: 1px solid #bbb; overflow-y:auto; overflow-x:auto; padding:0px;">
+              <table id="startup_libs" style="width:100%; background:white;">
+              </table>
+            </div>
+            <div class="submit_buttons">
+              <button type="button" onClick="startup_lib_new()" data-i18n="[title]prefs.startup.new_tt">
+                <span data-i18n="prefs.startup.new"></span>
+              </button>
+              <button type="button" id="startup_lib_edit" onClick="startup_lib_edit()" data-i18n="[title]prefs.startup.edit_tt">
+                <span data-i18n="prefs.startup.edit"></span>
+              </button>
+              <button type="button" id="startup_lib_delete" onClick="startup_lib_delete()" data-i18n="[title]prefs.startup.del_tt">
+                <span data-i18n="prefs.startup.del"></span>
+              </button>
+            </div>
+            <label data-i18n="[title]prefs.startup.flags_tt">
+              <span data-i18n="prefs.startup.flags"></span>
+              <input type="text"
+                     id="startup_flags"
+                     name="startup_flags">
+            </label>
+          </div>
+        </div>
       </div>
 
       <div class="submit_buttons prefs_buttons">
@@ -329,6 +384,86 @@ function get_gui_preset() {
     return document.getElementById("gui_preset").selectedOptions[0].value;
 }
 
+// startup config data
+
+// XXXTODO: We should maybe include these in the dialog, but the startup tab
+// is already crowded enough as it is, so currently we just record them in
+// global variables and then resubmit them unchanged. Note that all of these
+// options are rarely used by the average user, and they can also be entered
+// using the startup flags field if needed.
+var startup_use_stdpath = 1;
+var startup_verbose = 0;
+var startup_defeat_rt = 0;
+
+function get_startup_flags() {
+    return pdgui.encode_for_dialog(document.getElementById("startup_flags").value);
+}
+
+// Kludge alert: The user may create invalid empty entries in the paths and
+// libs tables during editing. The easiest way to deal with these would be to
+// just ignore them, but the table editor currently won't let us do that, so
+// instead we flag them using the special text "N/A" in red. These invalid
+// entries can be checked for with the valid_startup_item function below and
+// are filtered out when submitting the data to Pd. If anyone has a better
+// idea to deal with this corner case then please be my guest. -ag
+
+function valid_startup_item(item)
+{
+    return item.textContent != "N/A" || item.style.color != "red";
+}
+
+function finalize_startup_entry(item)
+{
+    if (!item.textContent) {
+        // flag empty entry as invalid
+        item.textContent = "N/A";
+        item.style.color = "red";
+    }
+}
+
+function prepare_startup_entry(item)
+{
+    if (!valid_startup_item(item)) {
+        // prepare an invalid entry for editing by turning it back into an
+        // empty string
+        item.textContent = "";
+        item.style.color = "black";
+    }
+}
+
+// retrieve the path and lib lists from the corresponding tables in the dialog
+// and encode them in a form which can be safely transmitted via FUDI
+
+function get_path_array() {
+    var table = document.getElementById("startup_paths");
+    var arr = [];
+    if (table != null) {
+        for (var i = 0; i < table.rows.length; i++) {
+            var cell = table.rows[i].cells[0];
+            // filter out empty items
+            if (valid_startup_item(cell)) {
+                arr.push(pdgui.encode_for_dialog(cell.textContent));
+            }
+        }
+    }
+    return arr;
+}
+
+function get_lib_array() {
+    var table = document.getElementById("startup_libs");
+    var arr = [];
+    if (table != null) {
+        for (var i = 0; i < table.rows.length; i++) {
+            var cell = table.rows[i].cells[0];
+            // filter out empty items
+            if (valid_startup_item(cell)) {
+                arr.push(pdgui.encode_for_dialog(cell.textContent));
+            }
+        }
+    }
+    return arr;
+}
+
 // callbacks for devices and/or their number of channels
 function dev_change(elem) {
     var attrs, id, direction, index;
@@ -458,6 +593,10 @@ function apply(save_prefs) {
     // Send the name of the gui preset to Pd
     pdgui.pdsend("pd gui-preset", get_gui_preset());
 
+    // Send the startup config data to Pd
+    pdgui.pdsend.apply(null, ["pd path-dialog", startup_use_stdpath, startup_verbose].concat(get_path_array()));
+    pdgui.pdsend.apply(null, ["pd startup-dialog", startup_defeat_rt, get_startup_flags()].concat(get_lib_array()));
+
     if (save_prefs) {
         // save the prefs in Pd...
         pdgui.pdsend("pd save-preferences");
@@ -702,12 +841,223 @@ function gui_prefs_callback(name) {
     }
 }
 
+// startup settings
+
 function path_prefs_callback(use_stdpath, verbose, path_array) {
-    pdgui.post("path callback: " + path_array.join("\n"));
+    //pdgui.post("path callback: " + path_array.join("\n"));
+    // XXXTODO
+    startup_use_stdpath = use_stdpath;
+    startup_verbose = verbose;
+    var table = document.getElementById("startup_paths");
+    if (table != null) {
+        // Add the path_array elements to the table.
+        for (var i = 0; i < path_array.length; i++) {
+            var row = table.insertRow(i);
+            var cell = row.insertCell(0);
+            cell.textContent = path_array[i];
+            // install the requisite handlers
+            startup_paths_handlers(cell);
+        }
+    }
+    selected_startup_path = -1;
+    update_path_buttons();
 }
 
 function lib_prefs_callback(defeat_rt, flag_string, lib_array) {
-    pdgui.post("lib callback: " + lib_array.join("\n"));
+    //pdgui.post("lib callback: " + lib_array.join("\n"));
+    // XXXTODO
+    startup_defeat_rt = defeat_rt;
+    document.getElementById("startup_flags").value = flag_string;
+    var table = document.getElementById("startup_libs");
+    if (table != null) {
+        // Add the lib_array elements to the table.
+        for (var i = 0; i < lib_array.length; i++) {
+            var row = table.insertRow(i);
+            var cell = row.insertCell(0);
+            cell.textContent = lib_array[i];
+            // install the requisite handlers
+            startup_libs_handlers(cell);
+        }
+    }
+    selected_startup_lib = -1;
+    update_lib_buttons();
+}
+
+var selected_startup_lib = -1;
+var selected_startup_path = -1;
+
+function current_startup_lib()
+{
+    var table = document.getElementById("startup_libs");
+    return table.rows[selected_startup_lib].cells[0];
+}
+
+function current_startup_path()
+{
+    var table = document.getElementById("startup_paths");
+    return table.rows[selected_startup_path].cells[0];
+}
+
+function update_lib_buttons()
+{
+    var val = selected_startup_lib < 0;
+    var edit_button = document.getElementById("startup_lib_edit");
+    var del_button = document.getElementById("startup_lib_delete");
+    edit_button.disabled = val;
+    del_button.disabled = val;
+}
+
+function update_path_buttons()
+{
+    var val = selected_startup_path < 0;
+    var edit_button = document.getElementById("startup_path_edit");
+    var del_button = document.getElementById("startup_path_delete");
+    edit_button.disabled = val;
+    del_button.disabled = val;
+}
+
+function startup_libs_handlers(item)
+{
+    item.style = "white-space: nowrap";
+    item.onclick = function () {
+        startup_lib_click(this);
+    };
+    item.onblur = function () {
+        startup_lib_blur(this);
+    };
+}
+
+function startup_paths_handlers(item)
+{
+    item.style = "white-space: nowrap";
+    item.onclick = function () {
+        startup_path_click(this);
+    };
+    item.onblur = function () {
+        startup_path_blur(this);
+    };
+}
+
+function startup_lib_click(item) {
+    var i = item.parentElement.rowIndex;
+    if (i != selected_startup_lib) {
+        if (selected_startup_lib >= 0) {
+            current_startup_lib().style.background = "white";
+            current_startup_lib().setAttribute("contenteditable", "false");
+        }
+        item.style.background = "#ddd";
+        selected_startup_lib = i;
+    } else if (item.getAttribute("contenteditable") != "true") {
+        item.style.background = "white";
+        item.contenteditable = "false";
+        selected_startup_lib = -1;
+    }
+    update_lib_buttons();
+}
+
+function startup_lib_new() {
+    var table = document.getElementById("startup_libs");
+    var row = table.insertRow(selected_startup_lib+1);
+    var cell = row.insertCell(0);
+    startup_libs_handlers(cell);
+    startup_lib_click(cell);
+    startup_lib_edit();
+}
+
+function startup_lib_edit() {
+    if (selected_startup_lib >= 0) {
+        var item = current_startup_lib();
+        prepare_startup_entry(item);
+        item.setAttribute("contenteditable", "true");
+        item.focus();
+    }
+}
+
+function startup_lib_blur(item) {
+    // Kludge alert: Apparently we can't just remove an empty row
+    // here, so we simply flag the entry as invalid instead.
+    finalize_startup_entry(item);
+    item.setAttribute("contenteditable", "false");
+}
+
+function startup_lib_delete() {
+    if (selected_startup_lib >= 0) {
+        var table = document.getElementById("startup_libs");
+        table.deleteRow(selected_startup_lib);
+        if (selected_startup_lib < table.rows.length) {
+            // make sure that the following row is selected, which is
+            // convenient when deleting successive items
+            var item = current_startup_lib();
+            selected_startup_lib = -1;
+            startup_lib_click(item);
+        } else {
+            selected_startup_lib = -1;
+            if (table.rows.length > 0)
+                startup_lib_click(table.rows[table.rows.length-1].cells[0]);
+        }
+    }
+    update_lib_buttons();
+}
+
+function startup_path_click(item) {
+    var i = item.parentElement.rowIndex;
+    if (i != selected_startup_path) {
+        if (selected_startup_path >= 0) {
+            current_startup_path().style.background = "white";
+            current_startup_path().setAttribute("contenteditable", "false");
+        }
+        item.style.background = "#ddd";
+        selected_startup_path = i;
+    } else if (item.getAttribute("contenteditable") != "true") {
+        item.style.background = "white";
+        item.contenteditable = "false";
+        selected_startup_path = -1;
+    }
+    update_path_buttons();
+}
+
+function startup_path_new() {
+    var table = document.getElementById("startup_paths");
+    var row = table.insertRow(selected_startup_path+1);
+    var cell = row.insertCell(0);
+    startup_paths_handlers(cell);
+    startup_path_click(cell);
+    startup_path_edit();
+}
+
+function startup_path_edit() {
+    if (selected_startup_path >= 0) {
+        var item = current_startup_path();
+        prepare_startup_entry(item);
+        item.setAttribute("contenteditable", "true");
+        item.focus();
+    }
+}
+
+function startup_path_blur(item) {
+    // Kludge alert: Apparently we can't just remove an empty row
+    // here, so we simply flag the entry as invalid instead.
+    finalize_startup_entry(item);
+    item.setAttribute("contenteditable", "false");
+}
+
+function startup_path_delete() {
+    if (selected_startup_path >= 0) {
+        var table = document.getElementById("startup_paths");
+        table.deleteRow(selected_startup_path);
+        if (selected_startup_path < table.rows.length) {
+            // make sure that the following row is selected, which is
+            // convenient when deleting successive items
+            var item = current_startup_path();
+            selected_startup_path = -1;
+            startup_path_click(item);
+        } else {
+            selected_startup_path = -1;
+            if (table.rows.length > 0)
+                startup_path_click(table.rows[table.rows.length-1].cells[0]);
+        }
+    }
+    update_path_buttons();
 }
 
 // This gets called from the nw_create_window function in index.html
diff --git a/pd/nw/locales/de/translation.json b/pd/nw/locales/de/translation.json
index d0f20e485..ee9ee8725 100644
--- a/pd/nw/locales/de/translation.json
+++ b/pd/nw/locales/de/translation.json
@@ -352,6 +352,8 @@
   },
   "prefs": {
     "heading": {
+      "startup": "Startup",
+      "startup_tt": "Startup-Einstellungen",
       "gui": "GUI",
       "gui_tt": "Einstellungen der Benutzeroberfläche",
       "audio": "Audio",
@@ -359,6 +361,20 @@
       "midi": "MIDI",
       "midi_tt": "konfiguriere die MIDI-Geräte"
     },
+    "startup": {
+      "flags": "Optionen",
+      "flags_tt": "Startup-Optionen für den Programmaufruf; Neustart der Anwendung ist erforderlich, damit Änderungen wirksam werden",
+      "libs": "Bibliotheken",
+      "libs_tt": "External-Bibliotheken, die beim Programmstart zu laden sind; Neustart der Anwendung ist erforderlich, damit Änderungen wirksam werden",
+      "paths": "Suchpfade",
+      "paths_tt": "Suchpfade für Abstraktionen und Externals; Neustart der Anwendung ist erforderlich, damit Änderungen wirksam werden",
+      "new": "Neu",
+      "new_tt": "Füge ein neues Element nach dem ausgewählten hinzu",
+      "edit": "Bearbeiten",
+      "edit_tt": "Bearbeite das ausgewählte Element",
+      "del": "Löschen",
+      "del_tt": "Lösche das ausgewählte Element"
+    },
     "gui": {
       "presets": {
         "gui_preset": "GUI-Preset",
diff --git a/pd/nw/locales/en/translation.json b/pd/nw/locales/en/translation.json
index 074466d9a..b2fb48daa 100644
--- a/pd/nw/locales/en/translation.json
+++ b/pd/nw/locales/en/translation.json
@@ -352,6 +352,8 @@
   },
   "prefs": {
     "heading": {
+      "startup": "startup",
+      "startup_tt": "startup settings",
       "gui": "GUI",
       "gui_tt": "settings for the user interface",
       "audio": "audio",
@@ -359,6 +361,20 @@
       "midi": "MIDI",
       "midi_tt": "configure MIDI devices"
     },
+    "startup": {
+      "flags": "startup flags",
+      "flags_tt": "Startup flags the program is invoked with; changes require a restart of the application to take effect",
+      "libs": "Libraries",
+	"libs_tt": "External libraries to be loaded on startup; changes require a restart of the application to take effect",
+      "paths": "Search Paths",
+      "paths_tt": "Search path for abstractions and externals; changes require a restart of the application to take effect",
+      "new": "New",
+      "new_tt": "Add an item after the selected one",
+      "edit": "Edit",
+      "edit_tt": "Edit the selected item",
+      "del": "Delete",
+      "del_tt": "Delete the selected item"
+    },
     "gui": {
       "presets": {
         "gui_preset": "GUI preset",
-- 
GitLab