drawio-offline
Version:
diagrams.net desktop
1,397 lines (1,191 loc) • 120 kB
JavaScript
/**
* 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);