Friday, August 9, 2013

Better Keyboard Shortcuts in Dart


Two days ago, I found that a known bug in Dart prevented me from using the KeyEvent class. The KeyEvent class is supposed to normalize all kinds of browser craziness surrounding keyboard handling, so I was bummed that I was unable to use it.

But nothing is preventing me from creating a subclass of KeyEvent that does not suffer from the problem.

So I do just that:
class KeyEventX extends KeyEvent {
  KeyboardEvent _parent;

  KeyEventX(KeyboardEvent parent): super(parent) {
    _parent = parent;
  }

  // Avoid bug in KeyEvent
  // https://code.google.com/p/dart/issues/detail?id=11139
  String get $dom_keyIdentifier => _parent.$dom_keyIdentifier;
  String get keyIdentifier => _parent.$dom_keyIdentifier;
  // ...
}
The two getter methods for key identifier should avoid the “Unsupported operation: keyIdentifier is unsupported” error from the other day. Instead of accessing the keyIdentifier property of the soon-to-be-awesome-but-currently-buggy KeyEvent class, I obtain it from the plain-old KeyboardEvent that DOM programmers know and love.

To make use of my new KeyEventX class, I need a stream that produces instances of it. I rather like the simplicity of the KeyboardEventStream class, so I extend its functionality into a new KeyboardEventStreamX class:
class KeyboardEventStreamX extends KeyboardEventStream {
  // ...
  static Stream<KeyEventX> onKeyDown(EventTarget target) {
    return Element.
      keyDownEvent.
      forTarget(target).
      map((e)=> new KeyEventX(e));
  }
}
Given an event target (i.e. a DOM Element), I return a stream that maps regular KeyboardEvent instances into KeyEventX instances. It is pretty awesome that streams are iterable—meaning that I transform my event without even the minor complexity of a stream transformer.

With my KeyboardEventStreamX class that produces a stream of my non-buggy KeyEventX objects, I can now rewrite some of the keyboard controls in the ICE Code Editor as:
    KeyboardEventStreamX.onKeyDown(document).listen((e) {
      if (!e.ctrlKey) return;

      switch(e.keyIdentifier) {
        case 'U+004E': // N
          new NewProjectDialog(this).open();
          e.preventDefault();
          break;
        case 'U+004F': // O
          new OpenDialog(this).open();
          e.preventDefault();
          break;
        // ...
      }
    });
And that works! The dartanalyzer tool is happy with the type information, the copious tests that I have written are all still passing and everything seems OK when I smoke test the sample application.

Now, that is nice and all, but since I am writing this to support keyboard shortcuts, I do not want to be checking for control keys, then checking for the character key. I want a predicate method on my KeyEventX that tells me if it is a Ctrl+N or Ctrl+O.

Thanks to last night's keyIdentifierFor() helper method, I can do just that:
class KeyEventX extends KeyEvent {
  // ...
  bool isCtrl(String char) {
    if (!ctrlKey) return false;

    if (keyIdentifier == keyIdentifierFor(char)) return true;

    return false;
  }

  String keyIdentifierFor(char) {
    if (char.codeUnits.length != 1) throw "Don't know how to type “$char”";

    // Keys are uppercase (see Event.keyCode)
    var key = char.toUpperCase();

    return 'U+00' + key.codeUnits.first.toRadixString(16).toUpperCase();
  }
}
With isCtrl() in hand, I can rewrite my keyboard shortcut code as:
    KeyboardEventStreamX.onKeyDown(document).listen((e) {
      if (e.isCtrl('N')) {
        new NewProjectDialog(this).open();
        e.preventDefault();
      }
      if (e.isCtrl('O')) {
        new OpenDialog(this).open();
        e.preventDefault();
      }
    });
That is very nice.

To be sure, there is some code duplication that I can clean-up in there. But I have made my keyboard handling code much cleaner and easier to read. Gone are the unicode strings. In are the the easy-to-follow isCtrl('N') booleans. I like.

And, unless I am wrong (though I am often wrong), I do believe that I am dangerously close to a legitimately testable approach to keyboard events in Dart. Last night, I was able to generate keyboard events in test that registered identically to those produced in Chrome. Tonight, I have significantly improved handling those events. All that is lacking is the ability to normalize those events across browsers. Dart will get there eventually. Until that happens, I have the perfect place get started: in KeyEventX. I will pick up with that tomorrow.


Day #838

No comments:

Post a Comment