Skip to content

Instantly share code, notes, and snippets.

@thedeeno
Created March 25, 2010 22:46
Show Gist options
  • Save thedeeno/344234 to your computer and use it in GitHub Desktop.
Save thedeeno/344234 to your computer and use it in GitHub Desktop.
Rough draft of layout trigger functionality
/*
* Object model to support layout triggers. It consists of:
* - LayoutTriggerController
* - Layout Representation
* - 3 Triggers: Anchor, Hot, and Toggle
*/
/* *************************************************************
* LayoutTriggerController
*/
// expects an object with 2 properties: triggers and defaultLayout
function LTController(options) {
this.defaultLayout = options.defaultLayout;
this.triggers = options.triggers;
this.revertToDefaultOnNextKeyPress = false;
// current layout starts as default
this.current = options.defaultLayout;
// add a helper function
this.triggers.findByKey = function(keyCode)
{
for (var i=0;i<this.length;i++) {
if (this[i].matches(keyCode))
return this[i];
}
return null;
}
}
// mode management functions
LTController.prototype.revertLayoutOnNextKeyPress = function() {
this.revertToDefaultOnNextKeyPress = true;
}
LTController.prototype.handleKey = function(key) {
var trigger = this.triggers.findByKey(key);
if (trigger != null) {
// ask trigger to process key and pass some callbacks
trigger.ltc = this; // work around for now
trigger.handleKey(key);
}
if (key.cancel) {
//log('cancel key');
return;
}
//log('process key normally');
switch (key.value) {
case 1:
this.keyPress(key);
break;
case 2:
this.keyHold(key);
break;
default:
this.keyRelease(key);
}
}
LTController.prototype.inAuxMode = function() {
return this.current != this.defaultLayout;
}
LTController.prototype.setLayout = function(layout) {
this.revertToDefaultOnNextKeyPress = false;
log('enable layout: ' + layout.desc);
this.current = layout;
}
LTController.prototype.emitKey = function(key) {
this.current.emitKey(key);
}
LTController.prototype.onTriggerActivate = function(trigger) {
this.setLayout(trigger.layout);
}
LTController.prototype.onTriggerDeactivate = function(trigger) {
this.revertLayoutOnNextKeyPress();
}
LTController.prototype.emitKey = function(code, value) {
this.current.emitKey(code, value);
}
// key event processors
LTController.prototype.keyPress = function(key) {
if (this.revertToDefaultOnNextKeyPress)
this.setLayout(this.defaultLayout);
this.current.emitKey(key.code, key.value);
}
LTController.prototype.keyHold = function(key) {
this.current.emitKey(key.code, key.value);
}
LTController.prototype.keyRelease = function(key) {
this.current.emitKey(key.code, key.value);
}
/* *************************************************************
* Trigger
*/
function Trigger(keyCode, layout) {
this.keyCode = keyCode;
this.layout = layout;
this.active = false;
};
Trigger.prototype.matches = function(key) {
return this.keyCode == key.code;
}
Trigger.prototype.handleKey = function(key, onTriggerActivate, onTriggerDeactivate, emitKey) {
// do nothing
}
Trigger.prototype.activate = function() {
// log("activate trigger")
this.active = true;
this.ltc.onTriggerActivate(this);
}
Trigger.prototype.deactivate = function() {
// log("deactivate trigger");
this.active = false;
this.ltc.onTriggerDeactivate(this);
}
/* *************************************************************
* AnchorTrigger
*/
function AnchorTrigger(keyCode, layout, emitBackspaceOnTrigger) {
Trigger.call(this, keyCode, layout);
this.emitBackspaceOnTrigger = emitBackspaceOnTrigger;
}
// extension
AnchorTrigger.prototype = new Trigger();
AnchorTrigger.base = Trigger.prototype;
// functions
AnchorTrigger.prototype.handleKey = function(key) {
if (this.matches(key) && key.value == 2 && !this.active) // do only on first repeat
{
this.tlc.emitKey(key.code, 0); // release key (even though it's still held down)
key.cancel = true;
this.activate(onTriggerActivate, emitKey);
}
if (this.matches(key) && key.value == 0 && this.active)
this.deactivate(onTriggerDeactivate);
if (this.active)
key.cancel = true; // supress key
}
AnchorTrigger.prototype.activate = function(onTriggerActivate) {
if (this.emitBackspaceOnTrigger)
{
// log("emit backspace");
this.tlc.emitKey(KEY_BACKSPACE, 1);
this.tlc.emitKey(KEY_BACKSPACE, 0);
}
AnchorTrigger.base.activate.call(this, onTriggerActivate);
}
/* *************************************************************
* HotTrigger
*/
function HotTrigger(keyCode, layout) {
Trigger.call(this, keyCode, layout);
}
// extension
HotTrigger.prototype = new Trigger();
// functions
HotTrigger.prototype.handleKey = function(key) {
if (this.matches(key) && key.value == 1 && !this.active) // do only on key down
this.activate();
if (this.matches(key) && key.value == 0 && this.active)
this.deactivate();
}
/* *************************************************************
* ToggleTrigger
*/
function ToggleTrigger(keyCode, layout, ltController) {
Trigger.call(this, keyCode, layout, ltController);
}
// extension
ToggleTrigger.prototype = new Trigger();
// functions
ToggleTrigger.prototype.handleKey = function(key) {
if (this.matches(key) && key.value == 1) { // do only on key down
if (!this.active)
this.activate();
else
this.deactivate();
}
}
/* *************************************************************
* Layout
*/
function Layout(config) {
this.sysSyms = _code2sym;
this.sysCodes = _sym2code;
this.desc = config.desc;
this.mapTable = new cloneObject(this.sysCodes); // start by copying the system layout
if (config.suppressUnknownKeys)
this.clearMappings(); // prevent translation of non-explicit mappings
config.mapping(this);
}
Layout.prototype.map = function(actual, translated) {
this.mapTable['KEY_' + actual] = this.sysCodes['KEY_' + translated];
}
Layout.prototype.clearMappings = function() {
for (i in this.mapTable) {
this.mapTable[i] = null;
}
}
Layout.prototype.translateCode = function(sysCode) {
var s = this.sysSyms[sysCode];
return this.mapTable[s];
}
Layout.prototype.emitKey = function(code, value) {
var tcode = this.translateCode(code);
if (!tcode) {
log("suppress key, it's not in the layout");
return;
}
log(this.desc + " emit " + this.sysSyms[tcode] + ' ' + tcode + ' ' + value);
emit(EV_KEY, tcode, value);
}
/* *************************************************************
* Helper Functions
*/
function cloneObject(source) {
for (i in source) {
if (typeof source[i] == 'source') {
this[i] = new cloneObject(source[i]);
}
else {
this[i] = source[i];
}
}
}
// Mangler that shows how to leverage LayoutTriggers.
// - The default layout is dvorak.
// - While LEFTCTRL is pressed the keyboard is in qwerty modethe layout back to qwerty,
// facilitating the use of standard hotkeys.
// - CAPSLOCK toggles a special 'navigation' auxiliary layout
include('keysyms.js');
include('keynames.js');
include('LayoutTriggers.js');
// configuration
var navigation = {
desc : "navigation",
suppressUnknownKeys: true,
mapping: function(layout) {
layout.map('E', 'UP');
layout.map('D', 'DOWN');
layout.map('S', 'LEFT');
layout.map('F', 'RIGHT');
layout.map('W', 'HOME');
layout.map('R', 'END');
layout.map('Q', 'PAGEUP');
layout.map('A', 'PAGEDOWN');
layout.map('SEMICOLON', 'ENTER');
layout.map('G', 'DELETE');
layout.map('H', 'BACKSPACE');
// inclunde triggers in layout, otherwise they may be suppressed
// while the layout will be activated it may cause some unexpected behaviour.
// for instance, if capslock is surpress on activatio but not deactivation
// it you'll be accidentally toggling capslock as well as the layout.
layout.map('LEFTSHIFT', 'LEFTSHIFT');
layout.map('CAPSLOCK', 'CAPSLOCK');
layout.map('SPACE', 'SPACE');
}
};
var dvorak = {
desc : "dvorak",
mapping : function(layout) {
layout.map('MINUS','LEFTBRACE');
layout.map('EQUAL','RIGHTBRACE');
layout.map('Q','APOSTROPHE');
layout.map('W','COMMA');
layout.map('E','DOT');
layout.map('R','P');
layout.map('T','Y');
layout.map('Y','F');
layout.map('U','G');
layout.map('I','C');
layout.map('O','R');
layout.map('P','L');
layout.map('LEFTBRACE','SLASH');
layout.map('RIGHTBRACE','EQUAL');
layout.map('A','A');
layout.map('S','O');
layout.map('D','E');
layout.map('F','U');
layout.map('G','I');
layout.map('H','D');
layout.map('J','H');
layout.map('K','T');
layout.map('L','N');
layout.map('SEMICOLON','S');
layout.map('APOSTROPHE','MINUS');
layout.map('Z','SEMICOLON');
layout.map('X','Q');
layout.map('C','J');
layout.map('V','K');
layout.map('B','X');
layout.map('N','B');
layout.map('M','M');
layout.map('COMMA','W');
layout.map('DOT','V');
layout.map('SLASH','Z');
}
}
var qwerty = {
desc : "qwerty",
mapping: function(layout) {
// do nothing, the default input layout is qwerty so no need to translate
}
}
var dvorak = new Layout(dvorak);
var tlc = new LTController({
defaultLayout : dvorak,
triggers : [
new ToggleTrigger(KEY_CAPSLOCK, new Layout(navigation)),
new HotTrigger(KEY_LEFTCTRL, new Layout(qwerty), false),
new AnchorTrigger(KEY_SPACE, new Layout(navigation))
]
});
/* This is the entry point of event processing, this function will be called by the system for every key event.
* IMPORTANT NOTE: this function must eventually emit some events back to the system otherwise your system
* completely stops responding to keyboard!!!
* Normally you want to emit almost all events with an exception of some keys you wish to process in a different way.
*/
function process(ev){
switch (ev.type) {
case EV_KEY:
processKey(ev);
break;
default:
emit(ev.type, ev.code, ev.value);
}
}
function processKey(key) {
tlc.handleKey(key);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment