Keymap and Actions is a whole new keyboard handling system that replaces the prior KeyboardPolicy system. So, to start with, the KeyboardPolicy system that was in Pollock is totally gone.
The design was created by Vassili and has the whole Splash/Tools system in mind.
There are three main objects in this system that the developer sees. They are named, Keymap, Action and Keystroke. The basic idea is that you take a Keystroke (or a series of Keystrokes that make a keystroke sequence) and map it to an Action which is held onto by a Keymap. At runtime, the Agent looks up the keystroke/sequence in the Keymap, and matches it to an Action, and executes that Action.
Keystroke
A Keystroke can be defined with the following creation methods:
"<aSymbolOrCharacter>" Can be either a symbol that represents any "meta" keystroke, such as #Left, #Right, #AltL, #Home, etc., or a Character such as $A, $b, Character esc, etc.
"<aCollectionOfSymbol>" Is a collection, usually an immediate array, of the following symbols: #alt, #control, #shift, #meta, #command.
Note that we are already getting ready for new keyboard handling that will be coming in future, which will do the real thing for Mac OSs in terms of #command not mapping to #alt, and mapping to a unique modifier.
Additionally, there are the following helper creation methods, that create for you many of the typical special keystrokes:
Keystroke backspace
Keystroke backTab
Keystroke del
Keystroke enter
Keystroke esc
Keystroke return
Keystroke space
Keystroke tab
If you want to define a Keystroke sequence, such as if you wanted to create Emacs or other bizarre mappings, you simply chain Keystrokes together, separated by a comma:
Keystroke esc, (Keystroke ctrl: $b)
This, for instance, is used in the TextEditor to make a selection Bold.
Action
Actions are objects that have an ID, an action to execute, plus an optional enabled value. The executable action can either be a Block or a MessageSend. Simple creation methods are provided so that you don't have to get into the internals of creating a MessageSend, and to create a Block version. The enabled value, what we are calling a Boolean Supplier (currently a Boolean, or Block that answers a boolean, or a ObservedValue that answers a boolean), is used to determine, at runtime, if the action is executable.
Each Pane, Form, Window and UserInterface has it's own Keymap. A Keymap is a runtime created object, which contains the mapping between a Keystroke and an Action for the owning object.
You add mappings by using the binding manipulation instance methods of Keymap:
map: <aKeystrokeOrKeystrokeSequence> to: <aSymbol>
map: <aKeystrokeOrKeystrokeSequence> to: <aSymbol> onPlatforms: <aCollectionOfSymbol>
map: <aKeystrokeOrKeystrokeSequence> to: <aSymbol> onPlatformsExcept: <aCollectionOfSymbol>
<aSymbol> represents the Action ID for an Action. <aCollectionOfSymbol> represents one or more symbols that can be answered by the OSHandle currentOS method (#win32, #mac or #unix).
With this, you can define the following:
aKeymap
map: (Keystroke key: Graphics.TextConstants.Ctrla)
to: #selectAll
onPlatformsExcept: #(#mac).
aKeymap
map: (Keystroke cmd: $a)
to: #selectAll
onPlatforms: #(#mac).
All Together Now
How these three classes fit together is where the fun comes in.
First, we need to get into detail about the Keymap. Keymaps can be chained to one another. A Keymap also has an owner. The owner is the object which actions for the keystrokes are looked up within. A Keymap is created with these methods:
Each Pane, Window and UserInterface has an instance variable named Keymap. During keystroke lookup, a the pane with focus looks to it's Keymap. If it can't find a matching keystroke in the main Keymap, it walks down the "next Keymap" chain looking for a match. Unless you use one of the withNextKeymap: methods, or set the nextKeymap, by default a Keymap's nextKeymap value is nil. The lookup stops when it gets to the nextKeymap that is nil. More on this below.
Panes (including Forms), Windows and UserInterfaces all have a Class Instance variable named commonKeymap. All these classes also have a Class initialize method, which initializes the commonKeymap for the specific subclass.
The Keymap value for each instance is created as a new Keymap with a copy of the commonKeymap as it's nextKeymap. Thus, all instances of a pane share a copy of the commonKeymap, but it is the "nextKeymap" allowing you to add more Keymaps for any instance in the actual instances' Keymap. You can thus override a keymapping in the commonKeymap by giving the local Keymap a different mapping, or just add your own special key mappings.
In Splash and the Tools, we will add things like "Code Editing" Keymaps as "nextKeymaps" to the commonKeymaps for you. Also, you can build your own "Pool" of Keymaps, and add them to your UserInterface and so on.
All Panes (including Forms), Windows and UserInterfaces all have an instance variable named actions. When instantiated, each of these goes to the class side to get their basic actions.
Basically, the "system" Actions and Keymaps are on the class side of these objects. For Actions, the method is #createActionsFor:
For Keymaps, the method is addBindingsTo: <aKeymap> on the class side. Here is the example from Button:
addBindingsTo: aKeymap
aKeymap
map: Keystroke tab to: #nextPaneInTabOrder;
map: (Keystroke ctrl: Character tab) to: #nextPaneInTabOrder;
map: Keystroke backTab to: #previousPaneInTabOrder;
map: (Keystroke key: Character tab modifiedBy: #(#shift #control)) to: #previousPaneInTabOrder;
map: (Keystroke key: #BackTab) to: #previousPaneInTabOrder;
map: (Keystroke shift: #BackTab) to: #previousPaneInTabOrder;
map: Keystroke enter to: #doDefaultAction;
map: Keystroke return to: #doDefaultAction;
map: Keystroke space to: #emulatePressButton;
map: Keystroke esc to: #doCancelAction;
yourself.
At runtime, you can add to a Keymap by sending:
<aPaneWindowOrUserInterface> keymap map: <aKestroke> to: ...
How it all ties together (the nitty gritty details).
When you press a key, the system, as per usual, first looks to find the current keyboard consumer. Having found it, it sends #processKeyboardEvent: to that pane's Agent.
From there, it first tries to see if there is a KeyboardHook for that instance, and if so, tries to execute that keyboard hook. Note, because of the new system, keyboard hooks shouldn't be needed since you can add your own keymapings to instances. I left it in because, well, I'm sure you'd all complain if I didn't, and of course, I might be wrong that they aren't needed.
Next, it does the keystroke mapping lookup for that pane. Next it tries any shortcut keys. These are currently only used for Mnemonics on RadioButtons, CheckBoxes, Labels and GroupBoxs, and in a Window, for Accelerators in the MenuBar.
Next, if no one has processed the keystroke, the pane's enclosing pane (if there is one) is asked to do the same thing.
Finally, if no one anywhere in the hierarchy of widgets handles the keystroke, and if the keystroke is NOT a shortcut key and NOT a symbol, it is sent off to the local character key handler. This last is where keys for entering text and lookup in a list box occurs.
If you're interested in how it works, look at #processKeyboardEvent: in AbstractAgent.
The general overview is this. First, a keystroke is attempted to execute within the scope of the current widget in focus. Then, it moves to the enclosing pane. That pane if it is a Window or a Form then gives each of it's widgets a chance to respond. If none of those widgets act, then if the pane is a Form, it acts on any local actions that it has associated with that keystroke. If that Form has a UserInterface, it then get's a chance. If that pane is a Window, and that Window has a MenuBar, then the Menus attempt to do their thing. If after all that, the Window has a UserInterface (which is only not the case if you are for some reason not using the full UserInterface system of Pollock) finally the UserInterface gets a chance.
Thus, you can add a key binding that is associated with just a pane, a form that contains many panes, a UserInterface in a Form, a Window or a UserInterface that may be used in multiple windows (or forms).
This new system will totally got rid of all of the special processKeyboardEvent: methods, except two. The base one, in AbstractAgent will replaced all of the "If This If That" case code typically associated with keystroke processing.
Menus
Menu items also use Actions, and are tied into the larger action system. Unlike Wrapper, and unlike previous versions of Pollock, they no longer have the selector/block behavior that was directly embedded in the menu item itself. Nor do they have the old notion of an action performer. Instead, using Actions, they do lookup just like the way keystroke lookup does it. First trying the pane that currently has focus, and going from there, looking up actions (instead of just keystrokes) along the way.
Much More To Come
Next time we'll talk about the refactored events that all panes trigger, as well as the biggest change in Pollock: The death of Controllers.