keymaster icon indicating copy to clipboard operation
keymaster copied to clipboard

Key Sequences

Open micsco opened this issue 13 years ago • 9 comments

I'm writing this to start a "high" level discussion about how to implement this.

I'm looking to implement similar functionality to that of GMail. Allowing me to have a global first-press, followed by an action.

IE: I want to be able to press g then h, to go home. However at present this doesn't seem possible with keymaster.

I currently implement something similar by using setScope and having a scope specific for my "go actions", however if I'm already in a scope, and these are intended to be global actions, then it gets difficult to return to the previous scope.

My suggestion is a proposed setTemporaryScope method, whereby it automatically returns to the previous scope after next keypress, or if there is no keypress within a certain time frame (say 1second) then it'll return to the previous scope after time out.

Thoughts?

micsco avatar Aug 30 '11 10:08 micsco

I like this, but am not sure about the best API for it yet. It might not be the best way to just sequence up shortcuts, and I feel a full (VIM-style) system of keyboard commands is beyond the scope of Keymaster. I will give it some thought!

madrobby avatar Aug 30 '11 13:08 madrobby

Just an idea:

key.twostep('g', [
  ['h', function(){
    console.log("home")
  }],
  ['s', function(){
    console.log("settings")
  }]
]);

scq avatar Aug 30 '11 13:08 scq

key("g",key.when({
    'h':function(){
        console.log("Home");
    },
    's':key.when("i",function(){
        console.log("Settings -> Issues")
    },function(){
        console.log("Settings");
    })
}))

Where key.when returns a function that binds the even to the given key(s) and unbinds them after they are triggered or a time runs out. You could also pass a callback for when the time runs out. (this could use scope internally). This could be written as a plugin instead of putting it in the core

arextar avatar Aug 30 '11 14:08 arextar

how about:

    key.sequence(["g", "h"], function () { console.log("go home"); });
    key.sequence(["s", "f", "s"], function () {console.log("Settings -> Fonts -> Size"); });

The difficult part of having this functionality comes when a key press or sequence is part of a larger sequence, e.g. if you have this together with the code above:

    key("s", function() {console.log("Settings"); });
    // or
    key.sequence(["s", "f"], function () {console.log("Settings -> Fonts"); });

Not sure if that's a situation that would come up often.

ghost avatar Aug 30 '11 15:08 ghost

The following code works in Chrome, haven't tested it yet in other browsers. IE will fail due to use of Function.bind, so a polyfill will be needed.

(function (keymaster) {
    keymaster._seqScope = "seq_";

    keymaster.sequence = function (keys, scope, method) {
    if (method == undefined) {
      method = scope;
      scope = 'all';
    }

    for(i = 0; i < keys.length; i++) {
      if (i < keys.length-1) {
        //create specific scope for current key in sequence
        _seqScope = _seqScope + keys[i];

        assignKey(keys[i], scope, function (ev, key) {
          setScope(this.toString());

            // reset scope after 1 second
          _timer = setTimeout(function () {
            setScope('all');
          }, 1000);
        }.bind(_seqScope));
      } else {
        // last key should perform the method
        assignKey(keys[i], _seqScope, method);
      }
      scope = _seqScope;
    }
    // reset _seqScope for new sequence
    _seqScope = "seq_";
  }
})(key);

I need to test the plugin setup, and of course it should be tested for robustness in various browsers.

ghost avatar Aug 30 '11 15:08 ghost

A quick draft of how my idea would work as a plug-in (it will likely fail when scoping is used)

(function(keymaster){

    keymaster.when=function(key, fn, callback, timeout){

        timeout||(timeout = 1000)

        return function(){
                var called=0;

                timeout&&setTimeout(function(){
                    called=1;
                    call=typeof key=="object"?
                        fn
                    :
                        callback;

                    call&&call();
                }, timeout)

                if(typeof key=="object"){
                    for(var x in key){
                        (function(key,fn){
                            keymaster(key, function(){
                                if(!called){
                                    fn.apply(this.arguments);
                                    called=1;
                                }
                            })
                        }(x,key[x]))
                    }
                }
                else
                {
                    keymaster(key, function(){
                        if(!called){
                            fn.apply(this.arguments);
                            called=1;
                        }
                    })
                }
            }
    }
})(key);

arextar avatar Aug 30 '11 16:08 arextar

How about a space?

key('⌘+a d', function(){ });

paales avatar Jan 27 '12 19:01 paales

I decided to take a crack at this and wrapped it up into a sorta plugin for keymaster, since I'm not sure this belongs in core. Anyway, here it is: https://github.com/cmawhorter/keymaster-sequences.js

Another lib I looked at used spaces too, as @paales recommended, so I decided to go with that.

key.sequence('ctrl+a d', function(){});

Demo: http://jsfiddle.net/ggZxB/1/

cmawhorter avatar Jul 09 '14 06:07 cmawhorter

late in the game, but here is how i solved it:

const sequence = (mapping, scope, fn, timeout) => {
   mapping.split(',').map(s => s.trim()).forEach(map => { 
      const parts = mapping.split(' ').map(s => s.trim())
      parts.reduce((currentScope, part, index) => {
          const subScope = `${currentScope}_${part}`
          key(part, currentScope, (...args) => {
              if(index == 0 && parts.length > 1)
                setTimeout( () => { key.setScope(scope) }, timeout)
              }
              if(index == parts.length -1){
                key.setScope(scope) 
                fn(args)
             } else {
                key.setScope(subScope)
             }
           })
      return subScope
      }, scope)
  })
}

a mapping like this: sequence('a b c d', 'foo', alert('done'), 2000) will generate 3 mappings like this: key('a', 'myscope', () => { setTimeout( () => { key.setScope('myscope') }, 2000); setScope('myscope_a') }) key('b', 'myscope_a', () => { setScope('myscope_sequence_a_b') }) key('c', 'myscope_a_b', () => { setScope('myscope_sequence_a_b_c') }) key('d', 'myscope_a_b_c', () => { alert('done') })

gregory avatar Apr 20 '18 16:04 gregory