swingのキー・バインディング

 CellEditor周りの動作を調べていたのですが、よく分かりません。次のページを見て勉強します。

http://java.sun.com/products/jfc/tsc/special_report/kestrel/keybindings.html

JDK1.2のキーボード・バインディング

Swing1.1では、キーボード・バインディングのためのメカニズムは2つ。
JComponentのregisterKeyboardAction と テキスト・クラスがサポートするKeymap 。
どちらのアプローチも、KeyStrokeとActionを使っていた。

JComponent.registerKeyboardAction()

JComponentクラスは、キーボード・バインディングの管理を、次のpublicメソッドでサポートしていた。

void registerKeyboardAction(ActionListener a, String command, 
                                KeyStroke k, int condition)
void registerKeyboardAction(ActionListener a, KeyStroke k, 
                                int condition)
void unregisterKeyboardAction(KeyStroke k)
void resetKeyboardActions()

conditionは次のいずれか:


キーストロークは、JComponent.processKeyEvent メソッドで処理される。FocusManager や KeyListenerにとられなければ、KeyEventに関連したアクションがバインディング・テーブルから取り出される。

ComponentUIのサブクラスは、テキストクラスを除いてすべて、registerKeyboardAction を使ってキーボード・ナビゲーションを可能にしている。通常、各BasicXXXUIクラスは、installKeyboardActions() と uninstallKeyboardActions() の2つのメソッドの中で、それをやってる。たとえば、次に示すのは、BasicListUIのinstallKeyboardActions() の一部:

	// page up
list.registerKeyboardAction(new PageUpAction
	        ("SelectPageUp", CHANGE_SELECTION),
		KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0),
		JComponent.WHEN_FOCUSED);
list.registerKeyboardAction(new PageUpAction
	        ("ExtendSelectPageUp", EXTEND_SELECTION),
		KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP,
                     InputEvent.SHIFT_MASK), JComponent.WHEN_FOCUSED);

// page down
list.registerKeyboardAction(new PageDownAction
	        ("SelectPageDown", CHANGE_SELECTION),
		KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0),
		JComponent.WHEN_FOCUSED);
list.registerKeyboardAction(new PageDownAction
	        ("ExtendSelectPageDown", EXTEND_SELECTION),
		KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN,
		InputEvent.SHIFT_MASK), JComponent.WHEN_FOCUSED);
テキスト・パッケージとKeymap

JTextComponent(Swingのテキスト・コンポーネントの親クラス)には、キー・バインディングを定義するための、Keymapsという名の順序付リストがある。各テキスト・コンポーネントは、一般的なキー・バインディングを含むデフォルトのキー・マップを持っている。TextUIクラスは、ルック&フィール特有のキー・マップを、デフォルトのキー・マップ(JTextComponentのもの)の前に挿入する。

不整合

JComponentのregisterKeyboardActionの機構とテキスト・コンポーネントのKeymapベースのシステムには、2つの厄介な不整合がある。

  • registerComponentAction の "condition"
    • テキスト・パッケージは、子孫あるいは祖先がフォーカスを持っているときに適用されるバインディングを作ることを、明示的には許していない。JComponent の registerComponentAction()は、こういう場合には、 WHEN_ANCESTOR_OF_FOCUSED_COMPONENT あるいは WHEN_IN_FOCUSED_WINDOW のconditionを使う。Swingのほとんどのバインディングは、WHEN_FOCUSED で定義されている。
  • ActionEvent.getActionCommand() の違い
    • JComponent の registerKeyboardAction メソッドは、ActionEventにコマンドを指定することを許している。ほとんどのテキストではないSwingコンポーネントは、コマンドに意味のある名前を使っており、これらの名前に依存したコードが、たとえばアクセシビリティのコードに、入り込んでいる。テキスト・コンポーネントは、KeyEventsのkeyCharプロパティの文字列バージョンである、ActionEvents の "actionCommand"プロパティに依存している。actionCommandに使う値をオーバライドして指定することは、Keymapsでは許されない。この不整合は、Actionオブジェクトにコマンド文字列を与えることを許し、かつ、Actionのコマンド文字列がnullのときは、keyChar文字列を使うことで、回避することができる。

JDK 1.3 のキーボード・バインディング

JDK1.3のキーボード・バインディング・システムは、新しいAPIに置き換えられている。新しいAPIは、InputMapとActionMapという2つの新しいクラスをベースにしている。InputMapはKeyStrokeをオブジェクトにマップし、ActionMapはオブジェクトをActionにマップする。入力されるキー・イベントは、3段階で処理される。

Object actionMapKey = inputMap.get(KeyStroke.getKeyStroke(keyEvent));
if (actionMapKey != null) {
    Action action = actionMap.get(actionMapKey);
    if (action != null) {
	// run the actions actionPerformed() method
    }
}

入力されるKeyEventsは、KeyStrokeオブジェクトに変換される。
KeyStrokeは、コンポーネントのInputMapによって、ActionMapのキーとして使われるオブジェクトにマップされる。ActionMapから非nullのエントリーが見つかれば、actionPerformedメソッドが実行される。

実際のキー・バインディングの構造は、フォーカスの関係で多少複雑になる。自分がフォーカスを持っている場合、子孫がフォーカスを持っている場合、自分がフォーカスを持つトップレベル・ウィンドウの子孫である場合、それぞれ別のInputMapを持つ。

あらゆるSwingコンポーネントは、1つのActionMapと3つのInputMapを持つことになる。

ActionMapは、ActionMap型のparentプロパティを持っており、get(key)メソッドで検索を行うと、ローカルのマップにマッチしなければ、親のマップを再帰的に検索する。(InputMapも同様)これによって、マップを共有することができる。ほとんどのテキスト・コンポーネントは、キャレットの動作、テキスト編集、カット&ペーストなどの基本的なバインディングを持つ、単一のInputMapを共有している。

F10キーに"cut"アクションをバインドするサンプル:

myComponent.getInputMap().put(KeyStroke.getKeyStroke("F10"), 
                              "cut");

"control-C"を無効にするサンプル:

myComponent.getInputMap().put(KeyStroke.getKeyStroke("control C"), 
                              "none");

新しいアクションを追加するサンプル:

ction myAction = new AbstractAction("doSomething") {
    public void actionPerformed() {
	doSomething();
    }
};
myComponent.getActionMap().put(myAction.get(Action.NAME), 
                               myAction);

次のセクションでは、変更もしかは追加されたAPIを説明する。

InputMapクラス

InputMapクラスは、KeyStrokeとObject(通常はActionの名前を示す文字列だが、何を指定しても構わない。)InputMapは、基本的に型付けが強くなっただけのMapである。ただし、InputMap型のparentプロパティを持っている。

ActionMapクラス

ActionMapクラスは、ObjectとActionを関連付ける。KeyStrokeがObjectに、ObjectがActionに置き換わっただけことを除けば、InputMapと基本的には同じである。

その他の変更

InputMap,ActionMapを使うために、JComponentに次のメソッドが追加された。

    public final void setInputMap(int, InputMap);
    public final InputMap getInputMap(int);
    public setActionMap(ActionMap);
    public ActionMap getActionMap();

set/getInputMap に渡されるintパラメータは、次のいずれかになる。

  • WHEN_IN_FOCUSED_WINDOW
  • WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
  • WHEN_IN_FOCUSED_WINDOW

つまり、JComponentは、3つのInpotMapと一つのActionMapを持つことになる。
各ComponentUIは、ActionMapとInputMapを提供することができる。UIが提供するActionMapは、UIResourceインタフェース(use javax.swing.plaf.InputMapUIResource and javax.swing.plaf.ActionMapUIResource)を実装する。

UIのInputMapをインストールするコード:

SwingUtilties.replaceUIInputMap(jcomponent, int, keyMap);

UIのInputMapを削除するコード:

SwingUtilties.replaceUIInputMap(component, int, null);
キーイベントの処理

新しいAPIでは、キーイベント処理をオーバライドするために、数多くのエントリポイントが存在する。

(9個挙げられているけど一部だけ)

  1. JComponent#processKeyEvent。このメソッドは、KeyEventが有効になっている(有効なInputMapを持っていれば、JComponentが有効にする。)あるいはKeyListenerが登録されている場合にしか呼び出されない。
  2. FocusManager
  3. super.processKeyEventを呼ぶようにすれば. すべてのKeyListenerは通知を受けるようになる。
  4. processComponentKeyEvent
  5. processKeyBinding
  6. KeyboardManager
plafクラス

次のメソッドが追加されている。

  • InputMap getInputMap(int)
    • which is to return the InputMap for the sepcified condition. If the component does not share the InputMap for the specified condition this method will call createInputMap() each time it is invoked.
    • InputMap createInputMap(int)
    • to create a InputMap. This is currently only invoked for InputMaps of type WHEN_IN_FOCUSED_WINDOW, all others are stored in the defaults table so that the component UI never needs to instantiate one. This method only exists for components that create a InputMap per instance (most don't).
  • ActionMap getActionMap()
    • which returns the appropriate ActionMap. If the ActionMap is not shared, this method will call createActionMap to create the ActionMap. If the ActionMap is shared the ActionMap is stored in the defaults table under the name XXX.actionMap. So, the first time getActionMap is invoked there would be no entry, and createActionMap would be invoked, the return value would then be placed in the defaults table.

createActionMap to instantiate the ActionMap with the appropriate Actions.