mirror of
https://github.com/ijprest/keyboard-layout-editor.git
synced 2026-04-13 03:20:44 -07:00
-- We tried to render the keys before body.onLoad was invoked, so the render templates weren't ready. -- Replaced with an init() function that is called earlier during startup. -- Fixes #91.
989 lines
32 KiB
JavaScript
989 lines
32 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";
|
|
|
|
function toJsonPretty(obj) {
|
|
var res = [];
|
|
obj.forEach(function(elem) { res.push($serial.toJsonL(elem)); });
|
|
return res.join(",\n")+"\n";
|
|
}
|
|
function fromJsonPretty(json) { return $serial.fromJsonL('['+json+']'); }
|
|
|
|
// The angular module for our application
|
|
var kbApp = angular.module('kbApp', ["ngSanitize", "ui.utils", "ui.bootstrap", "ngFileUpload", "ang-drag-drop", "colorpicker.module"]);
|
|
|
|
// The main application controller
|
|
kbApp.controller('kbCtrl', ['$scope','$http','$location','$timeout', '$sce', '$sanitize', '$modal', function($scope, $http, $location, $timeout, $sce, $sanitize, $modal) {
|
|
var serializedTimer = false;
|
|
|
|
// The application version
|
|
$scope.version = "0.12";
|
|
|
|
// 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 = {};
|
|
|
|
// Options
|
|
$scope.sizeStep = 0.25;
|
|
$scope.moveStep = 0.25;
|
|
$scope.rotateStep = 15;
|
|
|
|
// The keyboard data
|
|
$scope.keyboard = { keys: [], meta: {} };
|
|
$scope.keys = function(newKeys) { if(newKeys) { $scope.keyboard.keys = newKeys; } return $scope.keyboard.keys; };
|
|
|
|
// Helper to evaluate the alignment states and determine if a label field is editable
|
|
$scope.labelDisabled = function(index) {
|
|
return $scope.selectedKeys<1 || $scope.multi.align & $renderKey.noRenderText[index];
|
|
};
|
|
|
|
// Helper function to select/deselect all keys
|
|
$scope.unselectAll = function() {
|
|
$scope.selectedKeys = [];
|
|
$scope.multi = {};
|
|
};
|
|
$scope.selectAll = function(event) {
|
|
if(event) { event.preventDefault(); }
|
|
$serial.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) {
|
|
$serial.saveLayout($http, layout,
|
|
function(fn) {
|
|
//success
|
|
$scope.dirty = false;
|
|
$scope.saved = fn;
|
|
$location.path("/layouts/"+fn).hash("").replace();
|
|
$scope.saveError = "";
|
|
},
|
|
function(data,status) {
|
|
// error
|
|
$scope.saved = false;
|
|
$scope.saveError = status.toString() + " - " + data.toString();
|
|
}
|
|
);
|
|
}
|
|
$scope.save = function(event) {
|
|
if(event) {
|
|
event.preventDefault();
|
|
}
|
|
if($scope.dirty) {
|
|
saveLayout($serial.serialize($scope.keyboard));
|
|
}
|
|
};
|
|
$scope.canSave = function() {
|
|
return $scope.dirty;
|
|
};
|
|
$scope.downloadSvg = function() {
|
|
var data = $renderKey.fullSVG($scope.keys(), $scope.keyboard.meta);
|
|
var blob = new Blob([data], {type:"image/svg+xml"});
|
|
saveAs(blob, "keyboard-layout.svg");
|
|
};
|
|
$scope.downloadJson = function() {
|
|
var data = angular.toJson($serial.serialize($scope.keyboard), true /*pretty*/);
|
|
var blob = new Blob([data], {type:"application/json"});
|
|
saveAs(blob, "keyboard-layout.json");
|
|
};
|
|
$scope.uploadJson = function(file, event) {
|
|
if(file && file[0]) {
|
|
var reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
transaction("upload", function() {
|
|
$scope.$apply(function() {
|
|
$scope.deserializeAndRender($serial.fromJsonL(event.target.result));
|
|
});
|
|
});
|
|
};
|
|
reader.readAsText(file[0]);
|
|
}
|
|
};
|
|
|
|
// 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 = "";
|
|
$scope.serializedObjects = [];
|
|
|
|
// 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 = $color.sRGB8(color.r,color.g,color.b).hex();
|
|
});
|
|
});
|
|
});
|
|
|
|
// 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;
|
|
$scope.keys().forEach(function(key) {
|
|
bottom = Math.max(bottom, key.bbox.y2);
|
|
});
|
|
if($scope.keyboard.meta.name || $scope.keyboard.meta.author)
|
|
bottom += 32;
|
|
$scope.kbHeight = bottom;
|
|
};
|
|
|
|
// Given a key, generate the HTML needed to render it
|
|
function renderKey(key) {
|
|
key.html = $sce.trustAsHtml($renderKey.html(key,$sanitize));
|
|
}
|
|
|
|
$scope.deserializeAndRender = function(data) {
|
|
$scope.serializedObjects = data; // cache serialized objects
|
|
$scope.keyboard = $serial.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.serializedObjects = $serial.serialize($scope.keyboard);
|
|
$scope.serialized = toJsonPretty($scope.serializedObjects);
|
|
}
|
|
|
|
$scope.$on('$locationChangeStart', function(event, newUrl, oldUrl) {
|
|
if($location.path() === '') {
|
|
event.preventDefault();
|
|
}
|
|
});
|
|
|
|
$renderKey.init();
|
|
$scope.deserializeAndRender([]);
|
|
if($location.hash()) {
|
|
var loc = $location.hash();
|
|
if(loc[0]=='@') {
|
|
$scope.deserializeAndRender(URLON.parse(encodeURI(loc)));
|
|
} else {
|
|
$scope.deserializeAndRender($serial.fromJsonL(loc));
|
|
}
|
|
} else if($location.path()[0] === '/') {
|
|
$http.get($serial.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) {
|
|
if($scope.dirty) return '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.';
|
|
};
|
|
|
|
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 {
|
|
$location.path("").hash("").replace();
|
|
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)); },
|
|
rotation_angle : function() { return Math.max(-180, Math.min(180, value)); },
|
|
rotation_x : function() { return Math.max(0, Math.min(36, value)); },
|
|
rotation_y : function() { return Math.max(0, Math.min(36, value)); },
|
|
};
|
|
return (v[prop] || v._)();
|
|
}
|
|
|
|
function update(key,prop,value,index) {
|
|
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; },
|
|
text : function() { if(index<0) { key.text = [value[0]]; } else { key.text = 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;
|
|
}
|
|
}
|
|
},
|
|
rotation_angle : function() { key.rotation_angle = value; key.rotation_x = $scope.multi.rotation_x; key.rotation_y = $scope.multi.rotation_y; },
|
|
};
|
|
return (u[prop] || u._)();
|
|
}
|
|
|
|
$scope.updateMulti = function(prop, index) {
|
|
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], index);
|
|
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.calcKbHeight();
|
|
};
|
|
$scope.validateMeta = function(prop) {
|
|
};
|
|
|
|
updateSerialized();
|
|
|
|
$scope.swapColors = function() {
|
|
transaction("swapColors", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
var temp = selectedKey.color;
|
|
selectedKey.color = selectedKey.text[0];
|
|
selectedKey.text = [temp];
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
};
|
|
|
|
$scope.getTextColor = function(colors, index) {
|
|
return colors ? (colors[index] ? colors[index] : colors[0]) : null;
|
|
};
|
|
$scope.clickSwatch = function(color,$event) {
|
|
$scope.dropSwatch(color,$event,$event.ctrlKey || $event.altKey,-1);
|
|
};
|
|
$scope.dropSwatch = function(color,$event,isText,textIndex) {
|
|
$event.preventDefault();
|
|
if($scope.selectedKeys.length<1) {
|
|
return;
|
|
}
|
|
transaction("color-swatch", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
if(isText) {
|
|
if(textIndex<0)
|
|
selectedKey.text = [color.css];
|
|
else
|
|
selectedKey.text[textIndex] = 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.round10(Math.max(0,selectedKey.x + x),-2);
|
|
selectedKey.y = Math.round10(Math.max(0,selectedKey.y + y),-2);
|
|
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.round10(Math.max(1,selectedKey.width + x),-2));
|
|
update(selectedKey, 'height', Math.round10(Math.max(1,selectedKey.height + y),-2));
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
if(y!==0) { $scope.calcKbHeight(); }
|
|
};
|
|
|
|
$scope.rotateKeys = function(angle,$event) {
|
|
$event.preventDefault();
|
|
if($scope.selectedKeys.length<1) {
|
|
return;
|
|
}
|
|
transaction("rotate", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
var newangle = (selectedKey.rotation_angle+angle+360)%360;
|
|
while(newangle > 180) { newangle -= 360; }
|
|
update(selectedKey, 'rotation_angle', Math.round(newangle));
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
$scope.calcKbHeight();
|
|
};
|
|
$scope.moveCenterKeys = function(x,y,$event) {
|
|
$event.preventDefault();
|
|
if($scope.selectedKeys.length<1) {
|
|
return;
|
|
}
|
|
transaction("moveCenter", function() {
|
|
$scope.selectedKeys.forEach(function(selectedKey) {
|
|
update(selectedKey, 'rotation_x', validate(selectedKey, 'rotation_x', Math.round10($scope.multi.rotation_x + x,-2)));
|
|
update(selectedKey, 'rotation_y', validate(selectedKey, 'rotation_y', Math.round10($scope.multi.rotation_y + y,-2)));
|
|
renderKey(selectedKey);
|
|
});
|
|
$scope.multi = angular.copy($scope.selectedKeys.last());
|
|
});
|
|
$scope.calcKbHeight();
|
|
};
|
|
|
|
$scope.loadPalette = function(p) {
|
|
$scope.palette = p;
|
|
};
|
|
$scope.colorName = function(color) {
|
|
if(color && $scope.palette.colors) {
|
|
for (var i = 0; i < $scope.palette.colors.length; i++) {
|
|
if($scope.palette.colors[i].css === color) {
|
|
return $scope.palette.colors[i].name;
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
$scope.loadPreset = function(preset) {
|
|
transaction("preset", function() {
|
|
$scope.deserializeAndRender(preset);
|
|
});
|
|
$scope.dirty = false;
|
|
};
|
|
$scope.loadSample = function(sample) {
|
|
$http.get(sample).success(function(data) {
|
|
$scope.loadPreset(data);
|
|
$location.path(sample).hash("").replace();
|
|
}).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
|
|
$serial.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;
|
|
$serial.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) {
|
|
if(key.rotation_angle == $scope.multi.rotation_angle && key.rotation_x == $scope.multi.rotation_x && key.rotation_y == $scope.multi.rotation_y) {
|
|
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);
|
|
newKey = $serial.defaultKeyProps();
|
|
if($scope.selectedKeys.length>0) {
|
|
newKey.color = $scope.multi.color;
|
|
newKey.text = $scope.multi.text;
|
|
newKey.profile = $scope.multi.profile;
|
|
newKey.rotation_angle = $scope.multi.rotation_angle;
|
|
newKey.rotation_x = $scope.multi.rotation_x;
|
|
newKey.rotation_y = $scope.multi.rotation_y;
|
|
}
|
|
$.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) {
|
|
$serial.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.bbox.x >= $scope.selRect.l && key.bbox.x+key.bbox.w <= $scope.selRect.l+$scope.selRect.w &&
|
|
key.bbox.y >= $scope.selRect.t && key.bbox.y+key.bbox.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) {
|
|
// Rotate the mouse coordinates into transformed key-space, if necessary
|
|
var pt = { x:event.pageX-offsetx, y:event.pageY-offsety };
|
|
if(key.rotation_angle) {
|
|
pt = key.mat.transformPt(pt);
|
|
}
|
|
|
|
// Just check to see if the mouse click is within any key rectangle
|
|
if( (key.rect.x <= pt.x && key.rect.x+key.rect.w >= pt.x &&
|
|
key.rect.y <= pt.y && key.rect.y+key.rect.h >= pt.y) ||
|
|
(key.rect2.x <= pt.x && key.rect2.x+key.rect2.w >= pt.x &&
|
|
key.rect2.y <= pt.y && key.rect2.y+key.rect2.h >= pt.y) )
|
|
{
|
|
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($scope.serializedObjects);
|
|
return url;
|
|
};
|
|
|
|
// Called on 'j' or 'k' keystrokes; navigates to the next or previous key
|
|
$scope.prevKey = function(event) {
|
|
if($scope.keys().length>0) {
|
|
$serial.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) {
|
|
$serial.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();
|
|
}
|
|
};
|
|
|
|
var activeModal = null;
|
|
$scope.showMarkdown = function(filename, event) {
|
|
if($scope.markdownTitle !== filename) {
|
|
$scope.markdownTitle = filename;
|
|
$scope.markdownContent = '';
|
|
$http.get(filename).success(function(data) {
|
|
$scope.markdownContent = $sce.trustAsHtml(marked(data));
|
|
});
|
|
}
|
|
if(activeModal) activeModal.close();
|
|
activeModal = $modal.open({
|
|
templateUrl:"markdownDialog.html",
|
|
controller:"modalCtrl",
|
|
scope:$scope,
|
|
windowClass:"modal-xxl markdownDialog",
|
|
resolve: { params: function() { return { parentScope: $scope }; } }
|
|
});
|
|
event.preventDefault();
|
|
};
|
|
$scope.previewNotes = function(event) {
|
|
$scope.markdownTitle = 'About This Keyboard Layout';
|
|
var name = $scope.keyboard.meta.name;
|
|
var author = $scope.keyboard.meta.author;
|
|
var notes = $scope.keyboard.meta.notes;
|
|
var markdown = (name ? ("### " + name + "\n") : "") +
|
|
(author ? ("#### _" + author + "_\n") : "") +
|
|
(notes ? (notes) : "");
|
|
$scope.markdownContent = $sce.trustAsHtml($sanitize(marked(markdown)));
|
|
$scope.showMarkdown($scope.markdownTitle, event);
|
|
};
|
|
|
|
$scope.showHelp = function(event) {
|
|
if(!event.srcElement || (event.srcElement.nodeName !== "INPUT" && event.srcElement.nodeName !== "TEXTAREA")) {
|
|
if(activeModal) activeModal.dismiss('cancel');
|
|
activeModal = $modal.open({
|
|
templateUrl:"helpDialog.html",
|
|
controller:"modalCtrl",
|
|
scope:$scope,
|
|
windowClass:"modal-xl",
|
|
resolve: { params: null }
|
|
});
|
|
event.preventDefault();
|
|
}
|
|
};
|
|
|
|
$scope.showOptions = function(event) {
|
|
if(activeModal) activeModal.dismiss('cancel');
|
|
activeModal = $modal.open({
|
|
templateUrl:"optionsDialog.html",
|
|
controller:"modalCtrl",
|
|
scope:$scope,
|
|
resolve: { params: function() { return { moveStep:$scope.moveStep, sizeStep:$scope.sizeStep, rotateStep:$scope.rotateStep }; } }
|
|
});
|
|
activeModal.result.then(function(params) { $.extend($scope, params); });
|
|
event.preventDefault();
|
|
};
|
|
|
|
// 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;
|
|
}
|
|
$serial.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; };
|
|
|
|
$scope.keyboardTop = function() { var kbElem = $("#keyboard"); return kbElem.position().top + parseInt(kbElem.css('margin-top'),10); };
|
|
$scope.keyboardLeft = function() { var kbElem = $("#keyboard"); return kbElem.position().left + parseInt(kbElem.css('margin-left'),10); };
|
|
}]);
|
|
|
|
// Simple modal-popup controller
|
|
kbApp.controller('modalCtrl', function($scope, $modalInstance, params) {
|
|
$scope.params = params;
|
|
$scope.ok = function() {
|
|
$modalInstance.close($scope.params);
|
|
};
|
|
$scope.cancel = function() {
|
|
$modalInstance.dismiss('cancel');
|
|
};
|
|
});
|
|
|
|
kbApp.directive('kbdColorPicker', function($timeout) {
|
|
return {
|
|
templateUrl: "colorPicker.html",
|
|
restrict: "E",
|
|
scope: { hintText: "@", pickerId: "@", pickerPosition: "@", color: "=ngModel", _onChange: "&ngChange", _onBlur: "&ngBlur", isDisabled: "&ngDisabled" },
|
|
link: function($scope) {
|
|
$scope.onChange = function() { $timeout($scope._onChange); };
|
|
$scope.onBlur = function() { $timeout($scope._onBlur); };
|
|
}
|
|
};
|
|
});
|
|
|
|
kbApp.directive('kbdLabelEditor', function() {
|
|
return { templateUrl: "labelEditor.html", restrict: "E", scope: { hintText: "@", labelIndex: "=" } };
|
|
});
|
|
kbApp.directive('kbdMultiCheck', function() {
|
|
return { templateUrl: "multiCheck.html", restrict: "E", scope: { hintText: "@", field: "@" }, transclude: true };
|
|
});
|
|
kbApp.directive('kbdMultiNumbox', function() {
|
|
return { templateUrl: "multiNumbox.html", restrict: "E", scope: { field: "@", size:"@", min:"@", max:"@", step:"@" } };
|
|
});
|
|
|
|
}());
|