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