瀏覽代碼

Find dialog (#37)

Lutz Roeder 7 年之前
父節點
當前提交
2a7e3af10d
共有 14 個文件被更改,包括 462 次插入54 次删除
  1. 1 0
      setup.py
  2. 157 30
      src/app.js
  3. 2 0
      src/view-browser.html
  4. 2 0
      src/view-browser.js
  5. 2 0
      src/view-electron.html
  6. 9 0
      src/view-electron.js
  7. 39 0
      src/view-find.css
  8. 131 0
      src/view-find.js
  9. 12 9
      src/view-node.js
  10. 6 1
      src/view-render.css
  11. 11 1
      src/view-render.js
  12. 1 1
      src/view-template.js
  13. 3 1
      src/view.css
  14. 86 11
      src/view.js

+ 1 - 0
setup.py

@@ -89,6 +89,7 @@ setuptools.setup(
             'view-browser.html', 'view-browser.js',
             'view-render.css', 'view-render.js',
             'view-node.css', 'view-node.js',
+            'view-find.css', 'view-find.js',
             'view-template.js',
             'view.js', 'view.css'
         ]

+ 157 - 30
src/app.js

@@ -13,6 +13,7 @@ class Application {
     constructor() {
         this._views = new ViewCollection();
         this._configuration = new ConfigurationService();
+        this._menu = new MenuService();
         this._openFileQueue = [];
 
         electron.app.setAppUserModelId('com.lutzroeder.netron');
@@ -101,10 +102,10 @@ class Application {
         }
         this.resetMenu();
         this._views.on('active-view-changed', (e) => {
-            this.resetMenu();
+            this.updateMenu();
         });
         this._views.on('active-view-updated', (e) => {
-            this.resetMenu();
+            this.updateMenu();
         });
     }
 
@@ -211,6 +212,7 @@ class Application {
         if (view) {
             view.execute(command, data || {});
         }
+        this.updateMenu();
     }
 
     reload() {
@@ -291,9 +293,15 @@ class Application {
             (process.defaultApp || /node_modules[\\/]electron[\\/]/.test(process.execPath));
     }
 
-    resetMenu() {
+    updateMenu() {
+        var context = {};
+        context.window = electron.BrowserWindow.getFocusedWindow();
+        context.webContents = context.window ? context.window.webContents : null; 
+        context.view = this._views.activeView;
+        this._menu.update(context);
+    }
 
-        var view = this._views.activeView;
+    resetMenu() {
 
         var menuRecentsTemplate = [];
         if (this._configuration.has('recents')) {
@@ -344,10 +352,10 @@ class Application {
                 },
                 { type: 'separator' },
                 { 
+                    id: 'file.export',
                     label: '&Export...',
                     accelerator: 'CmdOrCtrl+Shift+E',
                     click: () => this.export(),
-                    enabled: view && view.path ? true : false
                 },
                 { type: 'separator' },
                 { role: 'close' },
@@ -369,19 +377,37 @@ class Application {
         menuTemplate.push({
             label: '&Edit',
             submenu: [
-                { 
+                {
+                    id: 'edit.cut',
+                    label: 'Cu&t',
+                    accelerator: 'CmdOrCtrl+X',
+                    click: () => this.execute('cut', null),
+                },
+                {
+                    id: 'edit.copy',
                     label: '&Copy',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+C' : 'Ctrl+C',
+                    accelerator: 'CmdOrCtrl+C',
                     click: () => this.execute('copy', null),
-                    enabled: view && view.path ? true : false
                 },
-                /* { type: 'separator' },
                 {
+                    id: 'edit.paste',
+                    label: '&Paste',
+                    accelerator: 'CmdOrCtrl+V',
+                    click: () => this.execute('paste', null),
+                },
+                {
+                    id: 'edit.select-all',
+                    label: 'Select &All',
+                    accelerator: 'CmdOrCtrl+A',
+                    click: () => this.execute('selectall', null),
+                },
+                { type: 'separator' },
+                {
+                    id: 'edit.find',
                     label: '&Find...',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+F' : 'Ctrl+F',
+                    accelerator: 'CmdOrCtrl+F',
                     click: () => this.execute('find', null),
-                    enabled: view && view.path ? true : false
-                } */
+                }
             ]
         });
     
@@ -389,49 +415,47 @@ class Application {
             label: '&View',
             submenu: [
                 {
-                    label: !view || !view.get('show-details') ?  'Show &Details' : 'Hide &Details',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+D' : 'Ctrl+D',
+                    id: 'view.show-details',
+                    accelerator: 'CmdOrCtrl+D',
                     click: () => this.execute('toggle-details', null),
-                    enabled: view && view.path ? true : false
                 },
                 {
-                    label: !view || !view.get('show-names') ?  'Show &Names' : 'Hide &Names',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+U' : 'Ctrl+U',
+                    id: 'view.show-names',
+                    accelerator: 'CmdOrCtrl+U',
                     click: () => this.execute('toggle-names', null),
-                    enabled: view && view.path ? true : false
                 },
                 { type: 'separator' },
                 {
+                    id: 'view.reload',
                     label: '&Reload',
                     accelerator: (process.platform === 'darwin') ? 'Cmd+R' : 'F5',
                     click: () => this.reload(),
-                    enabled: view && view.path ? true : false
                 },
                 { type: 'separator' },
                 {
+                    id: 'view.reset-zoom',
                     label: 'Actual &Size',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+Backspace' : 'Ctrl+Backspace',
+                    accelerator: 'CmdOrCtrl+Backspace',
                     click: () => this.execute('reset-zoom', null),
-                    enabled: view && view.path ? true : false
                 },
                 {
+                    id: 'view.zoom-in',
                     label: 'Zoom &In',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+Up' : 'Ctrl+Down',
+                    accelerator: 'CmdOrCtrl+Up',
                     click: () => this.execute('zoom-in', null),
-                    enabled: view && view.path ? true : false
                 },
                 {
+                    id: 'view.zoom-out',
                     label: 'Zoom &Out',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+Down' : 'Ctrl+Down',
+                    accelerator: 'CmdOrCtrl+Down',
                     click: () => this.execute('zoom-out', null),
-                    enabled: view && view.path ? true : false
                 },
                 { type: 'separator' },
                 {
+                    id: 'view.show-properties',
                     label: '&Properties...',
-                    accelerator: (process.platform === 'darwin') ? 'Cmd+Enter' : 'Ctrl+Enter',
+                    accelerator: 'CmdOrCtrl+Enter',
                     click: () => this.execute('show-properties', null),
-                    enabled: view && view.path ? true : false
                 }        
             ]
         };
@@ -477,8 +501,51 @@ class Application {
             submenu: helpSubmenu
         });
 
-        var menu = electron.Menu.buildFromTemplate(menuTemplate);
-        electron.Menu.setApplicationMenu(menu);
+        var commandTable = {};
+        commandTable['file.export'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['edit.cut'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['edit.copy'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['edit.paste'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['edit.select-all'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['edit.find'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['view.show-details'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; },
+            label: (context) => { return !context.view || !context.view.get('show-details') ? 'Show &Details' : 'Hide &Details'; }
+        };
+        commandTable['view.show-names'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; },
+            label: (context) => { return !context.view || !context.view.get('show-names') ? 'Show &Names' : 'Hide &Names'; }
+        };
+        commandTable['view.reload'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['view.reset-zoom'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['view.zoom-in'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['view.zoom-out'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+        commandTable['view.show-properties'] = {
+            enabled: (context) => { return context.view && context.view.path ? true : false; }
+        };
+
+        this._menu.build(menuTemplate, commandTable);
+        this.updateMenu();
     }
 
     static minimizePath(file) {
@@ -752,7 +819,7 @@ class ConfigurationService {
                 var dir = electron.app.getPath('userData');
                 if (dir && dir.length > 0) {
                     var file = path.join(dir, 'configuration.json'); 
-                    fs.writeFileSync(file, data);          
+                    fs.writeFileSync(file, data);
                 }
             }
         }
@@ -772,4 +839,64 @@ class ConfigurationService {
 
 }
 
+class MenuService {
+
+    build(menuTemplate, commandTable) {
+        this._menuTemplate = menuTemplate;
+        this._commandTable = commandTable;
+        this._itemTable = {};
+        menuTemplate.forEach((menuTemplateMenu) => {
+            menuTemplateMenu.submenu.forEach((menuTemplateItem) => {
+                if (menuTemplateItem.id) {
+                    this._itemTable[menuTemplateItem.id] = menuTemplateItem;
+                }
+            });
+        });
+        this.rebuild();
+    }
+
+    rebuild() {
+        this._menu = electron.Menu.buildFromTemplate(this._menuTemplate);
+        electron.Menu.setApplicationMenu(this._menu);
+    }
+
+    update(context) {
+        if (!this._menu && !this._commandTable) {
+            return;
+        }
+        var rebuild = false;
+        Object.keys(this._commandTable).forEach((id) => {
+            var menuItem = this._menu.getMenuItemById(id);
+            var command = this._commandTable[id];
+            if (command && command.label) {
+                var label = command.label(context);
+                if (label != menuItem.label) {
+                    var menuTemplateItem = this._itemTable[id];
+                    if (menuTemplateItem) {
+                        menuTemplateItem.label = label;
+                        rebuild = true;
+                    }
+                }
+            }
+        });
+        if (rebuild) {
+            this.rebuild();
+        }
+        Object.keys(this._commandTable).forEach((id) => {
+            var menuItem = this._menu.getMenuItemById(id);
+            var command = this._commandTable[id];
+            if (command) {
+                if (command.enabled) {
+                    menuItem.enabled = command.enabled(context);
+                }
+                if (command.label) {
+                    if (menuItem.label != command.label(context)) {
+
+                    }
+                }
+            }
+        });
+    }
+}
+
 var application = new Application();

+ 2 - 0
src/view-browser.html

@@ -6,6 +6,7 @@
 <link rel='stylesheet' type='text/css' href='open-sans.css'>
 <link rel='stylesheet' type='text/css' href='view-render.css'>
 <link rel='stylesheet' type='text/css' href='view-node.css'>
+<link rel='stylesheet' type='text/css' href='view-find.css'>
 <link rel='stylesheet' type='text/css' href='view.css'>
 <link rel='shortcut icon' type='image/x-icon' href='favicon.ico' />
 <link rel='icon' type='image/png' href='icon.png' />
@@ -87,6 +88,7 @@
 <script type='text/javascript' src='view-browser.js'></script>
 <script type='text/javascript' src='view-render.js'></script>
 <script type='text/javascript' src='view-node.js'></script>
+<script type='text/javascript' src='view-find.js'></script>
 <script type='text/javascript' src='view.js'></script>
 </body>
 </html>

+ 2 - 0
src/view-browser.js

@@ -17,6 +17,8 @@ class BrowserHost {
                 if (!e.altKey && !e.shiftKey && (e.ctrlKey || e.metaKey)) {
                     switch (e.keyCode) {
                         case 70: // F
+                            this._view.find();
+                            e.preventDefault();
                             break;
                         case 68: // D
                             this._view.toggleDetails();

+ 2 - 0
src/view-electron.html

@@ -4,6 +4,7 @@
 <link rel='stylesheet' type='text/css' href='../node_modules/npm-font-open-sans/open-sans.css'>
 <link rel='stylesheet' type='text/css' href='view-render.css'>
 <link rel='stylesheet' type='text/css' href='view-node.css'>
+<link rel='stylesheet' type='text/css' href='view-find.css'>
 <link rel='stylesheet' type='text/css' href='view.css'>
 </head>
 <body>
@@ -79,6 +80,7 @@
 <script type='text/javascript' src='view-electron.js'></script>
 <script type='text/javascript' src='view-render.js'></script>
 <script type='text/javascript' src='view-node.js'></script>
+<script type='text/javascript' src='view-find.js'></script>
 <script type='text/javascript' src='view.js'></script>
 </body>
 </html>

+ 9 - 0
src/view-electron.js

@@ -40,9 +40,18 @@ class ElectronHost {
         electron.ipcRenderer.on('export', (event, data) => {
             this._view.export(data.file);
         });
+        electron.ipcRenderer.on('cut', (event, data) => {
+            this._view.cut();
+        });
         electron.ipcRenderer.on('copy', (event, data) => {
             this._view.copy();
         });
+        electron.ipcRenderer.on('paste', (event, data) => {
+            this._view.paste();
+        });
+        electron.ipcRenderer.on('selectall', (event, data) => {
+            this._view.selectAll();
+        });
         electron.ipcRenderer.on('toggle-details', (event, data) => {
             this._view.toggleDetails();
             this.update('show-details', this._view.showDetails);

+ 39 - 0
src/view-find.css

@@ -0,0 +1,39 @@
+
+.find input[type=text] {
+    font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf;
+    font-size: 12px;
+    padding: 4px 6px 4px 6px;
+    background: #fff;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+    outline: 0;
+}
+
+.find ol {
+    list-style-type: none;
+    border-radius: 4px;
+    border: 1px solid #ccc;
+    overflow-y: auto;
+    margin: 8px 0 20px 0;
+    padding: 0;
+}
+
+.find li {
+    font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace; 
+    font-size: 10px;
+    margin: 0;
+    padding: 5px 8px 5px 8px;
+    outline: 0;
+    white-space: nowrap;
+    user-select: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
+}
+
+.find li:not(:first-child) {
+    border-top: 1px solid #f0f0f0;
+}
+
+.find li:hover {
+    background: #eee;
+}

+ 131 - 0
src/view-find.js

@@ -0,0 +1,131 @@
+/*jshint esversion: 6 */
+
+class FindView {
+
+    constructor(graphElement, graph) {
+        this._graphElement = graphElement;
+        this._graph = graph;
+        this._contentElement = document.createElement('div');
+        this._contentElement.setAttribute('class', 'find');
+        this._searchElement = document.createElement('input');
+        this._searchElement.setAttribute('id', 'search');
+        this._searchElement.setAttribute('type', 'text');
+        this._searchElement.setAttribute('placeholder', 'Search...');
+        this._searchElement.setAttribute('style', 'width: 100%');
+        this._searchElement.addEventListener('input', (e) => {
+            this.update(e.target.value);
+            this.raise('search-text-changed', e.target.value);
+        });
+        this._resultElement = document.createElement('ol');
+        this._resultElement.addEventListener('click', (e) => {
+            this.select(e);
+        });
+        this._contentElement.appendChild(this._searchElement);
+        this._contentElement.appendChild(this._resultElement);
+    }
+
+    on(event, callback) {
+        this._events = this._events || {};
+        this._events[event] = this._events[event] || [];
+        this._events[event].push(callback);
+    }
+
+    raise(event, data) {
+        if (this._events && this._events[event]) {
+            this._events[event].forEach((callback) => {
+                callback(this, data);
+            });
+        }
+    }
+
+    select(e) {
+        var selection = [];
+        var id = e.target.id;
+
+        var nodesElement = this._graphElement.getElementById('nodes');
+        var nodeElement = nodesElement.firstChild;
+        while (nodeElement) { 
+            if (nodeElement.id == id) {
+                selection.push(nodeElement);
+            }
+            nodeElement = nodeElement.nextSibling;
+        }
+
+        var edgePathsElement = this._graphElement.getElementById('edge-paths');
+        var edgePathElement = edgePathsElement.firstChild; 
+        while (edgePathElement) {
+            if (edgePathElement.id == id) {
+                selection.push(edgePathElement);
+            }
+            edgePathElement = edgePathElement.nextSibling;
+        }
+
+        if (selection.length > 0) {
+            this.raise('select', selection);
+        }
+    }
+
+    focus(searchText) {
+        this._searchElement.focus();
+        this._searchElement.value = '';
+        this._searchElement.value = searchText;
+        this.update(searchText);
+    }
+
+    update(searchText) {
+        while (this._resultElement.lastChild) {
+            this._resultElement.removeChild(this._resultElement.lastChild);
+        }
+
+        var text = searchText.toLowerCase();
+
+        var nodeMatches = {};
+        var edgeMatches = {};
+
+        this._graph.nodes.forEach((node) => {
+            var name = node.name;
+            if (name && name.toLowerCase().indexOf(text) != -1 && !nodeMatches[name]) {
+                var item = document.createElement('li');
+                item.innerText = '\u25A2 ' + node.name;
+                item.id = 'node-' + node.name;
+                this._resultElement.appendChild(item);
+                nodeMatches[node.name] = true;
+            }
+
+            node.inputs.forEach((input) => {
+                input.connections.forEach((connection) => {
+                    if (connection.id && connection.id.toLowerCase().indexOf(text) != -1 && !edgeMatches[connection.id]) {
+                        var item = document.createElement('li');
+                        if (!connection.initializer) {
+                            item.innerText = '\u2192 ' + connection.id.split('@').shift();
+                            item.id = 'edge-' + connection.id;
+                            this._resultElement.appendChild(item);
+                            edgeMatches[connection.id] = true;
+                        }
+                    }    
+                });
+            });
+        });
+
+        this._graph.nodes.forEach((node) => {
+            node.outputs.forEach((output) => {
+                output.connections.forEach((connection) => {
+                    if (connection.id && connection.id.toLowerCase().indexOf(text) != -1 && !edgeMatches[connection.id]) {
+                        var item = document.createElement('li');
+                        item.innerText = '\u2192 ' + connection.id.split('@').shift();
+                        item.id = 'edge-' + connection.id;
+                        this._resultElement.appendChild(item);
+                        edgeMatches[connection.id] = true;
+                    }    
+                });
+            });
+        });
+
+        this._resultElement.style.display = this._resultElement.childNodes.length != 0 ? 'block' : 'none';
+    }
+    
+    get content() {
+        return this._contentElement;
+    }
+
+}

+ 12 - 9
src/view-node.js

@@ -129,6 +129,7 @@ class NodeViewItem {
         inputName.setAttribute('type', 'text');
         inputName.setAttribute('value', name);
         inputName.setAttribute('title', name);
+        inputName.setAttribute('readonly', 'true');
         itemName.appendChild(inputName);
 
         var itemValueList = document.createElement('div');
@@ -271,20 +272,22 @@ class NodeViewItemConnection {
             this._element.appendChild(this._expander);
         }
 
-        this._hasId = this._connection.id ? true : false;
-        if (this._hasId) {
-            var idLine = document.createElement('div');
-            idLine.className = 'node-view-item-value-line';
-            var id = this._connection.id.split('@').shift();
-            idLine.innerHTML = '<span class=\'node-view-item-value-line-content\'>id: <b>' + id + '</b></span>';
-            this._element.appendChild(idLine);
-        }
-        else if (initializer) {
+        var id = this._connection.id || '';
+        this._hasId = id ? true : false;
+        if (initializer && !this._hasId) {
             var kindLine = document.createElement('div');
             kindLine.className = 'node-view-item-value-line';
             kindLine.innerHTML = 'kind: <b>' + initializer.kind + '</b>';
             this._element.appendChild(kindLine);
         }
+        else {
+            var idLine = document.createElement('div');
+            idLine.className = 'node-view-item-value-line';
+            id = this._connection.id.split('@').shift();
+            id = id || ' ';
+            idLine.innerHTML = '<span class=\'node-view-item-value-line-content\'>id: <b>' + id + '</b></span>';
+            this._element.appendChild(idLine);
+        }
     }
 
     get element() {

+ 6 - 1
src/view-render.css

@@ -61,4 +61,9 @@
 .edge-label text { font-family: 'Open Sans', --apple-system, "Helvetica Neue", Helvetica, Arial, sans-serf; font-size: 10px; }
 .edge-path { stroke: #000; stroke-width: 1px; fill: none; }
 
-.cluster rect { stroke: #000; fill: #000; fill-opacity: 0.02; stroke-opacity: 0.06; stroke-width: 1px; }
+.cluster rect { stroke: #000; fill: #000; fill-opacity: 0.02; stroke-opacity: 0.06; stroke-width: 1px; }
+
+.select .node.border { stroke: #333; stroke-width: 2px; stroke-dasharray: 6px 3px; stroke-dashoffset: 0; animation: pulse 4s infinite linear; }
+.select.edge-path { stroke: #333; stroke-width: 2px; stroke-dasharray: 6px 3px; stroke-dashoffset: 0; animation: pulse 4s infinite linear; }
+
+@keyframes pulse { from { stroke-dashoffset: 100px; } to { stroke-dashoffset: 0; } }

+ 11 - 1
src/view-render.js

@@ -9,18 +9,22 @@ class GraphRenderer {
     render(graph) {
 
         var svgClusterGroup = this.createElement('g');
+        svgClusterGroup.setAttribute('id', 'clusters');
         svgClusterGroup.setAttribute('class', 'clusters');
         this._svgElement.appendChild(svgClusterGroup);
 
         var svgEdgePathGroup = this.createElement('g');
+        svgEdgePathGroup.setAttribute('id', 'edge-paths');
         svgEdgePathGroup.setAttribute('class', 'edge-paths');
         this._svgElement.appendChild(svgEdgePathGroup);
 
         var svgEdgeLabelGroup = this.createElement('g');
+        svgEdgeLabelGroup.setAttribute('id', 'edge-labels');
         svgEdgeLabelGroup.setAttribute('class', 'edge-labels');
         this._svgElement.appendChild(svgEdgeLabelGroup);
 
         var svgNodeGroup = this.createElement('g');
+        svgNodeGroup.setAttribute('id', 'nodes');
         svgNodeGroup.setAttribute('class', 'nodes');
         this._svgElement.appendChild(svgNodeGroup);
 
@@ -28,6 +32,9 @@ class GraphRenderer {
             if (graph.children(nodeId).length == 0) {
                 var node = graph.node(nodeId);
                 var element = this.createElement('g');
+                if (node.id) {
+                    element.setAttribute('id', node.id);
+                }
                 element.setAttribute('class', node.hasOwnProperty('class') ? ('node ' + node.class) : 'node');
                 element.style.setProperty('opacity', 0);
                 var container = this.createElement('g');
@@ -114,6 +121,9 @@ class GraphRenderer {
             element.setAttribute('class', edge.hasOwnProperty('class') ? ('edge-path ' + edge.class) : 'edge-path');
             element.setAttribute('d', points);
             element.setAttribute('marker-end', 'url(#arrowhead-vee)');
+            if (edge.id) {
+                element.setAttribute('id', edge.id);
+            }
             svgEdgePathGroup.appendChild(element);
         });
 
@@ -335,7 +345,7 @@ class NodeFormatter {
         if (hasAttributes) {
             root.append('line').classed('node', true).attr('x1', 0).attr('y1', itemHeight).attr('x2', maxWidth).attr('y2', itemHeight);
         }
-        var border = root.append('path').classed('node', true).attr('d', this.roundedRect(0, 0, maxWidth, itemHeight + attributesHeight, true, true, true, true));
+        var border = root.append('path').classed('node border', true).attr('d', this.roundedRect(0, 0, maxWidth, itemHeight + attributesHeight, true, true, true, true));
 
         if (this._controlDependencies) {
             border.classed('node-control-dependency', true);

+ 1 - 1
src/view-template.js

@@ -125,7 +125,7 @@ var summaryTemplate = `
 {{#if name}}
 <div class='property'>
 <div class='name'>Name</div>
-<div class='value'><input type='text' value='{{name}}'/></div>
+<div class='value'><input type='text' value='{{name}}' readonly='true'/></div>
 </div>
 {{/if}}
 {{#if version}}

+ 3 - 1
src/view.css

@@ -140,7 +140,6 @@ body {
     padding-left: 20px;
     padding-right: 20px;
     overflow-y: auto;
-    position: relative;
 }
 
 .sidebar button {
@@ -187,6 +186,9 @@ body {
 .toolbar {
     padding: 0;
     margin: 0;
+    user-select: none;
+    -webkit-user-select: none;
+    -moz-user-select: none;
 }
 
 .toolbar button {

+ 86 - 11
src/view.js

@@ -5,10 +5,12 @@ class View {
     constructor(host) {
         this._host = host;
         this._model = null;
+        this._selection = [];
         this._sidebar = new Sidebar();
         this._host.initialize(this);
         this._showDetails = true;
         this._showNames = false;
+        this._searchText = '';
         document.documentElement.style.overflow = 'hidden';
         document.body.scroll = 'no';        
         document.getElementById('model-properties-button').addEventListener('click', (e) => {
@@ -26,6 +28,9 @@ class View {
         document.getElementById('sidebar').addEventListener('mousewheel', (e) => {
             this.preventZoom(e);
         });
+        document.addEventListener('keydown', (e) => {
+            this.clearSelection();
+        });
     }
     
     show(page) {
@@ -76,12 +81,37 @@ class View {
         }
     }
 
+    cut() {
+        document.execCommand('cut');
+    }
+
     copy() {
         document.execCommand('copy');
     }
 
+    paste() {
+        document.execCommand('paste');
+    }
+
+    selectAll() {
+        document.execCommand('selectall');
+    }
+
     find() {
-        this._sidebar.open('<div></div>', 'Find');
+        if (this._activeGraph) {
+            this.clearSelection();
+            var graphElement = document.getElementById('graph');
+            var view = new FindView(graphElement, this._activeGraph);
+            view.on('search-text-changed', (sender, text) => {
+                this._searchText = text;
+            });
+            view.on('select', (sender, selection) => {
+                this._sidebar.close();
+                this.select(selection);
+            });
+            this._sidebar.open(view.content, 'Find');  
+            view.focus(this._searchText);  
+        }
     }
 
     toggleDetails() {
@@ -131,6 +161,44 @@ class View {
         }
     }
 
+    select(selection) {
+        this.clearSelection();
+        if (selection && selection.length > 0) {
+            var graphElement = document.getElementById('graph');
+            var graphRect = graphElement.getBoundingClientRect();
+            var x = 0;
+            var y = 0;
+            selection.forEach((element) => {
+                var classes = element.getAttribute('class').split(' ');
+                classes.push('select');
+                element.setAttribute('class', classes.join(' '));
+                this._selection.push(element);
+                var box = element.getBBox();
+                var ex = box.x + (box.width / 2);
+                var ey = box.y + (box.height / 2);
+                var transform = element.transform.baseVal.consolidate();
+                if (transform) {
+                    ex = transform.matrix.e;
+                    ey = transform.matrix.f;
+                }
+                x += ex;
+                y += ey;
+            });
+            x = x / selection.length;
+            y = y / selection.length;
+            this._zoom.transform(d3.select(graphElement), d3.zoomIdentity.translate((graphRect.width / 2) - x, (graphRect.height / 2) - y));        
+        }
+    }
+
+    clearSelection() {
+        while (this._selection.length > 0) {
+            var element = this._selection.pop();
+            var classes = element.getAttribute('class').split(' ');
+            classes = classes.filter((className) => className != 'select');
+            element.setAttribute('class', classes.join(' '));
+        }
+    }
+
     loadBuffer(buffer, identifier, callback) {
         var modelFactoryRegistry = [
             new OnnxModelFactory(),
@@ -276,6 +344,7 @@ class View {
         }
 
         graph.nodes.forEach((node) => {
+
             var formatter = new NodeFormatter();
     
             function addOperator(view, formatter, node) {
@@ -407,8 +476,14 @@ class View {
                     });
                 }
             }
-    
-            g.setNode(nodeId, { label: formatter.format(svgElement) });
+
+            var name = node.name;
+            if (name) {
+                g.setNode(nodeId, { id: 'node-' + name, label: formatter.format(svgElement) });
+            }
+            else {
+                g.setNode(nodeId, { label: formatter.format(svgElement) });
+            }
     
             function createCluster(name) {
                 if (!clusterMap[name]) {
@@ -479,7 +554,7 @@ class View {
     
             var formatter = new NodeFormatter();
             formatter.addItem(output.name, [ 'graph-item-output' ], output.type, () => {
-                this.showProperties();
+                this.showModelProperties();
             });
             g.setNode(nodeId++, { label: formatter.format(svgElement) } ); 
         });
@@ -507,10 +582,10 @@ class View {
                     }
 
                     if (to.dependency) { 
-                        g.setEdge(tuple.from.node, to.node, { label: text, arrowhead: 'vee', curve: d3.curveBasis, class: 'edge-path-control' } );
+                        g.setEdge(tuple.from.node, to.node, { label: text, id: 'edge-' + edge, arrowhead: 'vee', curve: d3.curveBasis, class: 'edge-path-control' } );
                     }
                     else {
-                        g.setEdge(tuple.from.node, to.node, { label: text, arrowhead: 'vee', curve: d3.curveBasis } );
+                        g.setEdge(tuple.from.node, to.node, { label: text, id: 'edge-' + edge, arrowhead: 'vee', curve: d3.curveBasis } );
                     }
                 });
             }
@@ -537,15 +612,15 @@ class View {
             d3.select(originElement).attr('transform', d3.event.transform);
         });
         var svg = d3.select(svgElement);
-        svg.call(this._zoom);
-        svg.call(this._zoom.transform, d3.zoomIdentity);
+        this._zoom(svg);
+        this._zoom.transform(svg, d3.zoomIdentity);
         this._svg = svg;
     
         setTimeout(() => {
     
             var graphRenderer = new GraphRenderer(originElement);
             graphRenderer.render(g);
-    
+
             var svgSize = svgElement.getBoundingClientRect();
     
             var inputElements = svgElement.getElementsByClassName('graph-input');
@@ -561,10 +636,10 @@ class View {
                 x = x / inputElements.length;
                 y = y / inputElements.length;
     
-                svg.call(this._zoom.transform, d3.zoomIdentity.translate((svgSize.width / 2) - x, (svgSize.height / 4) - y));
+                this._zoom.transform(svg, d3.zoomIdentity.translate((svgSize.width / 2) - x, (svgSize.height / 4) - y));
             }
             else {
-                svg.call(this._zoom.transform, d3.zoomIdentity.translate((svgSize.width - g.graph().width) / 2, (svgSize.height - g.graph().height) / 2));
+                this._zoom.transform(svg, d3.zoomIdentity.translate((svgSize.width - g.graph().width) / 2, (svgSize.height - g.graph().height) / 2));
             }    
         
             this.show('graph');