mirror of
https://github.com/ijprest/keyboard-layout-editor.git
synced 2026-04-11 10:30:37 -07:00
1047 lines
37 KiB
JavaScript
1047 lines
37 KiB
JavaScript
/*jslint bitwise:true, white:true, plusplus:true, vars:true, browser:true, devel:true, regexp:true */
|
|
/*global angular:true, rison:true, $:true */
|
|
(function () {
|
|
"use strict";
|
|
|
|
// We need this so we can test locally and still save layouts to AWS
|
|
var base_href = "http://www.keyboard-layout-editor.com";
|
|
|
|
// Lenient JSON reader/writer
|
|
function toJsonL(obj) {
|
|
var res = [], key;
|
|
if(obj instanceof Array) {
|
|
obj.forEach(function(elem) { res.push(toJsonL(elem)); });
|
|
return '['+res.join(',')+']';
|
|
}
|
|
if(typeof obj === 'object') {
|
|
for(key in obj) { if(obj.hasOwnProperty(key)) { res.push(key+':'+toJsonL(obj[key])); } }
|
|
return '{'+res.join(',')+'}';
|
|
}
|
|
return angular.toJson(obj);
|
|
}
|
|
function toJsonPretty(obj) {
|
|
var res = [];
|
|
obj.forEach(function(elem) { res.push(toJsonL(elem)); });
|
|
return res.join(",\n")+"\n";
|
|
}
|
|
function fromJsonL(json) { return jsonl.parse(json); }
|
|
function fromJsonPretty(json) { return fromJsonL('['+json+']'); }
|
|
|
|
// Darken a color by 20%
|
|
function darkenColor(color) {
|
|
var num = parseInt(color.slice(1), 16),
|
|
R = ((num >> 16) & 0xff) * 0.8,
|
|
G = ((num >> 8) & 0xff) * 0.8,
|
|
B = (num & 0xFF) * 0.8;
|
|
return "#" + (0x1000000 + (((R & 0xff) << 16) + ((G & 0xff) << 8) + (B & 0xff))).toString(16).slice(1);
|
|
}
|
|
|
|
// Convert RGB values to a CSS-color string
|
|
function rgb(r, g, b) {
|
|
r = r.toString(16); while(r.length<2) { r = "0"+r; }
|
|
g = g.toString(16); while(g.length<2) { g = "0"+g; }
|
|
b = b.toString(16); while(b.length<2) { b = "0"+b; }
|
|
return "#"+r+g+b;
|
|
}
|
|
|
|
// Simple String.format() implementation
|
|
if(!String.prototype.format) {
|
|
String.prototype.format = function() {
|
|
var args = arguments;
|
|
return this.replace(/\{(\d+)\}/g, function(match, number) {
|
|
return typeof args[number] !== 'undefined' ? args[number] : match;
|
|
});
|
|
};
|
|
}
|
|
if(!String.prototype.trimStart) {
|
|
String.prototype.trimStart = function() { return this.replace(/^\s\s*/, ''); };
|
|
}
|
|
if(!String.prototype.trimEnd) {
|
|
String.prototype.trimEnd = function() { return this.replace(/\s\s*$/, ''); };
|
|
}
|
|
if(!String.prototype.trim) {
|
|
String.prototype.trim = function() { this.trimStart().trimEnd(); };
|
|
}
|
|
|
|
if(!Array.prototype.last) {
|
|
Array.prototype.last = function() {
|
|
return this[this.length-1];
|
|
}
|
|
}
|
|
|
|
function sortKeys(keys) {
|
|
keys.sort(function(a,b) { return a.y === b.y ? a.x - b.x : a.y - b.y; });
|
|
}
|
|
|
|
// Convert between our in-memory format & our serialized format
|
|
function serialize(keyboard) {
|
|
var keys = keyboard.keys;
|
|
var rows = [], row = [], xpos = 0, ypos = 0, color = "#eeeeee", text = "#000000", profile = "", ghost = false, align = 4, fontheight = 3, fontheight2 = 3;
|
|
if(keyboard.meta) {
|
|
var meta = angular.copy(keyboard.meta);
|
|
if(meta.backcolor === '#eeeeee') { delete meta.backcolor; }
|
|
if(!$.isEmptyObject(meta)) {
|
|
rows.push(meta);
|
|
}
|
|
}
|
|
sortKeys(keys);
|
|
keys.forEach(function(key) {
|
|
var props = {}, prop = false;
|
|
var label = key.labels.join("\n").trimEnd();
|
|
if(key.y !== ypos) { rows.push(row); row = []; ypos++; xpos = 0; }
|
|
function serializeProp(nname,val,defval) { if(val !== defval) { props[nname] = val; prop = true; } return val; }
|
|
ypos += serializeProp("y", key.y-ypos, 0);
|
|
xpos += serializeProp("x", key.x-xpos, 0) + key.width;
|
|
color = serializeProp("c", key.color, color);
|
|
text = serializeProp("t", key.text, text);
|
|
ghost = serializeProp("g", key.ghost, ghost);
|
|
profile = serializeProp("p", key.profile, profile);
|
|
align = serializeProp("a", key.align, align);
|
|
if(key.fontheight != fontheight) {
|
|
fontheight = serializeProp("f", key.fontheight, fontheight);
|
|
fontheight2 = serializeProp("f2", key.fontheight2, fontheight);
|
|
} else {
|
|
fontheight2 = serializeProp("f2", key.fontheight2, fontheight2);
|
|
}
|
|
serializeProp("w", key.width, 1);
|
|
serializeProp("h", key.height, 1);
|
|
serializeProp("w2", key.width2, key.width);
|
|
serializeProp("h2", key.height2, key.height);
|
|
serializeProp("x2", key.x2, 0);
|
|
serializeProp("y2", key.y2, 0);
|
|
serializeProp("n", key.nub || false, false);
|
|
serializeProp("l", key.stepped || false, false);
|
|
if(prop) { row.push(props); }
|
|
row.push(label);
|
|
});
|
|
if(row.length>0) { rows.push(row); }
|
|
return rows;
|
|
}
|
|
|
|
function deserialize(rows) {
|
|
var xpos = 0, ypos = 0, color = "#eeeeee", text = "#000000", keys = [], width=1, height=1, xpos2=0, ypos2=0, width2=0, height2=0, profile = "", r, k, nub = false, ghost = false, align = 4, fontheight = 3, fontheight2 = 3, stepped = false;
|
|
var meta = { backcolor: "#eeeeee" };
|
|
for(r = 0; r < rows.length; ++r) {
|
|
if(rows[r] instanceof Array) {
|
|
for(k = 0; k < rows[r].length; ++k) {
|
|
var key = rows[r][k];
|
|
if(typeof key === 'string') {
|
|
keys.push({x:xpos, y:ypos, width:width, height:height, profile:profile, color:color, text:text, labels:key.split('\n'), x2:xpos2, y2:ypos2, width2:width2===0?width:width2, height2:height2===0?height:height2, nub:nub, ghost:ghost, align:align, fontheight:fontheight, fontheight2:fontheight2, stepped:stepped});
|
|
xpos += width;
|
|
width = height = 1;
|
|
xpos2 = ypos2 = width2 = height2 = 0;
|
|
nub = stepped = false;
|
|
} else {
|
|
if(key.a != null) { align = key.a; }
|
|
if(key.f) { fontheight = fontheight2 = key.f; }
|
|
if(key.f2) { fontheight2 = key.f2; }
|
|
if(key.p) { profile = key.p; }
|
|
if(key.c) { color = key.c; }
|
|
if(key.t) { text = key.t; }
|
|
if(key.x) { xpos += key.x; }
|
|
if(key.y) { ypos += key.y; }
|
|
if(key.w) { width = key.w; }
|
|
if(key.h) { height = key.h; }
|
|
if(key.x2) { xpos2 = key.x2; }
|
|
if(key.y2) { ypos2 = key.y2; }
|
|
if(key.w2) { width2 = key.w2; }
|
|
if(key.h2) { height2 = key.h2; }
|
|
if(key.n) { nub = key.n; }
|
|
if(key.l) { stepped = key.l; }
|
|
if(key.g != null) { ghost = key.g; }
|
|
}
|
|
}
|
|
ypos++;
|
|
} else if(typeof rows[r] === 'object') {
|
|
$.extend(meta, rows[r]);
|
|
}
|
|
xpos = 0;
|
|
}
|
|
return { meta:meta, keys:keys };
|
|
}
|
|
|
|
// Some predefined sizes for our caps
|
|
var sizes = { cap: 54, padding: 2, margin: 6, spacing: 1 };
|
|
sizes.capsize = function(size) { return (size*sizes.cap) - (2*sizes.spacing); };
|
|
|
|
// The angular module for our application
|
|
var kbApp = angular.module('kbApp', ["ngSanitize", "ui.utils"]);
|
|
|
|
// The main application controller
|
|
kbApp.controller('kbCtrl', ['$scope','$http','$location','$timeout', '$sce', '$sanitize', function($scope, $http, $location, $timeout, $sce, $sanitize) {
|
|
var serializedTimer = false;
|
|
|
|
// The application version
|
|
$scope.version = "0.8";
|
|
|
|
// The selected tab; 0 == Properties, 1 == Kbd Properties, 2 == Raw Data
|
|
$scope.selTab = 0;
|
|
|
|
// An array used to keep track of the selected keys
|
|
$scope.selectedKeys = [];
|
|
|
|
// A single key selection; if multiple keys are selected, this is the
|
|
// most-recently selected one.
|
|
$scope.multi = {};
|
|
$scope.meta = {};
|
|
|
|
// The keyboard data
|
|
$scope.keyboard = { keys: [] };
|
|
$scope.keys = function(newKeys) { if(newKeys) { $scope.keyboard.keys = newKeys; } return $scope.keyboard.keys; };
|
|
|
|
// Helper function to select/deselect all keys
|
|
$scope.unselectAll = function() {
|
|
$scope.selectedKeys = [];
|
|
$scope.multi = {};
|
|
};
|
|
$scope.selectAll = function(event) {
|
|
if(event) { event.preventDefault(); }
|
|
sortKeys($scope.keys());
|
|
$scope.unselectAll();
|
|
$scope.keys().forEach(function(key) {
|
|
$scope.selectedKeys.push(key);
|
|
});
|
|
if($scope.keys().length>0) {
|
|
$scope.multi = angular.copy($scope.keys().last());
|
|
}
|
|
};
|
|
|
|
function saveLayout(layout) {
|
|
var data = angular.toJson(layout);
|
|
var fn = CryptoJS.MD5(data).toString();
|
|
|
|
// First test to see if the file is already available
|
|
$http.get(base_href+"/layouts/"+fn).success(function() {
|
|
$scope.dirty = false;
|
|
$scope.saved = fn;
|
|
$location.path("/layouts/"+fn);
|
|
$location.hash("");
|
|
$scope.saveError = "";
|
|
}).error(function() {
|
|
// Nope... need to upload it
|
|
var fd = new FormData();
|
|
fd.append("key", "layouts/"+fn);
|
|
fd.append("AWSAccessKeyId", "AKIAJSXGG74EMFBC57QQ");
|
|
fd.append("acl", "public-read");
|
|
fd.append("success_action_redirect", base_href);
|
|
fd.append("policy", "eyJleHBpcmF0aW9uIjoiMjAwMTQtMDEtMDFUMDA6MDA6MDBaIiwiY29uZGl0aW9ucyI6W3siYnVja2V0Ijoid3d3LmtleWJvYXJkLWxheW91dC1lZGl0b3IuY29tIn0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCJsYXlvdXRzLyJdLHsiYWNsIjoicHVibGljLXJlYWQifSx7InN1Y2Nlc3NfYWN0aW9uX3JlZGlyZWN0IjoiaHR0cDovL3d3dy5rZXlib2FyZC1sYXlvdXQtZWRpdG9yLmNvbSJ9LHsiQ29udGVudC1UeXBlIjoiYXBwbGljYXRpb24vanNvbiJ9LFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsODE5Ml1dfQ==");
|
|
fd.append("signature", "WOsX5QV/y9UlOs2kmtduXYEPeEQ=");
|
|
fd.append("Content-Type", "application/json");
|
|
fd.append("file", data);
|
|
$http.post("http://www.keyboard-layout-editor.com.s3.amazonaws.com/", fd, {
|
|
headers: {'Content-Type': undefined },
|
|
transformRequest: angular.identity
|
|
}).success(function() {
|
|
$scope.dirty = false;
|
|
$scope.saved = fn;
|
|
$location.path("/layouts/"+fn);
|
|
$location.hash("");
|
|
$scope.saveError = "";
|
|
}).error(function(data, status) {
|
|
if(status == 0) {
|
|
// We seem to get a 'cancelled' notification even though the POST
|
|
// is successful, so we have to double-check.
|
|
$http.get(base_href+"/layouts/"+fn).success(function() {
|
|
$scope.dirty = false;
|
|
$scope.saved = fn;
|
|
$location.path("/layouts/"+fn);
|
|
$location.hash("");
|
|
$scope.saveError = "";
|
|
}).error(function(data, status) {
|
|
$scope.saved = false;
|
|
$scope.saveError = status.toString() + " - " + data.toString();
|
|
});
|
|
} else {
|
|
$scope.saved = false;
|
|
$scope.saveError = status.toString() + " - " + data.toString();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
$scope.save = function(event) {
|
|
if(event) {
|
|
event.preventDefault();
|
|
}
|
|
if($scope.dirty) {
|
|
saveLayout(serialize($scope.keyboard));
|
|
}
|
|
};
|
|
$scope.canSave = function() {
|
|
return $scope.dirty;
|
|
};
|
|
|
|
// Helper function to select a single key
|
|
function selectKey(key,event) {
|
|
if(key) {
|
|
// If SHIFT is held down, we want to *extend* the selection from the last
|
|
// selected item to the new one.
|
|
if(event.shiftKey && $scope.selectedKeys.length > 0) {
|
|
// Get the indicies of all the selected keys
|
|
var currentSel = $scope.selectedKeys.map(function(key) { return $scope.keys().indexOf(key); });
|
|
currentSel.sort(function(a,b) { return parseInt(a) - parseInt(b); });
|
|
var cursor = $scope.keys().indexOf(key);
|
|
var anchor = $scope.keys().indexOf($scope.selectedKeys.last());
|
|
$scope.selectedKeys.pop();
|
|
}
|
|
|
|
// If neither CTRL or ALT is held down, clear the existing selection state
|
|
if(!event.ctrlKey && !event.altKey) {
|
|
$scope.unselectAll();
|
|
}
|
|
|
|
// SHIFT held down: toggle the selection everything between the anchor & cursor
|
|
if(anchor !== undefined && cursor !== undefined) {
|
|
if(anchor > cursor) {
|
|
for(var i = anchor; i >= cursor; --i) {
|
|
selectKey($scope.keys()[i],{ctrlKey:true});
|
|
}
|
|
} else {
|
|
for(var i = anchor; i <= cursor; ++i) {
|
|
selectKey($scope.keys()[i],{ctrlKey:true});
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Modify the selection
|
|
var ndx = $scope.selectedKeys.indexOf(key);
|
|
if(ndx >= 0) { //deselect
|
|
$scope.selectedKeys.splice(ndx,1);
|
|
if($scope.selectedKeys.length<1) {
|
|
$scope.multi = {};
|
|
} else {
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
}
|
|
} else { //select
|
|
$scope.selectedKeys.push(key);
|
|
$scope.multi = angular.copy(key);
|
|
}
|
|
}
|
|
};
|
|
|
|
// The serialized key data
|
|
$scope.serialized = "";
|
|
|
|
// Known layouts/presets
|
|
$scope.layouts = {};
|
|
$scope.samples = {};
|
|
$http.get('layouts.json').success(function(data) {
|
|
$scope.layouts = data.presets;
|
|
$scope.samples = data.samples;
|
|
});
|
|
|
|
// The currently selected palette
|
|
$scope.palette = {};
|
|
|
|
// The set of known palettes
|
|
$scope.palettes = {};
|
|
$http.get('colors.json').success(function(data) {
|
|
$scope.palettes = data;
|
|
$scope.palettes.forEach(function(palette) {
|
|
palette.colors.forEach(function(color) {
|
|
color.css = rgb(color.r,color.g,color.b);
|
|
});
|
|
});
|
|
});
|
|
|
|
// A set of "known special" keys
|
|
$scope.specialKeys = {};
|
|
$http.get('keys.json').success(function(data) {
|
|
$scope.specialKeys = data;
|
|
});
|
|
|
|
// Helper to calculate the height of the keyboard layout; cached to improve performance.
|
|
$scope.kbHeight = 0;
|
|
$scope.calcKbHeight = function() {
|
|
var bottom = 0;
|
|
$(".keyborder").each(function(i,e) {
|
|
bottom = Math.max(bottom, $(e).offset().top + $(e).outerHeight());
|
|
});
|
|
$scope.kbHeight = bottom - $('#keyboard').position().top - 10;
|
|
};
|
|
|
|
// Given a key, generate the HTML needed to render it
|
|
var noRenderText = [0,2,1,3,0,4,2,3];
|
|
function renderKey(key) {
|
|
var html = "";
|
|
var capwidth = sizes.capsize(key.width), capwidth2 = sizes.capsize(key.width2);
|
|
var capheight = sizes.capsize(key.height), capheight2 = sizes.capsize(key.height2);
|
|
var capx = sizes.capsize(key.x) + sizes.margin, capx2 = sizes.capsize(key.x+key.x2)+sizes.margin;
|
|
var capy = sizes.capsize(key.y) + sizes.margin, capy2 = sizes.capsize(key.y+key.y2)+sizes.margin;
|
|
var jShaped = (capwidth2 !== capwidth) || (capheight2 !== capheight) || (capx2 !== capx) || (capy2 !== capy);
|
|
var darkColor = darkenColor(key.color);
|
|
var innerPadding = (2*sizes.margin) + (2*sizes.padding);
|
|
var borderStyle = "keyborder", bgStyle = "keybg";
|
|
|
|
key.centerx = key.align&1 ? true : false;
|
|
key.centery = key.align&2 ? true : false;
|
|
key.centerf = key.align&4 ? true : false;
|
|
|
|
if(key.ghost) {
|
|
borderStyle += " ghosted";
|
|
bgStyle += " ghosted";
|
|
}
|
|
// The border
|
|
html += "<div style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4};' class='{5}'></div>\n"
|
|
.format( capwidth, capheight, capx, capy, darkColor, borderStyle );
|
|
if(jShaped) {
|
|
html += "<div style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4};' class='{5}'></div>\n"
|
|
.format( capwidth2, capheight2, capx2, capy2, darkColor, borderStyle );
|
|
}
|
|
// The key edges
|
|
html += "<div style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4};' class='{5}'></div>\n"
|
|
.format( capwidth, capheight, capx+1, capy+1, darkColor, bgStyle );
|
|
if(jShaped) {
|
|
html += "<div style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4};' class='{5}'></div>\n"
|
|
.format( capwidth2, capheight2, capx2+1, capy2+1, darkColor, bgStyle );
|
|
}
|
|
|
|
if(!key.ghost) {
|
|
// The top of the cap
|
|
html += "<div class='keyborder inner' style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4}; padding:{5}px;'></div>\n"
|
|
.format( capwidth-innerPadding, capheight-innerPadding, capx+sizes.margin, capy+(sizes.margin/2), key.color, sizes.padding );
|
|
if(jShaped && !key.stepped) {
|
|
html += "<div class='keyborder inner' style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4}; padding:{5}px;'></div>\n"
|
|
.format( capwidth2-innerPadding, capheight2-innerPadding, capx2+sizes.margin, capy2+(sizes.margin/2), key.color, sizes.padding );
|
|
}
|
|
|
|
if(jShaped && !key.stepped) {
|
|
html += "<div class='keyfg' style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4}; padding:{5}px;'>\n"
|
|
.format( capwidth2-innerPadding, capheight2-innerPadding, capx2+sizes.margin+1, capy2+(sizes.margin/2)+1, key.color, sizes.padding );
|
|
}
|
|
html += "</div><div class='keyfg' style='width:{0}px; height:{1}px; left:{2}px; top:{3}px; background-color:{4}; padding:{5}px;'>\n"
|
|
.format( capwidth-innerPadding, capheight-innerPadding, capx+sizes.margin+1, capy+(sizes.margin/2)+1, key.color, sizes.padding );
|
|
|
|
// The key labels
|
|
html += "<div class='keylabels' style='width:{0}px; height:{1}px;'>".format(capwidth-innerPadding, capheight-innerPadding);
|
|
key.labels.forEach(function(label,i) {
|
|
if(label && label !== "" && !(key.align&noRenderText[i])) {
|
|
var sanitizedLabel = $sanitize(label.replace(/<([^a-zA-Z\/]|$)/,"<$1"));
|
|
html += "<div class='keylabel keylabel{2} centerx-{5} centery-{6} centerf-{7} textsize{8}' style='color:{1};width:{3}px;height:{4}px;'><div style='width:{3}px;max-width:{3}px;height:{4}px;'>{0}</div></div>\n"
|
|
.format(sanitizedLabel, key.text, i+1, capwidth-innerPadding, capheight-innerPadding,
|
|
key.centerx, key.centery, key.centerf, i>0 ? key.fontheight2 : key.fontheight);
|
|
}
|
|
});
|
|
html += "</div></div>";
|
|
}
|
|
|
|
key.html = $sce.trustAsHtml(html);
|
|
key.rect = { x:capx, y:capy, w:capwidth, h:capheight };
|
|
key.rect2 = { x:capx2, y:capy2, w:capwidth2, h:capheight2 };
|
|
};
|
|
|
|
$scope.deserializeAndRender = function(data) {
|
|
$scope.keyboard = deserialize(data);
|
|
$scope.keys().forEach(function(key) {
|
|
renderKey(key);
|
|
});
|
|
$scope.meta = angular.copy($scope.keyboard.meta);
|
|
};
|
|
|
|
function updateSerialized() {
|
|
//$timeout.cancel(serializedTimer); // this is slow, for some reason
|
|
$scope.deserializeException = "";
|
|
$scope.serialized = toJsonPretty(serialize($scope.keyboard));
|
|
}
|
|
|
|
$scope.deserializeAndRender([]);
|
|
if($location.hash()) {
|
|
var loc = $location.hash();
|
|
if(loc[0]=='@') {
|
|
$scope.deserializeAndRender(URLON.parse(encodeURI(loc)));
|
|
} else {
|
|
$scope.deserializeAndRender(fromJsonL(loc));
|
|
}
|
|
} else if($location.path()[0] === '/') {
|
|
$http.get(base_href + $location.path()).success(function(data) {
|
|
$scope.deserializeAndRender(data);
|
|
updateSerialized();
|
|
}).error(function() {
|
|
$scope.loadError = true;
|
|
});
|
|
} else {
|
|
// Some simple default content... just a numpad
|
|
$scope.deserializeAndRender([["Num Lock","/","*","-"],["7\nHome","8\n↑","9\nPgUp",{h:2},"+"],["4\n←","5","6\n→"],["1\nEnd","2\n↓","3\nPgDn",{h:2},"Enter"],[{w:2},"0\nIns",".\nDel"]]);
|
|
}
|
|
|
|
// Undo/redo support
|
|
var undoStack = [];
|
|
var redoStack = [];
|
|
var canCoalesce = false;
|
|
$scope.canUndo = function() { return undoStack.length>0; };
|
|
$scope.canRedo = function() { return redoStack.length>0; };
|
|
$scope.dirty = false;
|
|
$scope.saved = false;
|
|
$scope.saveError = "";
|
|
window.onbeforeunload = function(e) {
|
|
return $scope.dirty ? 'You have made changes to the layout that are not saved. You can save your layout to the server by clicking the \'Save\' button. You can also save your layout locally by bookmarking the \'Permalink\' in the application bar.' : null;
|
|
};
|
|
|
|
function transaction(type, fn) {
|
|
var trans = undoStack.length>0 ? undoStack.last() : null;
|
|
if(trans === null || !canCoalesce || trans.type !== type) {
|
|
trans = { type:type, original:angular.copy($scope.keyboard), open:true, dirty:$scope.dirty };
|
|
undoStack.push(trans);
|
|
if(undoStack.length>32) {
|
|
undoStack.shift();
|
|
}
|
|
}
|
|
canCoalesce = true;
|
|
try {
|
|
fn();
|
|
} finally {
|
|
if($location.hash()) {
|
|
$location.hash("");
|
|
}
|
|
if($location.path()) {
|
|
$location.path("");
|
|
}
|
|
trans.modified = angular.copy($scope.keyboard);
|
|
trans.open = false;
|
|
redoStack = [];
|
|
if(type !== 'rawdata') { updateSerialized(); }
|
|
$scope.dirty = true;
|
|
$scope.saved = false;
|
|
$scope.saveError = "";
|
|
$scope.loadError = false;
|
|
}
|
|
}
|
|
|
|
$scope.undo = function() {
|
|
if($scope.canUndo()) {
|
|
var u = undoStack.pop();
|
|
$scope.keyboard = angular.copy(u.original);
|
|
updateSerialized();
|
|
$scope.keys().forEach(function(key) {
|
|
renderKey(key);
|
|
});
|
|
redoStack.push(u);
|
|
$scope.dirty = u.dirty;
|
|
$scope.unselectAll();
|
|
$scope.meta = $scope.keyboard.meta;
|
|
}
|
|
};
|
|
|
|
$scope.redo = function() {
|
|
if($scope.canRedo()) {
|
|
var u = redoStack.pop();
|
|
$scope.keyboard = angular.copy(u.modified);
|
|
updateSerialized();
|
|
$scope.keys().forEach(function(key) {
|
|
renderKey(key);
|
|
});
|
|
undoStack.push(u);
|
|
$scope.dirty = true;
|
|
$scope.unselectAll();
|
|
$scope.meta = $scope.keyboard.meta;
|
|
}
|
|
};
|
|
|
|
function validate(key,prop,value) {
|
|
var v = {
|
|
_ : function() { return value; },
|
|
x : function() { return Math.max(0, Math.min(36, value)); },
|
|
y : function() { return Math.max(0, Math.min(36, value)); },
|
|
x2 : function() { return Math.max(-Math.abs(key.width-key.width2), Math.min(Math.abs(key.width-key.width2), value)); },
|
|
y2 : function() { return Math.max(-Math.abs(key.height-key.height2), Math.min(Math.abs(key.height-key.height2), value)); },
|
|
width : function() { return Math.max(0.5, Math.min(12, value)); },
|
|
height : function() { return Math.max(0.5, Math.min(12, value)); },
|
|
width2 : function() { return Math.max(0.5, Math.min(12, value)); },
|
|
height2 : function() { return Math.max(0.5, Math.min(12, value)); },
|
|
fontheight : function() { return Math.max(1, Math.min(9, value)); },
|
|
fontheight2 : function() { return Math.max(1, Math.min(9, value)); },
|
|
};
|
|
return (v[prop] || v._)();
|
|
}
|
|
|
|
function update(key,prop,value) {
|
|
var u = {
|
|
_ : function() { key[prop] = value; },
|
|
width : function() { key.width = value; if(!key.stepped || key.width > key.width2) key.width2 = value; },
|
|
height : function() { key.height = value; if(!key.stepped || key.height > key.height2) key.height2 = value; },
|
|
centerx : function() { if(value) { key.align = key.align | 1; } else { key.align = key.align & (~1); } },
|
|
centery : function() { if(value) { key.align = key.align | 2; } else { key.align = key.align & (~2); } },
|
|
centerf : function() { if(value) { key.align = key.align | 4; } else { key.align = key.align & (~4); } },
|
|
fontheight : function() { key.fontheight = key.fontheight2 = value; },
|
|
stepped : function() {
|
|
key[prop] = value;
|
|
if(value && key.width === key.width2) {
|
|
if(key.width > 1) {
|
|
key.width = Math.max(1, key.width-0.5);
|
|
} else {
|
|
key.width2 = key.width+0.5;
|
|
}
|
|
}
|
|
},
|
|
};
|
|
return (u[prop] || u._)();
|
|
}
|
|
|
|
$scope.updateMulti = function(prop) {
|
|
if($scope.multi[prop] == null || $scope.selectedKeys.length <= 0) {
|
|
return;
|
|
}
|
|
var valid = validate($scope.multi, prop, $scope.multi[prop]);
|
|
if(valid !== $scope.multi[prop]) {
|
|
return;
|
|
}
|
|
|
|
transaction("update", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
update(selectedKey, prop, $scope.multi[prop]);
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
};
|
|
|
|
$scope.validateMulti = function(prop) {
|
|
if($scope.multi[prop] == null) {
|
|
$scope.multi[prop] = "";
|
|
}
|
|
var valid = validate($scope.multi, prop, $scope.multi[prop]);
|
|
if(valid !== $scope.multi[prop]) {
|
|
$scope.multi[prop] = valid;
|
|
$scope.updateMulti(prop);
|
|
}
|
|
};
|
|
|
|
$scope.updateMeta = function(prop) {
|
|
transaction("metadata", function() {
|
|
$scope.keyboard.meta[prop] = $scope.meta[prop];
|
|
});
|
|
}
|
|
$scope.validateMeta = function(prop) {
|
|
}
|
|
|
|
$scope.serialized = toJsonPretty(serialize($scope.keyboard));
|
|
|
|
$scope.clickSwatch = function(color,$event) {
|
|
$event.preventDefault();
|
|
if($scope.selectedKeys.length<1) {
|
|
return;
|
|
}
|
|
transaction("color-swatch", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
if($event.ctrlKey || $event.altKey) {
|
|
selectedKey.text = color.css;
|
|
} else {
|
|
selectedKey.color = color.css;
|
|
}
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
};
|
|
|
|
$scope.moveKeys = function(x,y,$event) {
|
|
$event.preventDefault();
|
|
if($scope.selectedKeys.length<1) {
|
|
return;
|
|
}
|
|
|
|
if(x<0 || y<0) {
|
|
var canMoveKeys = true;
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
if(selectedKey.x + x < 0 ||
|
|
selectedKey.y + y < 0 ||
|
|
selectedKey.x + selectedKey.x2 + x < 0 ||
|
|
selectedKey.y + selectedKey.y2 + y < 0) {
|
|
canMoveKeys = false;
|
|
}
|
|
});
|
|
if(!canMoveKeys) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
transaction("move", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
selectedKey.x = Math.max(0,selectedKey.x + x);
|
|
selectedKey.y = Math.max(0,selectedKey.y + y);
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
if(y !== 0) { $scope.calcKbHeight(); }
|
|
};
|
|
|
|
$scope.sizeKeys = function(x,y,$event) {
|
|
$event.preventDefault();
|
|
if($scope.selectedKeys.length<1) {
|
|
return;
|
|
}
|
|
transaction("size", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
update(selectedKey, 'width', Math.max(1,selectedKey.width + x));
|
|
update(selectedKey, 'height', Math.max(1,selectedKey.height + y));
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
if(y!==0) { $scope.calcKbHeight(); }
|
|
};
|
|
|
|
$scope.loadPalette = function(p) {
|
|
$scope.palette = p;
|
|
};
|
|
$scope.loadPreset = function(preset) {
|
|
transaction("preset", function() {
|
|
$scope.deserializeAndRender(preset);
|
|
});
|
|
$scope.dirty = false;
|
|
};
|
|
$scope.loadSample = function(sample) {
|
|
$http.get(base_href + sample).success(function(data) {
|
|
$scope.loadPreset(data);
|
|
$location.path(sample);
|
|
}).error(function() {
|
|
$scope.loadError = true;
|
|
});
|
|
};
|
|
|
|
$scope.deleteKeys = function() {
|
|
if($scope.selectedKeys<1)
|
|
return;
|
|
|
|
transaction('delete', function() {
|
|
// Sort the keys, so we can easily select the next key after deletion
|
|
sortKeys($scope.keys());
|
|
|
|
// Get the indicies of all the selected keys
|
|
var toDelete = $scope.selectedKeys.map(function(key) { return $scope.keys().indexOf(key); });
|
|
toDelete.sort(function(a,b) { return parseInt(a) - parseInt(b); });
|
|
|
|
// Figure out which key we're going to select after deletion
|
|
var toSelectNdx = toDelete.last()+1;
|
|
var toSelect = $scope.keys()[toSelectNdx];
|
|
|
|
// Delete the keys in reverse order so that the indicies remain valid
|
|
for(var i = toDelete.length-1; i >= 0; --i) {
|
|
$scope.keys().splice(toDelete[i],1);
|
|
}
|
|
|
|
// Select the next key
|
|
var ndx = $scope.keys().indexOf(toSelect);
|
|
if(ndx < 0) { ndx = toDelete[0]-1; }
|
|
if(ndx < 0) { ndx = 0; }
|
|
toSelect = $scope.keys()[ndx];
|
|
if(toSelect) {
|
|
$scope.selectedKeys = [toSelect];
|
|
$scope.multi = angular.copy(toSelect);
|
|
} else {
|
|
$scope.unselectAll();
|
|
}
|
|
});
|
|
$('#keyboard').focus();
|
|
};
|
|
|
|
function whereToAddNewKeys(nextline) {
|
|
var xpos = 0, ypos = -1;
|
|
sortKeys($scope.keys());
|
|
if(!nextline && $scope.selectedKeys.length>0 && $scope.keys().length>0 && $scope.multi.x == $scope.keys().last().x) {
|
|
xpos = $scope.multi.x + Math.max($scope.multi.width, $scope.multi.width2 || 0);
|
|
ypos = $scope.multi.y;
|
|
if(xpos >= 23) { xpos = 0; ypos++; }
|
|
} else {
|
|
$scope.keys().forEach(function(key) { ypos = Math.max(ypos,key.y); });
|
|
ypos++;
|
|
}
|
|
return {x:xpos, y:ypos};
|
|
}
|
|
|
|
$scope.addKey = function(proto, nextline) {
|
|
var newKey = null;
|
|
transaction("add", function() {
|
|
var pos = whereToAddNewKeys(nextline);
|
|
var color = $scope.multi.color || "#eeeeee";
|
|
var textColor = $scope.multi.text || "#000000";
|
|
newKey = {width:1, height:1, color:color, text:textColor, labels:[], x:0, y:0, x2:0, y2:0, width2:1, height2:1, profile:"", ghost:false, align:4, fontheight:3, fontheight2:3, nub:false, stepped:false};
|
|
$.extend(newKey, proto);
|
|
newKey.x += pos.x;
|
|
newKey.y += pos.y;
|
|
renderKey(newKey);
|
|
$scope.keys().push(newKey);
|
|
});
|
|
selectKey(newKey,{});
|
|
$scope.calcKbHeight();
|
|
$('#keyboard').focus();
|
|
};
|
|
|
|
$scope.addKeys = function(count) {
|
|
var i;
|
|
for(i = 0; i < count; ++i) {
|
|
$scope.addKey();
|
|
}
|
|
};
|
|
|
|
$scope.deserializeException = "";
|
|
$scope.updateFromSerialized = function() {
|
|
if(serializedTimer) {
|
|
$timeout.cancel(serializedTimer);
|
|
}
|
|
serializedTimer = $timeout(function() {
|
|
try {
|
|
$scope.deserializeException = "";
|
|
transaction("rawdata", function() {
|
|
$scope.deserializeAndRender(fromJsonPretty($scope.serialized));
|
|
});
|
|
$scope.unselectAll();
|
|
} catch(e) {
|
|
$scope.deserializeException = e.toString();
|
|
}
|
|
}, 1000);
|
|
};
|
|
|
|
$scope.selRect = { display:"none" };
|
|
|
|
// Called when the mouse is clicked within #keyboard; we use this to initiate a marquee
|
|
// selection action.
|
|
var doingMarqueeSelect = false;
|
|
$scope.selectClick = function(event) {
|
|
var kbElem = $("#keyboard");
|
|
$scope.selRect = { display:"none", x:event.pageX, y:event.pageY, l:event.pageX, t:event.pageY, w:0, h:0 };
|
|
$scope.selRect.kb = { left: kbElem.position().left + parseInt(kbElem.css('margin-left'),10),
|
|
top: kbElem.position().top + parseInt(kbElem.css('margin-top'),10),
|
|
width: kbElem.outerWidth(),
|
|
height:kbElem.outerHeight()
|
|
};
|
|
doingMarqueeSelect = true;
|
|
event.preventDefault();
|
|
};
|
|
|
|
// Called whenever the mouse moves over the document; ideally we'd get mouse-capture on
|
|
// mouse-down over #keyboard, but it doesn't look like there's a real way to do that in
|
|
// JS/HTML, so we do our best to simulate it. Also, there doesn't appear to be any way
|
|
// to recover if the user releases the mouse-button outside of the browser window.
|
|
$scope.selectMove = function(event) {
|
|
if(doingMarqueeSelect) {
|
|
// Restrict the mouse position to the bounds #keyboard
|
|
var pageX = Math.min($scope.selRect.kb.left + $scope.selRect.kb.width, Math.max($scope.selRect.kb.left, event.pageX));
|
|
var pageY = Math.min($scope.selRect.kb.top + $scope.selRect.kb.height, Math.max($scope.selRect.kb.top, event.pageY));
|
|
|
|
// Calculate the new marquee rectangle (normalized)
|
|
if(pageX < $scope.selRect.x) {
|
|
$scope.selRect.l = pageX;
|
|
$scope.selRect.w = $scope.selRect.x - pageX;
|
|
} else {
|
|
$scope.selRect.l = $scope.selRect.x;
|
|
$scope.selRect.w = pageX - $scope.selRect.x;
|
|
}
|
|
if(pageY < $scope.selRect.y) {
|
|
$scope.selRect.t = pageY;
|
|
$scope.selRect.h = $scope.selRect.y - pageY;
|
|
} else {
|
|
$scope.selRect.t = $scope.selRect.y;
|
|
$scope.selRect.h = pageY - $scope.selRect.y;
|
|
}
|
|
|
|
// If the mouse has moved more than our threshold, then display the marquee
|
|
if($scope.selRect.w + $scope.selRect.h > 5) {
|
|
$scope.selRect.display = "inherit";
|
|
}
|
|
}
|
|
};
|
|
|
|
// Called when the mouse button is released anywhere over the document; see notes above
|
|
// about mouse-capture.
|
|
$scope.selectRelease = function(event) {
|
|
if(doingMarqueeSelect) {
|
|
sortKeys($scope.keys());
|
|
doingMarqueeSelect = false;
|
|
|
|
// Calculate the offset between #keyboard and the mouse-coordinates
|
|
var kbElem = $("#keyboard");
|
|
var kbPos = kbElem.position();
|
|
var offsetx = kbPos.left + parseInt(kbElem.css('padding-left'),10) + parseInt(kbElem.css('margin-left'),10);
|
|
var offsety = kbPos.top + parseInt(kbElem.css('padding-top'),10) + parseInt(kbElem.css('margin-top'),10);
|
|
|
|
// Check to see if the marquee was actually displayed
|
|
if($scope.selRect.display !== "none") {
|
|
// Clear the array of selected keys if the CTRL isn't held down.
|
|
if(!event.ctrlKey && !event.altKey) {
|
|
$scope.unselectAll();
|
|
}
|
|
|
|
$scope.selRect.display = "none";
|
|
|
|
// Adjust the mouse coordinates to client coordinates
|
|
$scope.selRect.l -= offsetx;
|
|
$scope.selRect.t -= offsety;
|
|
|
|
// Iterate over all the keys
|
|
$scope.keys().forEach(function(key) {
|
|
// Check to see if the key is *entirely within* the marquee rectangle
|
|
if( key.rect.x >= $scope.selRect.l && key.rect.x+key.rect.w <= $scope.selRect.l+$scope.selRect.w &&
|
|
key.rect.y >= $scope.selRect.t && key.rect.y+key.rect.h <= $scope.selRect.t+$scope.selRect.h &&
|
|
key.rect2.x >= $scope.selRect.l && key.rect2.x+key.rect2.w <= $scope.selRect.l+$scope.selRect.w &&
|
|
key.rect2.y >= $scope.selRect.t && key.rect2.y+key.rect2.h <= $scope.selRect.t+$scope.selRect.h )
|
|
{
|
|
// Key is inside the rectangle; select it (if not already selected).
|
|
if($scope.selectedKeys.indexOf(key) < 0) {
|
|
selectKey(key, {ctrlKey:true});
|
|
}
|
|
}
|
|
});
|
|
} else {
|
|
// Clear the array of selected keys if the CTRL isn't held down.
|
|
if(!event.ctrlKey && !event.altKey && !event.shiftKey) {
|
|
$scope.unselectAll();
|
|
}
|
|
|
|
// The marquee wasn't displayed, so we're doing a single-key selection;
|
|
// iterate over all the keys.
|
|
$scope.keys().forEach(function(key) {
|
|
// Just check to see if the mouse click is within any key rectangle
|
|
if( (key.rect.x <= event.pageX-offsetx && key.rect.x+key.rect.w >= event.pageX-offsetx &&
|
|
key.rect.y <= event.pageY-offsety && key.rect.y+key.rect.h >= event.pageY-offsety) ||
|
|
(key.rect2.x <= event.pageX-offsetx && key.rect2.x+key.rect2.w >= event.pageX-offsetx &&
|
|
key.rect2.y <= event.pageY-offsety && key.rect2.y+key.rect2.h >= event.pageY-offsety) )
|
|
{
|
|
selectKey(key, {ctrlKey:event.ctrlKey, altKey:event.altKey, shiftKey:event.shiftKey});
|
|
}
|
|
});
|
|
}
|
|
canCoalesce = false;
|
|
|
|
event.preventDefault();
|
|
|
|
// Focus the keyboard, so keystrokes have the desired effect
|
|
$('#keyboard').focus();
|
|
}
|
|
};
|
|
|
|
$scope.getPermalink = function() {
|
|
var url = $location.absUrl().replace(/#.*$/,"");
|
|
url += "##" + URLON.stringify(serialize($scope.keyboard));
|
|
return url;
|
|
};
|
|
|
|
// Called on 'j' or 'k' keystrokes; navigates to the next or previous key
|
|
$scope.prevKey = function(event) {
|
|
if($scope.keys().length>0) {
|
|
sortKeys($scope.keys());
|
|
var ndx = ($scope.selectedKeys.length>0) ? Math.max(0,$scope.keys().indexOf($scope.selectedKeys.last())-1) : 0;
|
|
var selndx = $scope.selectedKeys.indexOf($scope.keys()[ndx]);
|
|
if(event.shiftKey && $scope.keys().length>1 && $scope.selectedKeys.length>0 && selndx>=0) {
|
|
$scope.selectedKeys.pop(); //deselect the existing cursor
|
|
$scope.selectedKeys.splice(selndx,1); //make sure the new cursor is at the end of the selection list
|
|
}
|
|
selectKey($scope.keys()[ndx], {ctrlKey:event.shiftKey});
|
|
canCoalesce = false;
|
|
}
|
|
};
|
|
$scope.nextKey = function(event) {
|
|
if($scope.keys().length>0) {
|
|
sortKeys($scope.keys());
|
|
var ndx = ($scope.selectedKeys.length>0) ? Math.min($scope.keys().length-1,$scope.keys().indexOf($scope.selectedKeys.last())+1) : $scope.keys().length-1;
|
|
var selndx = $scope.selectedKeys.indexOf($scope.keys()[ndx]);
|
|
if(event.shiftKey && $scope.keys().length>1 && $scope.selectedKeys.length>0 && selndx>=0) {
|
|
$scope.selectedKeys.pop(); //deselect the existing cursor
|
|
$scope.selectedKeys.splice(selndx,1); //make sure the new cursor is at the end of the selection list
|
|
}
|
|
selectKey($scope.keys()[ndx], {ctrlKey:event.shiftKey});
|
|
canCoalesce = false;
|
|
}
|
|
};
|
|
|
|
$scope.focusKb = function() { $('#keyboard').focus(); };
|
|
$scope.focusEditor = function() {
|
|
if($scope.selectedKeys.length > 0) {
|
|
if($scope.selTab !== 0) {
|
|
$scope.selTab = 0;
|
|
$('#properties').removeClass('hidden');
|
|
}
|
|
$('#labeleditor').focus().select();
|
|
} else {
|
|
if($scope.selTab !== 1) {
|
|
$scope.selTab = 1;
|
|
$('#kbdproperties').removeClass('hidden');
|
|
}
|
|
$('#kbdcoloreditor').focus().select();
|
|
}
|
|
};
|
|
|
|
$scope.showHelp = function(event) {
|
|
if(event.srcElement.nodeName !== "INPUT" && event.srcElement.nodeName !== "TEXTAREA") {
|
|
event.preventDefault();
|
|
$('#helpDialog').modal('show');
|
|
}
|
|
};
|
|
|
|
// Clipboard functions
|
|
var clipboard = {};
|
|
$scope.cut = function(event) {
|
|
if(event) {
|
|
event.preventDefault();
|
|
}
|
|
if($scope.selectedKeys.length>0) {
|
|
clipboard = angular.copy($scope.selectedKeys);
|
|
$scope.deleteKeys();
|
|
}
|
|
};
|
|
$scope.copy = function(event) {
|
|
if(event) {
|
|
event.preventDefault();
|
|
}
|
|
if($scope.selectedKeys.length>0) {
|
|
clipboard = angular.copy($scope.selectedKeys);
|
|
}
|
|
};
|
|
$scope.paste = function(event) {
|
|
if(event) {
|
|
event.preventDefault();
|
|
}
|
|
if(clipboard.length<1) {
|
|
return;
|
|
}
|
|
sortKeys(clipboard);
|
|
|
|
// Copy the clipboard keys, and adjust them all relative to the first key
|
|
var clipCopy = angular.copy(clipboard);
|
|
var minx = 0, miny = 0, singleRow = true;
|
|
clipCopy.forEach(function(key) {
|
|
minx = Math.min(minx, key.x -= clipboard[0].x);
|
|
miny = Math.min(miny, key.y -= clipboard[0].y);
|
|
});
|
|
|
|
// Adjust to make sure nothing < 0
|
|
clipCopy.forEach(function(key) {
|
|
key.x -= minx;
|
|
key.y -= miny;
|
|
if(key.y>0) { singleRow = false; }
|
|
});
|
|
|
|
// Figure out where to put the keys
|
|
var pos = whereToAddNewKeys(!singleRow);
|
|
|
|
// Perform the transaction
|
|
transaction("paste", function() {
|
|
clipCopy.forEach(function(key,i) {
|
|
key.x += pos.x;
|
|
key.y += pos.y;
|
|
renderKey(key);
|
|
$scope.keys().push(key);
|
|
$scope.selectedKeys = clipCopy;
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
});
|
|
};
|
|
$scope.canCopy = function() { return $scope.selectedKeys.length > 0; }
|
|
$scope.canPaste = function() { return clipboard.length > 0; }
|
|
}]);
|
|
|
|
// Modernizr-inspired check to see if "color" input fields are supported;
|
|
// we hide them if they aren't (e.g., on IE), because it's just a duplicate
|
|
// of the existing text version.
|
|
$(document).ready(function() {
|
|
$('.colorpicker').each(function(i,elem) {
|
|
var old = elem.value;
|
|
elem.value = ":)";
|
|
if(elem.value === ":)") {
|
|
elem.style.display = "none";
|
|
}
|
|
elem.value = old;
|
|
});
|
|
});
|
|
}());
|