From ce48a4caf05a4061657e0d02c5421feb3dbb6b0a Mon Sep 17 00:00:00 2001
From: Jonathan Wilkes <jon.w.wilkes@gmail.com>
Date: Fri, 13 May 2016 15:56:57 -0400
Subject: [PATCH] first try at getting App and "Edit" menu to behave correctly
 on OSX

---
 pd/nw/pd_canvas.js |  53 ++++++++--
 pd/nw/pd_menus.js  | 238 ++++++++++++++++++++++++++++++---------------
 2 files changed, 203 insertions(+), 88 deletions(-)

diff --git a/pd/nw/pd_canvas.js b/pd/nw/pd_canvas.js
index 8aa41aa0a..0015d849b 100644
--- a/pd/nw/pd_canvas.js
+++ b/pd/nw/pd_canvas.js
@@ -618,12 +618,14 @@ var canvas_events = (function() {
 
     // Copy event
     document.addEventListener("copy", function(evt) {
-        // This event doesn't currently get called because the
-        // nw menubar receives the event and doesn't propagate
-        // to the DOM. But if we add the ability to toggle menubar
-        // display, we might need to rely on this listener.
         pdgui.post("copy detected by DOM listener");
-        pdgui.pdsend(name, "copy");
+        // On OSX, this event gets triggered when we're editing
+        // inside an object/message box. So we only forward the
+        // copy message to Pd if we're in a "normal" canvas state
+        pdgui.post("copy detected by DOM listener");
+        if (canvas_events.get_state() === "normal") {
+            pdgui.pdsend(name, "copy");
+        }
     });
 
     // Listen to paste event using the half-baked Clipboard API from HTML5
@@ -658,12 +660,19 @@ var canvas_events = (function() {
         // user won't get bothered with a prompt at all, and normal Pd
         // paste behavior will follow.
 
-        // Finally, from a usability standpoint the main drawback is that
+        // From a usability standpoint the main drawback is that
         // you can't try to paste the same Pd source code more than once.
         // For users who want to pasting lots of source code this could be
         // a frustration, but Pd's normal copy/paste behavior remains
         // intuitive and in line with the way other apps tend to work.
 
+        // Yet another caveat: we only want to check the external buffer
+        // and/or send a "paste" event to Pd if the canvas is in a "normal"
+        // state.
+        if (canvas_events.get_state() !== "normal") {
+            return;
+        }
+
         if (might_be_a_pd_file(clipboard_data) &&
             clipboard_data !== pdgui.get_last_clipboard_data()) {
             if (permission_to_paste_from_external_clipboard()) {
@@ -952,13 +961,18 @@ function create_popup_menu(name) {
 }
 
 function nw_undo_menu(undo_text, redo_text) {
-    if (undo_text === "no") {
+    // Disabling undo/redo menu buttons on OSX will
+    // turn off any DOM events associated with that
+    // key command. So we can't disable them here--
+    // otherwise it would turn off text-editing
+    // undo/redo.
+    if (process.platform !== "darwin" && undo_text === "no") {
         canvas_menu.edit.undo.enabled = false;
     } else {
         canvas_menu.edit.undo.enabled = true;
         canvas_menu.edit.undo.label = l("menu.undo") + " " + undo_text;
     }
-    if (redo_text === "no") {
+    if (process.platform !== "darwin" && redo_text === "no") {
         canvas_menu.edit.redo.enabled = false;
     } else {
         canvas_menu.edit.redo.enabled = true;
@@ -994,11 +1008,16 @@ function instantiate_live_box() {
 var canvas_menu = {};
 
 function set_edit_menu_modals(state) {
+    // OSX needs to keep these enabled, otherwise the events won't trigger
+    if (process.platform === "darwin") {
+        state = true;
+    }
     canvas_menu.edit.undo.enabled = state;
     canvas_menu.edit.redo.enabled = state;
     canvas_menu.edit.cut.enabled = state;
     canvas_menu.edit.copy.enabled = state;
     canvas_menu.edit.paste.enabled = state;
+    canvas_menu.edit.selectall.enabled = state;
 }
 
 function set_editmode_checkbox(state) {
@@ -1100,11 +1119,25 @@ function nw_create_patch_window_menus(gui, w, name) {
     // Edit menu
     minit(m.edit.undo, {
         enabled: true,
-        click: function () { pdgui.pdsend(name, "undo"); }
+        click: function () {
+            if (process.platform === "darwin" &&
+                canvas_events.get_state() === "text") {
+                document.execCommand("undo", false, null);
+            } else {
+                pdgui.pdsend(name, "undo");
+            }
+        }
     });
     minit(m.edit.redo, {
         enabled: true,
-        click: function () { pdgui.pdsend(name, "redo"); }
+        click: function () {
+            if (process.platform === "darwin" &&
+                canvas_events.get_state() === "text") {
+                document.execCommand("redo", false, null);
+            } else {
+                pdgui.pdsend(name, "redo");
+            }
+        }
     });
     minit(m.edit.cut, {
         enabled: true,
diff --git a/pd/nw/pd_menus.js b/pd/nw/pd_menus.js
index bf03cb8fc..c1dfaccc9 100644
--- a/pd/nw/pd_menus.js
+++ b/pd/nw/pd_menus.js
@@ -36,6 +36,16 @@ function create_menu(gui, type) {
     // Window menu
     window_menu = new gui.Menu({ type: "menubar" });
 
+    // On OSX, we need to start with the built-in mac menu in order to
+    // get the application menu to show up correctly. Unfortunately, this
+    // will also spawn a built-in "Edit" and "Window" menu. Even more
+    // unfortunately, we must use the built-in "Edit" menu-- without it
+    // there is no way to get <command-v> shortcut to trigger the
+    // DOM "paste" event.
+    if (osx) {
+        window_menu.createMacBuiltin("purr-data");
+    }
+
     // File menu
     file_menu = new gui.Menu();
 
@@ -101,46 +111,94 @@ function create_menu(gui, type) {
     }));
 
     // Edit menu
-    edit_menu = new gui.Menu();
-
-    // Edit sub-entries
     m.edit = {};
-    if (canvas_menu) {
-        edit_menu.append(m.edit.undo = new gui.MenuItem({
+    // For OSX, we have to use the built-in "Edit" menu-- I haven't
+    // found any other way to get "paste" event to trickle down to
+    // the DOM. As a consequence, we must fetch the relevant menu
+    // items that were created above with createMacBuiltin and assign
+    // them to the relevant variables below
+    if (osx) {
+        edit_menu = window_menu.items[1].submenu;
+        m.edit.undo = window_menu.items[1].submenu.items[0];
+        // Remove the default Undo, since we can't seem to bind
+        // a click function to it
+        window_menu.items[1].submenu.remove(m.edit.undo);
+
+        // Now create a new one and insert it at the top
+        edit_menu.insert(m.edit.undo = new gui.MenuItem({
             label: l("menu.undo"),
-            tooltip: l("menu.undo_tt")
-        }));
-        edit_menu.append(m.edit.redo = new gui.MenuItem({
+            tooltip: l("menu.undo_tt"),
+            key: "z",
+            modifiers: cmd_or_ctrl
+        }), 0);
+
+        m.edit.redo = window_menu.items[1].submenu.items[1];
+        // Remove the default Undo, since we can't seem to bind
+        // a click function to it
+        window_menu.items[1].submenu.remove(m.edit.redo);
+
+        // Now create a new one and insert it at the top
+        edit_menu.insert(m.edit.redo = new gui.MenuItem({
             label: l("menu.redo"),
-            tooltip: l("menu.redo_tt")
-        }));
-        edit_menu.append(new gui.MenuItem({ type: "separator" }));
-        edit_menu.append(m.edit.cut = new gui.MenuItem({
-            label: l("menu.cut"),
-            key: "x",
+            tooltip: l("menu.redo_tt"),
+            key: "z",
+            modifiers: "shift+" + cmd_or_ctrl
+        }), 1);
+
+        // Note: window_menu.items[1].submenu.items[2] is the separator
+        m.edit.cut = window_menu.items[1].submenu.items[3];
+        m.edit.copy = window_menu.items[1].submenu.items[4];
+        m.edit.paste = window_menu.items[1].submenu.items[5];
+        // There's no "Delete" item for GNU/Linux or Windows--
+        // not sure yet what to do with it.
+        m.edit.delete = window_menu.items[1].submenu.items[6];
+        m.edit.selectall= window_menu.items[1].submenu.items[7];
+    } else {
+        edit_menu = new gui.Menu();
+        // Edit sub-entries
+        if (canvas_menu) {
+            edit_menu.append(m.edit.undo = new gui.MenuItem({
+                label: l("menu.undo"),
+                tooltip: l("menu.undo_tt")
+            }));
+            edit_menu.append(m.edit.redo = new gui.MenuItem({
+                label: l("menu.redo"),
+                tooltip: l("menu.redo_tt")
+            }));
+            edit_menu.append(new gui.MenuItem({ type: "separator" }));
+            edit_menu.append(m.edit.cut = new gui.MenuItem({
+                label: l("menu.cut"),
+                key: "x",
+                modifiers: cmd_or_ctrl,
+                tooltip: l("menu.cut_tt")
+            }));
+        }
+        edit_menu.append(m.edit.copy = new gui.MenuItem({
+            label: l("menu.copy"),
+            key: "c",
             modifiers: cmd_or_ctrl,
-            tooltip: l("menu.cut_tt")
-        }));
+            tooltip: l("menu.copy_tt")
+        }));
+        if (canvas_menu) {
+            // The nwjs menubar keybindings don't propagate down
+            // to the DOM. Here, we need the DOM to receive the
+            // "paste" event in case we want to paste a Pd file
+            // from an external buffer. So unfortunately we can't
+            // do the keybindings here. The side-effect is that
+            // "Ctrl-V" isn't displayed in the menu item.
+            edit_menu.append(m.edit.paste = new gui.MenuItem({
+                label: l("menu.paste"),
+                //key: "v",
+                //modifiers: cmd_or_ctrl,
+                tooltip: l("menu.paste_tt")
+            }));
+        }
     }
-    edit_menu.append(m.edit.copy = new gui.MenuItem({
-        label: l("menu.copy"),
-        key: "c",
-        modifiers: cmd_or_ctrl,
-        tooltip: l("menu.copy_tt")
-    }));
+
+    // We need "duplicate" for canvas_menu and for OSX, where it's not
+    // part of the builtin Edit menu...
+
     if (canvas_menu) {
-        // The nwjs menubar keybindings don't propagate down
-        // to the DOM. Here, we need the DOM to receive the
-        // "paste" event in case we want to paste a Pd file
-        // from an external buffer. So unfortunately we can't
-        // do the keybindings here. The side-effect is that
-        // "Ctrl-V" isn't displayed in the menu item.
-        edit_menu.append(m.edit.paste = new gui.MenuItem({
-            label: l("menu.paste"),
-            //key: "v",
-            //modifiers: cmd_or_ctrl,
-            tooltip: l("menu.paste_tt")
-        }));
         edit_menu.append(m.edit.duplicate = new gui.MenuItem({
             label: l("menu.duplicate"),
             key: "d",
@@ -148,12 +206,17 @@ function create_menu(gui, type) {
             tooltip: l("menu.duplicate_tt")
         }));
     }
-    edit_menu.append(m.edit.selectall = new gui.MenuItem({
-        label: l("menu.selectall"),
-        key: "a",
-        modifiers: cmd_or_ctrl,
-        tooltip: l("menu.selectall_tt")
-    }));
+
+    // OSX already has "Select All" in the builtin Edit menu...
+    if (!osx) {
+        edit_menu.append(m.edit.selectall = new gui.MenuItem({
+            label: l("menu.selectall"),
+            key: "a",
+            modifiers: cmd_or_ctrl,
+            tooltip: l("menu.selectall_tt")
+        }));
+    }
+
     if (canvas_menu) {
         // Unfortunately nw.js doesn't allow
         // key: "Return" or key: "Enter", so we
@@ -374,8 +437,12 @@ function create_menu(gui, type) {
 
     // Windows menu... call it "winman" (i.e., window management)
     // to avoid confusion
-    winman_menu = new gui.Menu();
-
+    if (osx) {
+        // on OSX, createMacBuiltin creates a window menu
+        winman_menu = window_menu.items[2].submenu;
+    } else {
+        winman_menu = new gui.Menu();
+    }
     // Win sub-entries
     m.win = {};
     winman_menu.append(m.win.nextwin = new gui.MenuItem({
@@ -479,46 +546,61 @@ function create_menu(gui, type) {
     }));
 
     // Add submenus to window menu
-
-    // On OSX, we need to start with the built-in mac menu in order to
-    // get the application menu to show up correctly. Unfortunately, this
-    // will also spawn a built-in "Edit" and "Window" menu. Even more
-    // unfortunately, we must use the built-in "Edit" menu-- without it
-    // there is no way to get <command-v> shortcut to trigger the
-    // DOM "paste" event.
     if (osx) {
-        window_menu.createMacBuiltin("purr-data");
-    }
-    window_menu.append(new gui.MenuItem({
-        label: l("menu.file"),
-        submenu: file_menu
-    }));
-    window_menu.append(new gui.MenuItem({
-        label: l("menu.edit"),
-        submenu: edit_menu
-    }));
-    window_menu.append(new gui.MenuItem({
-        label: l("menu.view"),
-        submenu: view_menu
-    }));
-    if (canvas_menu) {
+        window_menu.insert(new gui.MenuItem({
+            label: l("menu.file"),
+            submenu: file_menu
+        }), 1);
+        // Edit menu created from mac builtin above
+        window_menu.insert(new gui.MenuItem({
+            label: l("menu.view"),
+            submenu: view_menu
+        }), 3);
+        window_menu.insert(new gui.MenuItem({
+            label: l("menu.put"),
+            submenu: put_menu
+        }), 4);
+        // "Window" menu created from mac builtin above
+        window_menu.insert(new gui.MenuItem({
+            label: l("menu.media"),
+            submenu: media_menu
+        }), 5);
+        window_menu.append(new gui.MenuItem({
+            label: l("menu.help"),
+            submenu: help_menu
+        }));
+    } else {
+        window_menu.append(new gui.MenuItem({
+            label: l("menu.file"),
+            submenu: file_menu
+        }));
+        window_menu.append(new gui.MenuItem({
+            label: l("menu.edit"),
+            submenu: edit_menu
+        }));
         window_menu.append(new gui.MenuItem({
-        label: l("menu.put"),
-        submenu: put_menu
+            label: l("menu.view"),
+            submenu: view_menu
+        }));
+        if (canvas_menu) {
+            window_menu.append(new gui.MenuItem({
+                label: l("menu.put"),
+                submenu: put_menu
+            }));
+        }
+        window_menu.append(new gui.MenuItem({
+            label: l("menu.windows"),
+            submenu: winman_menu
+        }));
+        window_menu.append(new gui.MenuItem({
+            label: l("menu.media"),
+            submenu: media_menu
+        }));
+        window_menu.append(new gui.MenuItem({
+            label: l("menu.help"),
+            submenu: help_menu
         }));
     }
-    window_menu.append(new gui.MenuItem({
-    label: l("menu.windows"),
-    submenu: winman_menu
-    }));
-    window_menu.append(new gui.MenuItem({
-    label: l("menu.media"),
-    submenu: media_menu
-    }));
-    window_menu.append(new gui.MenuItem({
-    label: l("menu.help"),
-    submenu: help_menu
-    }));
 
     // Assign to window
     gui.Window.get().menu = window_menu;
-- 
GitLab