UNPKG

drawio-offline

Version:
1,397 lines (1,191 loc) 120 kB
/** * Copyright (c) 2006-2015, JGraph Ltd * Copyright (c) 2006-2015, Gaudenz Alder */ /** * Voice plugin for draw.io * * Documentation: * * https://www.diagrams.net/doc/faq/voice-plugin * * TODO: Use grammer https://msdn.microsoft.com/en-us/library/ee800145.aspx */ Draw.loadPlugin(function(ui) { // Speech recognition never supported without synthesis if (!('speechSynthesis' in window)) { ui.showError('Error', 'Speech output not supported in this browser.', 'OK'); return; } else { // Triggers loading of voices speechSynthesis.getVoices(); } // Do no use in chromeless mode if (ui.editor.isChromelessView()) { return; } // Mic PNG image var outputImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyRpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDpBNDU1RDkxODcxREIxMUU0OTU3Qjg3REYyOTYxQzc0QiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDpBNDU1RDkxOTcxREIxMUU0OTU3Qjg3REYyOTYxQzc0QiI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkE0NTVEOTE2NzFEQjExRTQ5NTdCODdERjI5NjFDNzRCIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOkE0NTVEOTE3NzFEQjExRTQ5NTdCODdERjI5NjFDNzRCIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+QsVUnQAAAX5JREFUeNqck00oRFEUx+eNyYSFUj7KgpfPBUlJWZgFw3IWihpRFpZia4e1hZSlNDsLopSNfBTFQqSwkKmZ5SA2ylfq+Z3pXL15ehlu/fqf9849555777mW4ziBfEZ9rd2InEEsmU4dmP/WHxKEkT1oglaSZOR/0DvRssIVUOQKjEASsxoGoQDmjT/oDUaOdRUzbkASrsMdzMIwSe2cBASXIjtQpyvHYQvzGeLQDkOwDG8w8p2A4GJEJre5Vk5DFBbZ7yG6D+PYL3oW0WwCgh/Re4i4t8PEE2QOxqikDN2AbuwQKr4WU4E4S3wOfw1CWtktFEI5ZDTu5y34DMvPIQl24dTHL9f2CRfQAB/wAFXwlE3gOO990Im95GmcLmQGEpyHTB6AI2xJKL4r7xYmYdX1betpT0kzoT1yhdhyY71aeW4rcyNySJswTVWXWkklcq5N1AETsCAuqkn9+hZ09RXoh1e4hm2CR//7mJqlB8xjCgXyHzXaDzETLONLgAEAxwd5e6Mz+S4AAAAASUVORK5CYII='; var micImage = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA5UlEQVR4Xr3SMYrCQBTGcSfIQiAgRIS9hOANBCurPUAuIAp7A7FVsFkIbLGYA9gKtoKNYG3jll5AFNKG518YMD6SISD4wY9J4MvkMYwRkZqOMSZkifGFe1b4pnvW3TqK8oMo14twxUgXPRSlDxU7TcUNPqATlG7wCi93cA2Iq2x7l7IJsgofB6UTiEjKklFqsabQSdFA5jqDAzrYQGeNNv5d9yDBEAME6NreFmP8Yuma4A8hFpiLSFNAYYYYn0jwCIUnxMcER4h1whS+7hseXKcu9ifGeQ+qeO8GjN7DPve+Q6+oewPhmE63Qfsb6AAAAABJRU5ErkJggg=='; // True if we're on ChromOs var chromeOs = mxClient.IS_CHROMEOS; // Maximum length of message to speak var maxMessageLength = 1000; // Maximum length of the label before the cell // is called by its shapename var maxLabelLength = 15; // Maximum length of output queue. var maxQueueLength = 3; // Specifies if speech output is enabled. var speechOutputEnabled = true; // Specifies if speech output is enabled. var speechInputEnabled = true; // Last message is never repeated var lastMessage = null; // Timestamp of last message var lastMessageTimestamp = null; // Sets global recognition language var lang = 'en-US'; // Caches action names var actions = null; var actionList = null; // Caches shape names var shapeList = null; // Last inserted cell var lastInserted = null; // Current voice var currentVoice = 10; // Current recognition thread var recognizing = null; // Adds menu mxResources.parse('voiceType=Voice Type'); mxResources.parse('speechOutput=Speech Output'); mxResources.parse('speechListen=Listen'); mxResources.parse('speechInstalled=Start with draw.io'); mxResources.parse('speechListenContinuous=Start/Stop Listen'); mxResources.parse('speechHint=Hint'); mxResources.parse('speechHelp=Help'); mxResources.parse('speechQuit=Quit'); // Installs footer click handler function getOrCreateVoiceButton(ui) { if (ui.voiceButton == null) { ui.voiceButton = document.createElement('div'); ui.voiceButton.className = 'geBtn'; ui.voiceButton.style.width = '140px'; ui.voiceButton.style.minWidth = '140px'; ui.voiceButton.style.textOverflow = 'ellipsis'; ui.voiceButton.style.overflowX = 'hidden'; ui.voiceButton.style.fontWeight = 'bold'; ui.voiceButton.style.textAlign = 'center'; ui.voiceButton.style.display = 'inline-block'; ui.voiceButton.style.padding = '0 10px 0 10px'; ui.voiceButton.style.marginTop = '-4px'; ui.voiceButton.style.height = '28px'; ui.voiceButton.style.lineHeight = '28px'; ui.voiceButton.style.color = '#235695'; if (ui.buttonContainer.firstChild != null) { ui.buttonContainer.insertBefore(ui.voiceButton, ui.buttonContainer.firstChild); } else { ui.buttonContainer.appendChild(ui.voiceButton); } } return ui.voiceButton; }; var td = getOrCreateVoiceButton(ui); if (td != null) { mxEvent.addGestureListeners(td, function(evt) { ui.editor.graph.popupMenuHandler.hideMenu(); if (ui.menubar == null && mxEvent.isPopupTrigger(evt)) { ui.editor.graph.popupMenuHandler.hideMenu(); var menu = new mxPopupMenu(ui.menus.get('voice').funct); menu.div.className += ' geMenubarMenu'; menu.smartSeparators = true; menu.showDisabled = true; menu.autoExpand = true; // Disables autoexpand and destroys menu when hidden menu.hideMenu = mxUtils.bind(this, function() { mxPopupMenu.prototype.hideMenu.apply(menu, arguments); menu.destroy(); }); var offset = mxUtils.getOffset(td); menu.popup(offset.x, offset.y + td.offsetHeight, null, evt); // Allows hiding by clicking on document ui.setCurrentMenu(menu); mxEvent.consume(evt); } }, null, function(evt) { if (!mxEvent.isPopupTrigger(evt)) { if (speechSynthesis.speaking) { speechSynthesis.cancel(); } App.listen(true); } mxEvent.consume(evt); }); mxEvent.disableContextMenu(td); } function setPluginInstalled(value) { if (mxSettings != null) { var plugins = mxSettings.getPlugins(); var installed = mxUtils.indexOf(plugins, '/plugins/voice.js') >= 0; if (value != installed) { if (installed) { mxUtils.remove('/plugins/voice.js', plugins); } else { plugins.push('/plugins/voice.js'); } mxSettings.setPlugins(plugins); mxSettings.save(); } } }; // Shows initial status message if only output is enable if (!('webkitSpeechRecognition' in window)) { if (td != null) { td.innerHTML = '<img style="margin-right:4px;" align="absmiddle" border="0" src="' + outputImage + '"/> Ready'; td.style.color = '#235695'; } } function updateStatusMessage() { if ('webkitSpeechRecognition' in window) { if (td != null) { if (recognizing != null) { td.innerHTML = '<img style="margin-right:4px;" align="absmiddle" border="0" src="' + micImage + '"/> Listening...'; td.setAttribute('title', 'Click to Stop (' + Editor.ctrlKey + '+O)'); td.style.color = 'darkGray'; } else { td.innerHTML = '<img style="margin-right:4px;" align="absmiddle" border="0" src="' + micImage + '"/> Click to Speak'; td.setAttribute('title', 'Click to Speak (' + Editor.ctrlKey + '+O)'); td.style.color = '#235695'; } } } }; updateStatusMessage(); var action = ui.actions.addAction('speechOutput', function() { speechOutputEnabled = !speechOutputEnabled; }, null, null, 'Ctrl/AltGr+Shift+Esc'); action.setToggleAction(true); action.setSelectedCallback(function() { return speechOutputEnabled; }); var action = ui.actions.addAction('speechInstalled', function() { setPluginInstalled(); }); action.setToggleAction(true); action.setSelectedCallback(function() { return mxUtils.indexOf(mxSettings.getPlugins(), '/plugins/voice.js') >= 0; }); ui.actions.addAction('speechListen', function() { App.listen(); }, null, null, 'Ctrl/AltGr+Esc'); ui.actions.addAction('speechListenContinuous', function() { App.listen(true); }, null, null, Editor.ctrlKey + '+O'); ui.actions.addAction('speechHint', function() { App.sayHint(); }, null, null, 'Shift+Esc'); ui.actions.addAction('speechHelp', function() { window.open('https://www.diagrams.net/doc/faq/voice-plugin'); }); // Hijacks the settings for storing current voice if (mxSettings != null) { var tmp = mxSettings.settings.voice; if (tmp != null) { currentVoice = parseInt(tmp); } } ui.menus.put('voiceType', new Menu(mxUtils.bind(this, function(menu, parent) { var voices = speechSynthesis.getVoices(); if (voices.length == 0) { menu.addItem('Loading...', null, function() {}, parent, null, false); } else { for (var i = 0; i < voices.length; i++) { (function(index) { var item = menu.addItem(voices[index].name + ' (' + voices[i].lang + ')', null, function() { currentVoice = index; App.say('hello'); if (mxSettings != null) { mxSettings.settings.voice = currentVoice; mxSettings.save(); } }, parent); if (index == currentVoice) { menu.addCheckmark(item, Editor.checkmarkImage); } })(i); } parent.div.style.overflowX = 'hidden'; parent.div.style.overflowY = 'auto'; parent.div.style.maxHeight = '100%'; // Workaround for document scrollbars with 100% max height in Chrome parent.div.style.marginBottom = '-20px'; } }))); ui.actions.addAction('speechQuit', function() { // Hides UI speechOutputEnabled = false; td.style.display = 'none'; if (menu != null) { menu.style.display = 'none'; } }); ui.menus.put('voice', new Menu(function(menu, parent) { ui.menus.addSubmenu('voiceType', menu, parent); ui.menus.addMenuItems(menu, ['-', 'speechOutput', 'speechHint', '-', 'speechListen', 'speechListenContinuous', '-', 'speechInstalled', 'speechHelp', '-', 'speechQuit']); })); if (ui.menubar != null) { var menu = ui.menubar.addMenu('Voice', ui.menus.get('voice').funct); // Inserts voice menu before help menu menu.parentNode.insertBefore(menu, menu.previousSibling.previousSibling.previousSibling); } function insertShape(shape, done) { var searchTerm = mxUtils.trim(shape); ui.sidebar.searchEntries(searchTerm, 1, 0, function(results, len, more) { if (results.length > 0) { var elt = results[0](); // Click is blocked, must use mousedown/-up sequence // LATER: Use touchstart or pointerEvents depending on system dispatchEvent(elt, mouseEvent('mousedown', 1, 50, 1, 50)); dispatchEvent(document.body, mouseEvent('mouseup', 1, 50, 1, 50)); } else { App.say('{1} not found', [searchTerm]); } if (done != null) { done(); } }); }; // http://stackoverflow.com/questions/11919065/sort-an-array-by-the-levenshtein-distance-with-best-performance-in-javascript //http://www.merriampark.com/ld.htm, http://www.mgilleland.com/ld/ldjavascript.htm, Damerau–Levenshtein distance (Wikipedia) var levenshteinDist = function(s, t) { var d = []; //2d matrix // Step 1 var n = s.length; var m = t.length; if (n == 0) return m; if (m == 0) return n; //Create an array of arrays in javascript (a descending loop is quicker) for (var i = n; i >= 0; i--) d[i] = []; // Step 2 for (var i = n; i >= 0; i--) d[i][0] = i; for (var j = m; j >= 0; j--) d[0][j] = j; // Step 3 for (var i = 1; i <= n; i++) { var s_i = s.charAt(i - 1); // Step 4 for (var j = 1; j <= m; j++) { //Check the jagged ld total so far if (i == j && d[i][j] > 4) return n; var t_j = t.charAt(j - 1); var cost = (s_i == t_j) ? 0 : 1; // Step 5 //Calculate the minimum var mi = d[i - 1][j] + 1; var b = d[i][j - 1] + 1; var c = d[i - 1][j - 1] + cost; if (b < mi) mi = b; if (c < mi) mi = c; d[i][j] = mi; // Step 6 //Damerau transposition if (i > 1 && j > 1 && s_i == t.charAt(j - 2) && s.charAt(i - 2) == t_j) { d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + cost); } } } // Step 7 return d[n][m]; } function naiveHammingDistance(str1, str2) { var dist = 0; str1 = str1.toLowerCase(); str2 = str2.toLowerCase(); for(var i = 0; i < str1.length; i++) { if (str2[i] && str2[i] !== str1[i]) { dist += Math.abs(str1.charCodeAt(i) - str2.charCodeAt(i)) + Math.abs(str2.indexOf( str1[i] )) * 2; } else if (!str2[i]) { // If there's no letter in the comparing string dist += dist; } } return dist; }; function getBestWord(str1, words, useLevenshteinDist) { if (words == null || words.length == 0) { return str1; } useLevenshteinDist = (useLevenshteinDist != null) ? useLevenshteinDist : true; var bestWord = words[0]; var minDist = ((useLevenshteinDist) ? levenshteinDist(str1, bestWord) : naiveHammingDistance(str1, bestWord)); for (var i = 1; i < words.length; i++) { var tmp = ((useLevenshteinDist) ? levenshteinDist(str1, words[i]) : ((str1 == words[i]) ? 0 : naiveHammingDistance(str1, words[i]))); if (tmp < minDist || (tmp == minDist && str1.length > bestWord.length && bestWord.length < words[i].length)) { bestWord = words[i]; minDist = tmp; } if (bestWord == str1) { break; } } return bestWord; } function mouseEvent(type, sx, sy, cx, cy, shift) { var evt; var e = { bubbles: true, cancelable: (type != "mousemove"), view: window, detail: 0, screenX: sx, screenY: sy, clientX: cx, clientY: cy, ctrlKey: false, altKey: false, shiftKey: (shift != null) ? shift : false, metaKey: false, button: 0, relatedTarget: undefined }; if (typeof( document.createEvent ) == "function") { evt = document.createEvent("MouseEvents"); evt.initMouseEvent(type, e.bubbles, e.cancelable, e.view, e.detail, e.screenX, e.screenY, e.clientX, e.clientY, e.ctrlKey, e.altKey, e.shiftKey, e.metaKey, e.button, document.body.parentNode); } else if (document.createEventObject) { evt = document.createEventObject(); for (prop in e) { evt[prop] = e[prop]; } evt.button = { 0:1, 1:4, 2:2 }[evt.button] || evt.button; } return evt; }; function dispatchEvent (el, evt) { if (el.dispatchEvent) { el.dispatchEvent(evt); } else if (el.fireEvent) { el.fireEvent('on' + type, evt); } return evt; }; var keyHandlerEscape = ui.keyHandler.escape; ui.keyHandler.escape = function(evt) { if ((!mxClient.IS_MAC && mxEvent.isAltDown(evt)) || ((mxClient.IS_MAC || chromeOs) && mxEvent.isControlDown(evt))) { if (speechOutputEnabled) { App.say('Speech output disabled'); } speechOutputEnabled = !speechOutputEnabled; if (speechOutputEnabled) { App.say('Speech output enabled'); } mxEvent.consume(evt); } else if (mxEvent.isShiftDown(evt)) { App.sayHint(); mxEvent.consume(evt); } else { keyHandlerEscape.apply(this, arguments); } }; ui.keyHandler.bindAction(32, true, 'speechListen'); // Ctrl+SPACE ui.keyHandler.bindAction(79, true, 'speechListenContinuous'); // Ctrl+O /** * Plays a beep. */ var beep = new Audio('data:audio/wav;base64,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qJrx4KOQjahZxWbcZgztj2c49nKmkId44S71j0c8eV9yDK6uPRzx5X18eDvjvQ6yKo9ZSS6l//8elePK/Lf//IInrOF/FvDoADYAGBMGb7FtErm5MXMlmPAJQVgWta7Zx2go+8xJ0UiCb8LHHdftWyLJE0QIAIsI+UbXu67dZMjmgDGCGl1H+vpF4NSDckSIkk7Vd+sxEhBQMRU8j/12UIRhzSaUdQ+rQU5kGeFxm+hb1oh6pWWmv3uvmReDl0UnvtapVaIzo1jZbf/pD6ElLqSX+rUmOQNpJFa/r+sa4e/pBlAABoAAAAA3CUgShLdGIxsY7AUABPRrgCABdDuQ5GC7DqPQCgbbJUAoRSUj+NIEig0YfyWUho1VBBBA//uQZB4ABZx5zfMakeAAAAmwAAAAF5F3P0w9GtAAACfAAAAAwLhMDmAYWMgVEG1U0FIGCBgXBXAtfMH10000EEEEEECUBYln03TTTdNBDZopopYvrTTdNa325mImNg3TTPV9q3pmY0xoO6bv3r00y+IDGid/9aaaZTGMuj9mpu9Mpio1dXrr5HERTZSmqU36A3CumzN/9Robv/Xx4v9ijkSRSNLQhAWumap82WRSBUqXStV/YcS+XVLnSS+WLDroqArFkMEsAS+eWmrUzrO0oEmE40RlMZ5+ODIkAyKAGUwZ3mVKmcamcJnMW26MRPgUw6j+LkhyHGVGYjSUUKNpuJUQoOIAyDvEyG8S5yfK6dhZc0Tx1KI/gviKL6qvvFs1+bWtaz58uUNnryq6kt5RzOCkPWlVqVX2a/EEBUdU1KrXLf40GoiiFXK///qpoiDXrOgqDR38JB0bw7SoL+ZB9o1RCkQjQ2CBYZKd/+VJxZRRZlqSkKiws0WFxUyCwsKiMy7hUVFhIaCrNQsKkTIsLivwKKigsj8XYlwt/WKi2N4d//uQRCSAAjURNIHpMZBGYiaQPSYyAAABLAAAAAAAACWAAAAApUF/Mg+0aohSIRobBAsMlO//Kk4soosy1JSFRYWaLC4qZBYWFRGZdwqKiwkNBVmoWFSJkWFxX4FFRQWR+LsS4W/rFRb/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////VEFHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU291bmRib3kuZGUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjAwNGh0dHA6Ly93d3cuc291bmRib3kuZGUAAAAAAAAAACU='); var beep2 = new Audio('data:audio/wav;base64,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'); App.beep = function(wav) { wav = (wav != null) ? wav : beep; wav.play(); }; // Thread to reset the label var resetStatus = null; /** * * Static method for speech output. */ App.say = function(message, params) { if ('speechSynthesis' in window && message != null) { var text = mxResources.replacePlaceholders(message, params || []); lastMessageTimestamp = null; lastMessage = text; if (text != null && text.length > 0) { if (td != null) { var tmp = text; // Capitalize string if (tmp != null && tmp.length > 1) { tmp = tmp.charAt(0).toUpperCase() + tmp.slice(1); } td.innerHTML = ((speechOutputEnabled) ? '<img style="margin-right:4px;" align="absmiddle" border="0" src="' + outputImage + '"/>' : '') + ' ' + tmp; td.style.color = '#235695'; if (resetStatus != null) { window.clearTimeout(resetStatus); resetStatus = null; } resetStatus = window.setTimeout(function() { updateStatusMessage(); }, (recognizing != null) ? 1000 : 3000); } // Workaround for talking too much if (speechOutputEnabled && (!speechSynthesis.speaking || !speechSynthesis.pending)) { if (text.length < maxMessageLength) { var msg = new SpeechSynthesisUtterance(); // Picks random voice with same main locale var voices = speechSynthesis.getVoices(); // Say "again" for same last message except more than 10 secs ago or shorter than again if (lastMessageTimestamp != null && text == lastMessage && text != null && text.length > 5 && lastMessage != 'again') { if (lastMessageTimestamp != null && new Date().getTime() - lastMessageTimestamp.getTime() < 10000) { text = 'repeat'; } } msg.voice = voices[currentVoice]; msg.voiceURI = 'native'; //msg.lang = lang; msg.text = text; console.log('App.say speak:', msg.text); speechSynthesis.speak(msg); lastMessageTimestamp = new Date(); } else { console.log('App.say ignored:', text); } } else { console.log('App.say skipped:', message); } } } }; /** * Static method for speech output. */ App.listen = function(continuous) { if ('webkitSpeechRecognition' in window && speechInputEnabled) { if (recognizing != null) { recognizing.stop(); } else { var recognition = new webkitSpeechRecognition(); recognition.interimResults = true; // TODO: Should use grammar instead of trying more alternatives recognition.maxAlternatives = 5; recognition.lang = lang; if (continuous != null) { recognition.continuous = continuous; } recognition.onstart = function(event) { updateStatusMessage(); App.beep(); }; recognition.onresult = function(event) { for (var i = event.resultIndex; i < event.results.length; ++i) { if (td != null) { td.innerHTML = '<img style="margin-right:4px;" align="absmiddle" border="0" src="' + micImage + '"/> ' + event.results[i][0].transcript; td.style.color = (event.results[i].isFinal) ? '#235695' : 'darkGray'; } if (event.results[i].isFinal) { var ok = false; for (var j = 0; j < event.results[i].length; j++) { if (App.executeVoiceCommand(event.results[i][j].transcript, recognition)) { ok = true; break; } } if (!ok) { App.say('{1} not found', [event.results[i][0].transcript]); } if (td != null) { if (resetStatus != null) { window.clearTimeout(resetStatus); resetStatus = null; } resetStatus = window.setTimeout(function() { updateStatusMessage(); }, 1000); } } } }; recognition.onend = function(event) { // Overrides footer recognizing = null; updateStatusMessage(); App.beep(beep2); }; recognition.start(); recognizing = recognition; } } else { speechOutputEnabled = !speechOutputEnabled; lastMessageTimestamp = null; App.say(lastMessage || 'Ready'); lastMessageTimestamp = null; } }; /** * Executes the given voice command. */ App.executeVoiceCommand = function(command, recognition) { console.log('App.execute:', mxUtils.trim(command)); var tokens = mxUtils.trim(command).split(' '); if (tokens.length > 0 && graph.isEnabled()) { // Ask for Mic permissions // FIXME: Dialog seems to be hidden until tab is changed function resolveStylename(token, styles) { var tmp = token.toLowerCase().replace(/ /g, ''); var style = null; for (var i = 0; i < styles.length; i++) { if (styles[i].toLowerCase() == tmp) { style = styles[i]; break; } } return style; }; // Main command tokens[0] = tokens[0].toLowerCase(); // TODO: Use hamming distance for best match command but include all possible actions // which might be too slow // console.log('connect', naiveHammingDistance(tokens[0], 'connect'), naiveHammingDistance('disable', 'connect'), naiveHammingDistance('change', 'connect')); if (graph.isEditing()) { if (tokens.length == 1 && tokens[0] == 'apply') { graph.stopEditing(); } else if (tokens.length == 1 && tokens[0] == 'undo') { document.execCommand('undo', false, null); } else if (tokens.length == 1 && tokens[0] == 'redo') { document.execCommand('redo', false, null); } else { document.execCommand('insertHTML', false, command); } return true; } else if (tokens[0] == 'edit' && tokens[1] == 'text') { var cells = graph.getSelectionCells(); if (cells.length == 1) { graph.startEditingAtCell(cells[0]); } return true; } else if (tokens[0] == 'hello' || tokens[0] == 'hi') { App.say('Hello! Try "Help", "Help Topic" or "Quick Start".'); return true; } else if (tokens[0] == 'help') { var wnd = ui.openLink('https://www.diagrams.net/doc/faq/voice-plugin'); if (wnd == null) { App.say('Popup blocked'); } else if (tokens.length > 1) { // Just used to check if popup windows are allowed wnd.close(); var searchTerm = mxUtils.trim(command.substring(tokens[0].length)); if (searchTerm != null && searchTerm.length > 0) { ui.openLink('https://www.google.com/search?q=site%3Adiagrams.net+inurl%3A%2Fdoc%2Ffaq%2F+' + encodeURIComponent(searchTerm)); App.say(command); } } else { App.say('help'); } return true; } else if ((tokens[0] == 'info' && tokens.length == 1) || mxUtils.trim(command).toLowerCase() == 'what\'s this') { App.sayHint(); return true; } else if (tokens[0] == 'install' && tokens.length == 1) { setPluginInstalled(true); App.say('Installed'); return true; } else if (tokens[0] == 'uninstall' && tokens.length == 1) { setPluginInstalled(false); App.say('Uninstalled'); return true; } else if (tokens[0] == 'quick' && tokens.length > 0 && tokens[1].toLowerCase() == 'start') { var wnd = window.open('https://youtu.be/8OaMWa4R1SE?t=1'); if (wnd == null) { App.say('Popup blocked'); } return true; } else if (tokens[0] == 'search' && tokens.length > 1) { var searchToken = tokens.slice(1, tokens.length).join(' ').toLowerCase(); var wnd = window.open('https://www.google.ch/search?q=' + encodeURIComponent(searchToken) + '&tbm=isch', 'voicePluginSearchResult'); if (wnd == null) { App.say('Popup blocked'); } return true; } else if (tokens[0] == 'shrink') { var cell = graph.getSelectionCell(); var geo = graph.getCellGeometry(cell); if (graph.getModel().isVertex(cell) && geo != null) { geo = geo.clone(); if (tokens.length == 1 || tokens[1] == 'height') { geo.height = graph.snap(Math.round(geo.height * 0.5)); } if (tokens.length == 1 || tokens[1] != 'height') { geo.width = graph.snap(Math.round(geo.width * 0.5)); } if (geo.width > graph.tolerance && geo.height > graph.tolerance) { graph.getModel().setGeometry(cell, geo); App.say('Resized'); } else { App.say('Too small'); } } else { App.say('No cell to resize'); } return true; } else if (tokens[0] == 'double') { var cell = graph.getSelectionCell(); var geo = graph.getCellGeometry(cell); if (graph.getModel().isVertex(cell) && geo != null) { geo = geo.clone(); if (tokens.length == 1 || tokens[1] == 'height') { geo.height *= 2; } if (tokens.length == 1 || tokens[1] != 'height') { geo.width *= 2; } if (geo.width > graph.tolerance && geo.height > graph.tolerance) { graph.getModel().setGeometry(cell, geo); App.say('Resized'); } else { App.say('Too small'); } } else { App.say('No cell to resize'); } return true; } else if (tokens[0] == 'width' || tokens[0] == 'with' || tokens[0] == 'height') { // Try numeric stylename if last token is numeric var lastToken = tokens[tokens.length - 1].toLowerCase(); // Fixes some special cases for recoginition of short phrases // like "stroke width 2" where it tries to be clever if (lastToken == 'tool' || lastToken == 'tune' || lastToken == 'tomb' || lastToken == 'tube') { lastToken = '2'; } else if (lastToken == 'd3') { lastToken = '3'; } else if (lastToken == 'v') { lastToken = '5'; } else { // Converts numeric words to numbers (system only seems to return written numbers for <= 4) var numbers = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine']; var tmpIndex = mxUtils.indexOf(numbers, lastToken); if (tmpIndex >= 0) { lastToken = tmpIndex; } } var lastValue = parseFloat(tokens[1].toLowerCase()); if (tokens.length >= 2 && !isNaN(lastValue)) { var cell = graph.getSelectionCell(); var geo = graph.getCellGeometry(cell); if (graph.getModel().isVertex(cell) && geo != null) { geo = geo.clone(); if (tokens[0] == 'height') { geo.height = lastValue; } else { geo.width = lastValue; } graph.getModel().setGeometry(cell, geo); App.say('Resized'); } else { App.say('No cell to resize'); } } return true; } else if (tokens[0] == 'insert' && tokens.length > 1) { // Fixes some common mistakes if (tokens[1].toLowerCase() == 'edits') { tokens[1] = 'ellipse'; } var searchTerm = mxUtils.trim(tokens.slice(1, tokens.length).join(' ')); var current = graph.getSelectionCell(); // Clears selection to disable built-in connecting graph.clearSelection(); insertShape(searchTerm, function() { var cell = graph.getSelectionCell(); // Connects dangling edge of previously selected edge and moves cell if (graph.model.isVertex(cell) && graph.model.isEdge(current) && (graph.model.getTerminal(current, true) == null || graph.model.getTerminal(current, false) == null)) { var edgeState = graph.view.getState(current); var vertexState = graph.view.getState(cell); if (vertexState != null && edgeState != null && edgeState.absolutePoints != null && edgeState.absolutePoints.length > 1) { var source = graph.model.getTerminal(current, true) == null; var pts = edgeState.absolutePoints; var pt = pts[(source) ? 0 : pts.length - 1]; var loc = new mxPoint(vertexState.getCenterX(), vertexState.getCenterY()); if (loc != null && edgeState != null) { var s = graph.view.scale; var dx = pt.x - loc.x; var dy = pt.y - loc.y; // TODO: Should add insert to transaction but need absolute position graph.model.beginUpdate(); try { graph.moveCells(graph.getSelectionCells(), dx / s, dy / s); graph.model.setTerminal(current, cell, source); } finally { graph.model.endUpdate(); } } } } }); return true; } else if (tokens[0] == 'connect' || tokens[0] == 'kinect' || (tokens[0] == 'clone' && tokens.length > 1)) { var cell = graph.getSelectionCell(); if (graph.getModel().isVertex(cell)) { // Uses east direction if token not understood var direction = mxConstants.DIRECTION_EAST; if (tokens.length > 1) { tokens[1] = tokens[1].toLowerCase(); // Guessing direction based on minimum hamming distance for given set var guess = getBestWord(tokens[1], ['up', 'left', 'down', 'right', 'north', 'south', 'east', 'west']); if (guess == 'up' || guess == mxConstants.DIRECTION_NORTH) { direction = mxConstants.DIRECTION_NORTH; } else if (guess == 'left' || guess == mxConstants.DIRECTION_WEST) { direction = mxConstants.DIRECTION_WEST; } else if (guess == 'down' || guess == mxConstants.DIRECTION_SOUTH) { direction = mxConstants.DIRECTION_SOUTH; } } var length = graph.defaultEdgeLength; var cloneSource = tokens[0] == 'clone'; var evt = mouseEvent('click', 1, 50, 1, 50, !cloneSource); var cells = graph.connectVertex(cell, direction, length, evt); graph.selectCellsForConnectVertex(cells, evt, ui.hoverIcons); } return true; } // Fixes drive and ride common mistakes for right else if (mxUtils.indexOf(['and', 'drive', 'ride', 'move', 'up', 'left', 'down', 'right', 'north', 'south', 'east', 'west', 'downright'], tokens[0]) >= 0) { var cell = graph.getSelectionCell(); if (!graph.isSelectionEmpty()) { // Uses east direction if token not understood var dx = 0; var dy = 0; for (var i = 0; i < tokens.length; i++) { tokens[i] = tokens[i].toLowerCase(); var direction = null; // Downright is a single word needs special handling if (tokens[i].toLowerCase() == 'downright') { dx += graph.defaultEdgeLength; dy += graph.defaultEdgeLength; } else { var guess = null; // Guessing direction based on minimum hamming distance for given set // Handle some common cases if (tokens[i] == 'drive' || tokens[i] == 'ride') { guess = 'right'; } else { guess = getBestWord(tokens[i], ['move', 'and', 'up', 'left', 'right', 'north', 'south', 'east', 'west', 'down', 'downright']); } if (guess == 'up' || guess == mxConstants.DIRECTION_NORTH) { direction = mxConstants.DIRECTION_NORTH; } else if (guess == 'left' || guess == mxConstants.DIRECTION_WEST) { direction = mxConstants.DIRECTION_WEST; } else if (guess == 'down' || guess == mxConstants.DIRECTION_SOUTH) { direction = mxConstants.DIRECTION_SOUTH; } else if (guess == 'right' || guess == mxConstants.DIRECTION_EAST) { direction = mxConstants.DIRECTION_EAST; } if (direction != null) { if (direction == mxConstants.DIRECTION_NORTH) { dy += -graph.defaultEdgeLength; } else if (direction == mxConstants.DIRECTION_WEST) { dx += -graph.defaultEdgeLength; } else if (direction == mxConstants.DIRECTION_SOUTH) { dy += graph.defaultEdgeLength; } else if (direction == mxConstants.DIRECTION_EAST) { dx += graph.defaultEdgeLength; } } } } if (dx != 0 || dy != null) { graph.moveCells(graph.getSelectionCells(), dx, dy); App.say('Moved'); } return true; } } else if (tokens[0] == 'text') { var cells = graph.getSelectionCells(); if (cells.length == 0 && graph.model.contains(lastInserted)) { cells = [lastInserted]; } if (cells.length == 0) { App.say('No cell for text'); } else if (tokens[0].length >= 2) { var value = tokens.slice(1, tokens.length).join(' '); if (value.length > 0) { // Capitalize string if (value.length > 1) { value = value.charAt(0).toUpperCase() + value.slice(1); } graph.labelChanged(cells[0], value); } } return true; } else if (tokens[0] == 'disconnect' && tokens.length == 1) { var cells = graph.getAllEdges(graph.getSelectionCells()); if (cells.length == 0) { App.say('No cell to disconnect'); } else { graph.removeCells(cells); } return true; } else if (tokens[0] == 'deselect' && tokens.length == 1) { graph.clearSelection(); return true; } else if (tokens[0] === 'select' && (tokens.length == 1 || mxUtils.indexOf( ['vertices', 'edges', 'none', 'all'], tokens[1].toLowerCase()) < 0)) { // Handles some other shortcuts if (tokens.length == 1 || mxUtils.indexOf(['last'], tokens[1].toLowerCase()) >= 0) { if (graph.model.contains(lastInserted)) { graph.setSelectionCell(lastInserted); App.say('{1} selected', [graph.getWordForCell(lastInserted).replace(/([A-Z])/g, ' $1')]); return true; } } // Handles alternative cases of edges else if (mxUtils.indexOf(['connection', 'connections', 'inches'], tokens[1].toLowerCase()) >= 0) { var edges = graph.getAllEdges(graph.getSelectionCells()); if (edges.length > 0) { graph.setSelectionCells(edges); } else { ui.actions.get('selectEdges').funct(); } App.sayHint(); return true; } // Handles alternative version of vertices else if (mxUtils.indexOf(['shapes'], tokens[1].toLowerCase()) >= 0) { ui.actions.get('selectVertices').funct(); App.sayHint(); return true; } else if (mxUtils.indexOf(['previous'], tokens[1].toLowerCase()) >= 0) { if (graph.isSelectionEmpty()) { // Selects the first vertex var parent = graph.getDefaultParent(); var childCount = graph.model.getChildCount(parent); for (var i = childCount - 1; i >= 0; i--) { var child = graph.model.getChildAt(parent, i); if (graph.model.isVertex(child)) { graph.setSelectionCell(child); App.say('{1} selected', [graph.getWordForCell(child).replace(/([A-Z])/g, ' $1')]); return true; } } } else { var cell = graph.getSelectionCell(); var model = graph.getModel(); var index = model.getParent(cell).getIndex(cell); var childCount = model.getChildCount(model.getParent(cell)); if (index >= 0) { var next = model.getParent(cell).getChildAt(((index == 0) ? childCount : index) - 1); if (next != null) { graph.setSelectionCell(next); App.say('{1} selected', [graph.getWordForCell(next)]); return true; } } App.say('Previous not found'); } } else if (mxUtils.indexOf(['source', 'target'], tokens[1].toLowerCase()) >= 0) { var cell = graph.getSelectionCell(); if (graph.model.isEdge(cell)) { var terminal = graph.model.getTerminal(cell, tokens[1].toLowerCase() == 'source'); if (terminal != null) { graph.setSelectionCell(terminal); return true; } } } else if (mxUtils.indexOf(['next'], tokens[1].toLowerCase()) >= 0) { if (graph.isSelectionEmpty()) { // Selects the first vertex var parent = graph.getDefaultParent(); var childCount = graph.model.getChildCount(parent); for (var i = 0; i < childCount; i++) { var child = graph.model.getChildAt(parent, i); if (graph.model.isVertex(child)) { graph.setSelectionCell(child);