/*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at http://www.netbeans.org/cddl.html
* or http://www.netbeans.org/cddl.txt.
*
* When distributing Covered Code, include this CDDL Header Notice in each file
* and include the License file at http://www.netbeans.org/cddl.txt.
* If applicable, add the following below the CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
* Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.editor;
import java.awt.event.KeyEvent;
import java.awt.event.ActionEvent;
import java.awt.event.InputEvent;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import javax.swing.text.Keymap;
import javax.swing.text.JTextComponent;
import javax.swing.text.DefaultEditorKit;
import javax.swing.KeyStroke;
import javax.swing.Action;
import javax.swing.AbstractAction;
import org.openide.awt.StatusDisplayer;
import org.openide.util.Lookup;
/**
* Keymap that is capable to work with MultiKeyBindings
*
* @author Miloslav Metelka
* @version 0.10
*/
public class MultiKeymap implements Keymap {
private static final boolean compatibleIgnoreNextTyped
= Boolean.getBoolean("netbeans.editor.keymap.compatible");
/** Action that does nothing */
public static final Action EMPTY_ACTION = new AbstractAction() {
public void actionPerformed(ActionEvent evt) {
}
};
/** Action that beeps. Used for wrong shortcut by default */
public static final Action BEEP_ACTION = new DefaultEditorKit.BeepAction();
/** JTextComponent.DefaultKeymap to be used for processing by this keymap */
private Keymap delegate;
/** Context keymap or null for base context */
private Keymap context;
/** Ignore possible keyTyped events after context reset */
private boolean ignoreNextTyped = false;
/** Action to return when there's no action for incoming key
* in some context. This action doesn't occur when no action
* is found in base context.
*/
private Action contextKeyNotFoundAction = BEEP_ACTION;
/**
* List of key strokes that form the present context.
* If this list differs from the global context maintained in the status displayer
* then the keymap must be reset and attempted to be put
* into the global context before attempting to process the given keystroke.
* If the keymap cannot be put into such a context then
* it returns null action for the given keystroke.
*/
private List contextKeys;
/** Construct new keymap.
* @param name name of new keymap
*/
public MultiKeymap(String name) {
delegate = JTextComponent.addKeymap(name, null);
contextKeys = new ArrayList();
}
/** Set the context keymap */
void setContext(Keymap contextKeymap) {
context = contextKeymap;
}
private static String getKeyText (KeyStroke keyStroke) {
if (keyStroke == null) return ""; // NOI18N
String modifText = KeyEvent.getKeyModifiersText
(keyStroke.getModifiers ());
if ("".equals (modifText)) // NOI18N
return KeyEvent.getKeyText (keyStroke.getKeyCode ());
return modifText + "+" + // NOI18N
KeyEvent.getKeyText (keyStroke.getKeyCode ());
}
/** Reset keymap to base context */
public void resetContext() {
context = null;
contextKeys.clear();
}
/**
* Add a context key to the global context maintained by the NbKeymap.
*
* @param key a key to be added to the global context.
*/
private void shiftGlobalContext(KeyStroke key) {
List globalContextList = getGlobalContextList();
if (globalContextList != null) {
globalContextList.add(key);
StringBuffer text = new StringBuffer();
for (Iterator it = globalContextList.iterator(); it.hasNext();) {
text.append(getKeyText((KeyStroke)it.next())).append(' ');
}
StatusDisplayer.getDefault().setStatusText(text.toString());
}
// Shift the locally maintained mirror context as well
contextKeys.add(key);
}
/**
* Reset the global context in case there is a reason for it.
*/
private void resetGlobalContext() {
List globalContextList = getGlobalContextList();
if (globalContextList != null) {
globalContextList.clear();
StatusDisplayer.getDefault().setStatusText("");
}
}
private List getGlobalContextList() {
// Retrieve the list from NbKeymap by reflection
// Get system classloader
List globalContextList;
try {
ClassLoader sysCL = (ClassLoader)Lookup.getDefault().lookup(ClassLoader.class);
Class nbKeymapClass = Class.forName("org.netbeans.core.NbKeymap", true, sysCL); // NOI18N
java.lang.reflect.Field contextField = nbKeymapClass.getDeclaredField("context"); // NOI18N
contextField.setAccessible(true);
globalContextList = (List)contextField.get(null);
} catch (Exception e) {
// Ignore the exception
globalContextList = null;
}
return globalContextList;
}
/** What to do when key is not resolved for context */
public void setContextKeyNotFoundAction(Action a) {
contextKeyNotFoundAction = a;
}
/** Loads the key to action mappings into this keymap in similar way
* as JTextComponent.loadKeymap() does. This method is able to handle
* MultiKeyBindings but for compatibility it expects
* JTextComponent.KeyBinding array.
*/
public void load(JTextComponent.KeyBinding[] bindings, Action[] actions) {
Map h = new HashMap(bindings.length);
// add actions to map to resolve by names quickly
for (int i = 0; i < actions.length; i++) {
Action a = actions[i];
String value = (String)a.getValue(Action.NAME);
h.put((value != null ? value : ""), a); // NOI18N
}
load(bindings, h);
}
/** Loads key to action mappings into this keymap
* @param bindings array of bindings
* @param actions map of [action_name, action] pairs
*/
public void load(JTextComponent.KeyBinding[] bindings, Map actions) {
// now create bindings in keymap(s)
for (int i = 0; i < bindings.length; i++) {
Action a = (Action)actions.get(bindings[i].actionName);
if (a != null) {
boolean added = false;
if (bindings[i] instanceof MultiKeyBinding) {
MultiKeyBinding mb = (MultiKeyBinding)bindings[i];
if (mb.keys != null) {
Keymap cur = delegate;
for (int j = 0; j < mb.keys.length; j++) {
if (j == mb.keys.length - 1) { // last keystroke in sequence
cur.addActionForKeyStroke(mb.keys[j], a);
} else { // not the last keystroke
Action sca = cur.getAction(mb.keys[j]);
if (!(sca instanceof KeymapSetContextAction)) {
sca = new KeymapSetContextAction(JTextComponent.addKeymap(null, null));
cur.addActionForKeyStroke(mb.keys[j], sca);
}
cur = ((KeymapSetContextAction)sca).contextKeymap;
}
}
added = true;
}
}
if (!added) {
if (bindings[i].key != null) {
delegate.addActionForKeyStroke(bindings[i].key, a);
} else { // key is null -> set default action
setDefaultAction(a);
}
}
}
}
}
public String getName() {
return (context != null) ? context.getName()
: delegate.getName();
}
/** Get default action of this keymap or parent keymap if this
* one doesn't have one. Context keymap can have default action
* but it will be not used.
*/
public Action getDefaultAction() {
return delegate.getDefaultAction();
}
public void setDefaultAction(Action a) {
if (context != null) {
context.setDefaultAction(a);
} else {
delegate.setDefaultAction(a);
}
}
private Action getActionImpl(KeyStroke key) {
Action a = null;
if (context != null) {
a = context.getAction(key);
// Commented out the next part to allow the other
// keystroke processors to work when the editor does not have an action
// for the particular keystroke.
/* if (a == null) { // possibly ignore modifier keystrokes
switch (key.getKeyCode()) {
case KeyEvent.VK_SHIFT:
case KeyEvent.VK_CONTROL:
case KeyEvent.VK_ALT:
case KeyEvent.VK_META:
return EMPTY_ACTION;
}
if (key.isOnKeyRelease()
|| (key.getKeyChar() != 0 && key.getKeyChar() != KeyEvent.CHAR_UNDEFINED)
) {
return EMPTY_ACTION; // ignore releasing and typed events
}
}
*/
} else {
a = delegate.getAction(key);
}
return a;
}
private boolean contextKeysEqual(List keys) {
if (keys.size() != contextKeys.size()) {
return false;
}
for (int i = keys.size() - 1; i >= 0; i--) {
if (!contextKeys.get(i).equals(keys.get(i))) {
return false;
}
}
return true;
}
public Action getAction(KeyStroke key) {
Action ret = null;
// Check whether the context in status displayer corresponds to the keymap's context
// If there would be a non-empty SD context that differs from the editor's one
// then do not return any action for this keystroke.
List globalContextList = getGlobalContextList();
if (globalContextList != null && globalContextList.size() > 0 && !contextKeysEqual(globalContextList)) {
resetContext();
int i;
for (i = 0; i < globalContextList.size(); i++) {
Action a = getActionImpl((KeyStroke)globalContextList.get(i));
if (a instanceof KeymapSetContextAction) {
a.actionPerformed(null);
} else {
// no multi-keystrokes for such context in editor
resetContext();
break;
}
}
if (i != globalContextList.size()) { // unsuccessful context switch
return null;
}
}
// Explicit patches of the keyboard problems
if (ignoreNextTyped) {
if (key.isOnKeyRelease()) { // ignore releasing here
ret = EMPTY_ACTION;
} else { // either pressed or typed
ignoreNextTyped = false;
}
if (key.getKeyChar() != 0 && key.getKeyChar() != KeyEvent.CHAR_UNDEFINED) {
ret = EMPTY_ACTION; // prevent using defaultAction
}
}
if (ret == null) {
ret = getActionImpl(key);
if (ret instanceof KeymapSetContextAction) { //
// Mark the context shifting
shiftGlobalContext(key);
} else { // not a context shift action
if (context != null) { // Already in a non-empty context
ignoreNextTyped = true;
} else if (compatibleIgnoreNextTyped) {
// #44307 = disabled extra ignoreNextTyped patches for past JDKs
if ( // Explicit patch for the keyTyped sent after Alt+key
(key.getModifiers() & InputEvent.ALT_MASK) != 0 // Alt pressed
&& (key.getModifiers() & InputEvent.CTRL_MASK) == 0 // Ctrl not pressed
) {
boolean patch = true;
if (key.getKeyChar() == 0 || key.getKeyChar() == KeyEvent.CHAR_UNDEFINED) {
switch (key.getKeyCode()) {
case KeyEvent.VK_ALT: // don't patch single Alt
case KeyEvent.VK_KANJI:
case KeyEvent.VK_KATAKANA:
case KeyEvent.VK_HIRAGANA:
case KeyEvent.VK_JAPANESE_KATAKANA:
case KeyEvent.VK_JAPANESE_HIRAGANA:
case 0x0107: // KeyEvent.VK_INPUT_METHOD_ON_OFF: - in 1.3 only
case KeyEvent.VK_NUMPAD0: // Alt+NumPad keys
case KeyEvent.VK_NUMPAD1:
case KeyEvent.VK_NUMPAD2:
case KeyEvent.VK_NUMPAD3:
case KeyEvent.VK_NUMPAD4:
case KeyEvent.VK_NUMPAD5:
case KeyEvent.VK_NUMPAD6:
case KeyEvent.VK_NUMPAD7:
case KeyEvent.VK_NUMPAD8:
case KeyEvent.VK_NUMPAD9:
patch = false;
break;
}
}
if (patch) {
ignoreNextTyped = true;
}
} else if ((key.getModifiers() & InputEvent.META_MASK) != 0) { // Explicit patch for the keyTyped sent after Meta+key for Mac OS X
ignoreNextTyped = true;
} else if ((key.getModifiers() & InputEvent.CTRL_MASK) != 0 &&
(key.getModifiers() & InputEvent.SHIFT_MASK) != 0 &&
(key.getKeyCode() == KeyEvent.VK_ADD || key.getKeyCode() == KeyEvent.VK_SUBTRACT)) {
// Explicit patch for keyTyped sent without the Ctrl+Shift modifiers on Mac OS X - see issue #39835
ignoreNextTyped = true;
}
}
resetContext(); // reset context when resolved
// The global context cannot be reset because
// this is just a situation when the editor keymap
// does not know but the system or other
}
if (context != null && ret == null) { // no action found when in context
// Letting to return null in order to give chance to other keymaps
// ret = contextKeyNotFoundAction;
}
}
// Reset global context if a valid action is found
if (ret != null && !(ret instanceof KeymapSetContextAction) && (ret != EMPTY_ACTION)) {
resetGlobalContext();
}
if (compatibleIgnoreNextTyped) {
// #44307 = disabled extra ignoreNextTyped patches for past JDKs
// Explicit patch for Ctrl+Space - eliminating the additional KEY_TYPED sent
if (key == KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, InputEvent.CTRL_MASK)) {
ignoreNextTyped = true;
}
}
/* System.out.println("key=" + key + ", keyChar=" + (int)key.getKeyChar() + ", keyCode=" + key.getKeyCode() + ", keyModifiers=" + key.getModifiers() // NOI18N
+ ", ignoreNextTyped=" + ignoreNextTyped + ", context=" + context // NOI18N
+ ", returning action=" + ((ret == EMPTY_ACTION) ? "EMPTY_ACTION" : ((ret == null) ? "null" : ((ret instanceof javax.swing.text.TextAction) // NOI18N
? ret.getValue(javax.swing.Action.NAME) : ret.getClass()))));
*/
return ret;
}
public KeyStroke[] getBoundKeyStrokes() {
return (context != null) ? context.getBoundKeyStrokes()
: delegate.getBoundKeyStrokes();
}
public Action[] getBoundActions() {
return (context != null) ? context.getBoundActions()
: delegate.getBoundActions();
}
public KeyStroke[] getKeyStrokesForAction(Action a) {
return (context != null) ? context.getKeyStrokesForAction(a)
: delegate.getKeyStrokesForAction(a);
}
public boolean isLocallyDefined(KeyStroke key) {
return (context != null) ? context.isLocallyDefined(key)
: delegate.isLocallyDefined(key);
}
public void addActionForKeyStroke(KeyStroke key, Action a) {
if (context != null) {
context.addActionForKeyStroke(key, a);
} else {
delegate.addActionForKeyStroke(key, a);
}
}
public void removeKeyStrokeBinding(KeyStroke key) {
if (context != null) {
context.removeKeyStrokeBinding(key);
} else {
delegate.removeKeyStrokeBinding(key);
}
}
public void removeBindings() {
if (context != null) {
context.removeBindings();
} else {
delegate.removeBindings();
}
}
public Keymap getResolveParent() {
return (context != null) ? context.getResolveParent()
: delegate.getResolveParent();
}
public void setResolveParent(Keymap parent) {
if (context != null) {
context.setResolveParent(parent);
} else {
delegate.setResolveParent(parent);
}
}
public String toString() {
return "MK: name=" + getName(); // NOI18N
}
/** Internal class used to set the context */
class KeymapSetContextAction extends AbstractAction {
Keymap contextKeymap;
static final long serialVersionUID =1034848289049566148L;
KeymapSetContextAction(Keymap contextKeymap) {
this.contextKeymap = contextKeymap;
}
public void actionPerformed(ActionEvent evt) {
setContext(contextKeymap);
}
}
}