# HG changeset patch # Parent 272a0faced2acd06c8a976261d000f81555a9b22 diff --git a/o.n.core/manifest.mf b/o.n.core/manifest.mf --- a/o.n.core/manifest.mf +++ b/o.n.core/manifest.mf @@ -4,4 +4,4 @@ OpenIDE-Module-Layer: org/netbeans/core/resources/mf-layer.xml AutoUpdate-Show-In-Client: false AutoUpdate-Essential-Module: true -OpenIDE-Module-Specification-Version: 3.31 +OpenIDE-Module-Specification-Version: 3.32 diff --git a/o.n.core/src/org/netbeans/core/NbKeymap.java b/o.n.core/src/org/netbeans/core/NbKeymap.java --- a/o.n.core/src/org/netbeans/core/NbKeymap.java +++ b/o.n.core/src/org/netbeans/core/NbKeymap.java @@ -85,6 +85,18 @@ public final class NbKeymap implements Keymap, Comparator { private static final RequestProcessor RP = new RequestProcessor(NbKeymap.class); + + /** + * Extension, which indicates that the given binding should be removed by keymap + * profile. The marker is ignored in the 'Shortcuts' base directory. + */ + public static final String BINDING_REMOVED = "removed"; // NO18N + + /** + * Extension of the DataShadow files; private in loaders API + */ + public static final String SHADOW_EXT = "shadow"; // NOI18N + //for unit testing only private RequestProcessor.Task refreshTask; @@ -196,10 +208,12 @@ } } Map id2Dir = new HashMap(); // #170677 + boolean processingProfile = false; for (FileObject dir : dirs) { if (dir != null) { for (FileObject def : dir.getChildren()) { if (def.isData()) { + boolean removed = processingProfile && BINDING_REMOVED.equals(def.getExt()); KeyStroke[] strokes = Utilities.stringToKeys(def.getName()); if (strokes == null || strokes.length == 0) { LOG.log(Level.WARNING, "could not load parse name of " + def.getPath()); @@ -213,17 +227,38 @@ sub = null; } if (sub == null) { + if (removed) { + // nothing more to remove now + break; + } binder.put(strokes[i], sub = new Binding()); } binder = sub.nested; } - // XXX warn about conflicts here too: - binder.put(strokes[strokes.length - 1], new Binding(def)); - if (strokes.length == 1) { - String id = idForFile(def); - KeyStroke former = id2Dir.put(id, dir) == dir ? id2Stroke.get(id) : null; - if (former == null || compare(former, strokes[0]) > 0) { - id2Stroke.put(id, strokes[0]); + if (removed) { + if (strokes.length == 1) { + // remove the recorded stroke so it is not found by keyStrokeForAction + id2Stroke.values().remove(strokes[0]); + } + + KeyStroke stroke = strokes[strokes.length - 1]; + Binding b = binder.get(stroke); + if (b.nested != null) { + Binding b2 = new Binding(); + b2.nested.putAll(b.nested); + binder.put(stroke, b2); + } else { + binder.remove(stroke); + } + } else { + // XXX warn about conflicts here too: + binder.put(strokes[strokes.length - 1], new Binding(def)); + if (strokes.length == 1) { + String id = idForFile(def); + KeyStroke former = id2Dir.put(id, dir) == dir ? id2Stroke.get(id) : null; + if (former == null || compare(former, strokes[0]) > 0) { + id2Stroke.put(id, strokes[0]); + } } } } @@ -231,6 +266,7 @@ dir.removeFileChangeListener(bindingsListener); dir.addFileChangeListener(bindingsListener); } + processingProfile = true; } if (refresh) { // Update accelerators of existing actions after switching keymap. @@ -394,7 +430,7 @@ * else just returns file path (usual for more modern registrations). */ private static String idForFile(FileObject f) { - if (f.hasExt("shadow")) { + if (f.hasExt(SHADOW_EXT)) { String path = (String) f.getAttribute("originalFile"); if (path != null && f.getSize() == 0) { f = FileUtil.getConfigFile(path); diff --git a/o.n.core/test/unit/src/org/netbeans/core/NbKeymapTest.java b/o.n.core/test/unit/src/org/netbeans/core/NbKeymapTest.java --- a/o.n.core/test/unit/src/org/netbeans/core/NbKeymapTest.java +++ b/o.n.core/test/unit/src/org/netbeans/core/NbKeymapTest.java @@ -220,6 +220,42 @@ assertEquals(KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.CTRL_MASK), a.getValue(Action.ACCELERATOR_KEY)); } + /* + * Checks that: + * - C-A is properly masked in one profile, visible in other profile + * - from two sequences C-C S-B, C-C S-S, only one is properly masked in a profile + */ + public void testKeymapMasksShortcut() throws Exception { + make("Shortcuts/C-A.instance").setAttribute("instanceCreate", new DummyAction("one")); + make("Shortcuts/C-C S-B.instance").setAttribute("instanceCreate", new DummyAction("two")); + make("Shortcuts/C-C S-C.instance").setAttribute("instanceCreate", new DummyAction("four")); + make("Keymaps/NetBeans/C-C S-B.removed"); + make("Keymaps/NetBeans/C-A.removed"); + make("Keymaps/Eclipse/C-A.instance").setAttribute("instanceCreate", new DummyAction("three")); + NbKeymap km = new NbKeymap(); + KeyStroke controlA = KeyStroke.getKeyStroke(KeyEvent.VK_A, KeyEvent.CTRL_MASK); + + assertNull("should be masked", km.getAction(controlA)); + + KeyStroke controlC = KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.CTRL_MASK); + KeyStroke shiftB = KeyStroke.getKeyStroke(KeyEvent.VK_B, KeyEvent.SHIFT_MASK); + KeyStroke shiftC = KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.SHIFT_MASK); + + Action a = km.getAction(controlC); + assertNotNull("other binding must prevail", a); + a.actionPerformed(null); + + assertNull("should be masked", km.getAction(shiftB)); + + a = km.getAction(controlC); + a.actionPerformed(null); + assertEquals("four", km.getAction(shiftC).getValue(Action.NAME)); + + FileUtil.getConfigFile("Keymaps").setAttribute("currentKeymap", "Eclipse"); + assertTrue(km.waitFinished()); + assertEquals("three", km.getAction(controlA).getValue(Action.NAME)); + } + public void testMultiKeyShortcuts() throws Exception { final AtomicReference ran = new AtomicReference(); class A extends AbstractAction { diff --git a/options.keymap/apichanges.xml b/options.keymap/apichanges.xml new file mode 100644 --- /dev/null +++ b/options.keymap/apichanges.xml @@ -0,0 +1,108 @@ + + + + + + + Keymap Options API + + + + + Allows a Profile to remove (override) global keymap entry + + + + + +

+ In order to remove a keymap entry, create a file in the profile's + directory, with extension remove: +

+

+                        <file name="DS-O.remove"/>
+                    
+

+ No attributes are necessary. The system will treat DS-O + key in the appropriate keymap profile as undefined. The convention should + be only used in keymap profiles, not in the base keymap. +

+
+ +
+
+ + + + + + Change History for the Keymap Options API + + + + + + +

Introduction

+ +

This document lists changes made to the Keymap Options API.

+ + +
+ + +

@FOOTER@

+ + +
+
diff --git a/options.keymap/manifest.mf b/options.keymap/manifest.mf --- a/options.keymap/manifest.mf +++ b/options.keymap/manifest.mf @@ -2,7 +2,7 @@ OpenIDE-Module: org.netbeans.modules.options.keymap OpenIDE-Module-Localizing-Bundle: org/netbeans/modules/options/keymap/Bundle.properties OpenIDE-Module-Layer: org/netbeans/modules/options/keymap/mf-layer.xml -OpenIDE-Module-Specification-Version: 1.18 +OpenIDE-Module-Specification-Version: 1.19 AutoUpdate-Show-In-Client: false AutoUpdate-Essential-Module: true diff --git a/options.keymap/nbproject/project.properties b/options.keymap/nbproject/project.properties --- a/options.keymap/nbproject/project.properties +++ b/options.keymap/nbproject/project.properties @@ -1,5 +1,6 @@ javac.compilerargs=-Xlint:unchecked javac.source=1.6 javadoc.arch=${basedir}/arch.xml +javadoc.apichanges=${basedir}/apichanges.xml test.config.stableBTD.includes=**/*Test.class diff --git a/options.keymap/src/org/netbeans/core/options/keymap/spi/KeymapManager.java b/options.keymap/src/org/netbeans/core/options/keymap/spi/KeymapManager.java --- a/options.keymap/src/org/netbeans/core/options/keymap/spi/KeymapManager.java +++ b/options.keymap/src/org/netbeans/core/options/keymap/spi/KeymapManager.java @@ -44,6 +44,7 @@ package org.netbeans.core.options.keymap.spi; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; diff --git a/options.keymap/src/org/netbeans/modules/options/keymap/LayersBridge.java b/options.keymap/src/org/netbeans/modules/options/keymap/LayersBridge.java --- a/options.keymap/src/org/netbeans/modules/options/keymap/LayersBridge.java +++ b/options.keymap/src/org/netbeans/modules/options/keymap/LayersBridge.java @@ -44,8 +44,11 @@ package org.netbeans.modules.options.keymap; +import java.awt.event.ActionListener; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; @@ -69,6 +72,8 @@ import org.openide.loaders.DataFolder; import org.openide.loaders.DataObject; import org.openide.loaders.DataShadow; +import org.openide.util.Exceptions; +import org.openide.util.Lookup; import org.openide.util.NbBundle; /** @@ -79,6 +84,11 @@ @org.openide.util.lookup.ServiceProvider(service=org.netbeans.core.options.keymap.spi.KeymapManager.class) public class LayersBridge extends KeymapManager { + /** + * Extension for DataObjects, which cause an action to be removed from the parent (general) keymap. + */ + private static final String EXT_REMOVED = "removed"; // NOI18N + private static final Logger LOG = Logger.getLogger(LayersBridge.class.getName()); static final String KEYMAPS_FOLDER = "Keymaps"; @@ -221,6 +231,12 @@ new HashMap>> (); /** + * The base keymap, shared for all profiles. Used as a baseline when generating + * 'removed' instructions for a profile. + */ + private volatile Map> baseKeyMap; + + /** * Returns Map (GlobalAction > Set (String (shortcut))). */ public Map> getKeymap (String profile) { @@ -229,10 +245,20 @@ Map> m = readKeymap (root); root = getRootFolder (KEYMAPS_FOLDER, profile); overrideWithKeyMap(m, readKeymap(root), profile); + m.remove(REMOVED); keymaps.put (profile, m); } return Collections.unmodifiableMap (keymaps.get (profile)); } + + private Map> getBaseKeyMap() { + if (baseKeyMap == null) { + DataFolder root = getRootFolder (SHORTCUTS_FOLDER, null); + Map> m = readKeymap (root); + baseKeyMap = m; + } + return baseKeyMap; + } /** * Overrides the base shortcut map with contents of the Keymap. If keymap specifies @@ -286,12 +312,14 @@ /** * Returns Map (GlobalAction > Set (String (shortcut))). */ - public Map> getDefaultKeymap (String profile) { + public synchronized Map> getDefaultKeymap (String profile) { if (!keymapDefaults.containsKey (profile)) { DataFolder root = getRootFolder (SHORTCUTS_FOLDER, null); + System.err.println(Arrays.asList(root.getChildren())); Map> m = readKeymap (root); root = getRootFolder (KEYMAPS_FOLDER, profile); - m.putAll (readKeymap (root)); + overrideWithKeyMap(m, readKeymap(root), profile); + m.remove(REMOVED); keymapDefaults.put (profile, m); } return Collections.unmodifiableMap (keymapDefaults.get (profile)); @@ -302,9 +330,20 @@ } /** + * Placeholder, which indicates shortcut(s) that should be removed. Must be used + * only internally ! + */ + private static final GlobalAction REMOVED = new GlobalAction(null, null, "") { + { + name = ""; // NOI18N + } + }; + + /** * Read keymap from one folder Map (GlobalAction > Set (String (shortcut))). */ private Map> readKeymap (DataFolder root) { + LOG.log(Level.FINEST, "Reading keymap from: {0}", root); Map> keymap = new HashMap> (); if (root == null) return keymap; @@ -314,7 +353,7 @@ if (dataObject instanceof DataFolder) continue; GlobalAction action = createAction (dataObject, null); if (action == null) continue; - String shortcut = dataObject.getName (); + String shortcut = dataObject.getPrimaryFile().getName(); LOG.log(Level.FINEST, "Action {0}: {1}, by {2}", new Object[] { action.getDisplayName(), @@ -331,6 +370,7 @@ return keymap; } + @Override public void deleteProfile (String profile) { FileObject root = FileUtil.getConfigFile(KEYMAPS_FOLDER); if (root == null) return; @@ -344,11 +384,13 @@ } // actionToShortcuts Map (GlobalAction > Set (String (shortcut)) + @Override public void saveKeymap (String profile, Map> actionToShortcuts) { // discard our cached copy first keymaps.remove(profile); - + keymapDefaults.remove(profile); // 1) get / create Keymaps/Profile folder + DataFolder defaultFolder = getRootFolder(SHORTCUTS_FOLDER, null); DataFolder folder = getRootFolder (KEYMAPS_FOLDER, profile); if (folder == null) { folder = getRootFolder (KEYMAPS_FOLDER, null); @@ -359,36 +401,41 @@ return; } } - saveKeymap (folder, actionToShortcuts, true); - - // FIXME: this rewrites the shared Shortcuts (default) folder; should be - // done only after switching the profile - folder = getRootFolder (SHORTCUTS_FOLDER, null); - saveKeymap (folder, actionToShortcuts, false); + saveKeymap (defaultFolder, folder, actionToShortcuts); } - private void saveKeymap (DataFolder folder, Map> actionToShortcuts, boolean add) { + private void saveKeymap (DataFolder defaultMap, DataFolder folder, Map> actionToShortcuts) { + LOG.log(Level.FINEST, "Saving keymap to: {0}", folder.getPrimaryFile().getPath()); // hack: initialize the actions map first getActions(); // 2) convert to: Map (String (shortcut AC-C X) > GlobalAction) Map shortcutToAction = shortcutToAction (actionToShortcuts); + Set definedShortcuts = new HashSet(shortcutToAction.keySet()); + // 3) delete obsolete DataObjects + FileObject targetDir = folder.getPrimaryFile(); + Enumeration en = folder.children (); while (en.hasMoreElements ()) { DataObject dataObject = (DataObject) en.nextElement (); + if (dataObject.getPrimaryFile().getExt().equals(EXT_REMOVED)) { + continue; + } GlobalAction a1 = (GlobalAction) shortcutToAction.get (dataObject.getName ()); if (a1 != null) { GlobalAction action = createAction (dataObject, null); if (action == null) continue; if (action.equals (a1)) { // shortcut already saved + LOG.log(Level.FINEST, "Found same binding: {0} -> {1}", new Object[] { dataObject.getName(), action.getId()}); shortcutToAction.remove (dataObject.getName ()); continue; } } - // obsolete shortcut + // obsolete shortcut. try { + LOG.log(Level.FINEST, "Removing obsolete binding: {0}", dataObject.getName()); dataObject.delete (); } catch (IOException ex) { ex.printStackTrace (); @@ -396,10 +443,24 @@ } // 4) add new shortcuts - if (!add) return; + en = defaultMap.children(); + while (en.hasMoreElements()) { + DataObject dataObject = (DataObject)en.nextElement(); + GlobalAction ga = (GlobalAction)shortcutToAction.get(dataObject.getName()); + if (ga == null) { + continue; + } + GlobalAction action = createAction(dataObject, null); + if (ga.equals(action)) { + LOG.log(Level.FINEST, "Leaving default shortcut: {0}", dataObject.getName()); + shortcutToAction.remove(dataObject.getName()); + } + } + Iterator it = shortcutToAction.keySet ().iterator (); while (it.hasNext ()) { String shortcut = (String) it.next (); + // check whether the DO does not already exist: GlobalAction action = (GlobalAction) shortcutToAction.get (shortcut); DataObject dataObject = actionToDataObject.get (action); if (dataObject == null) { @@ -409,11 +470,39 @@ } try { DataShadow.create (folder, shortcut, dataObject); + // remove the '.remove' file, if it exists: + FileObject f = targetDir.getFileObject(shortcut, EXT_REMOVED); + if (f != null) { + f.delete(); + } } catch (IOException ex) { ex.printStackTrace (); continue; } } + + // 5, mask out DataObjects from the global keymap, which are NOT present in this profile: + if (defaultMap != null) { + en = defaultMap.children(); + while (en.hasMoreElements()) { + DataObject dataObject = (DataObject) en.nextElement (); + if (definedShortcuts.contains(dataObject.getName())) { + continue; + } + try { + FileObject pf = dataObject.getPrimaryFile(); + // If the shortcut is ALSO defined in 'parent' folder, + // we cannot just 'delete' it, but also mask the parent by adding 'removed' file. + if (targetDir.getFileObject(pf.getName(), EXT_REMOVED) == null) { + LOG.log(Level.FINEST, "Masking out binding: {0}", pf.getName()); + folder.getPrimaryFile().createData(pf.getName(), EXT_REMOVED); + } + } catch (IOException ex) { + ex.printStackTrace (); + } + } + } + } private static DataFolder getRootFolder (String name1, String name2) { @@ -438,17 +527,66 @@ */ private GlobalAction createAction (DataObject dataObject, String prefix) { InstanceCookie ic = dataObject.getCookie(InstanceCookie.class); - if (ic == null) return null; + // handle any non-IC file as instruction to remove the action + if (ic == null) { + FileObject pf = dataObject.getPrimaryFile(); + if (!EXT_REMOVED.equals(pf.getExt())) { + LOG.log(Level.WARNING, "Invalid shortcut: {0}", dataObject); + } + // ignore the 'remove' file, if there's a shadow (= real action) present + if (FileUtil.findBrother(pf, "shadow") != null) { + // handle redefinition + removal: ignore the removal. + return null; + } + return REMOVED; + } try { Object action = ic.instanceCreate (); if (action == null) return null; if (!(action instanceof Action)) return null; - return new GlobalAction ((Action) action, prefix, dataObject.getPrimaryFile().getName()); + return createAction((Action) action, prefix, dataObject.getPrimaryFile().getName()); } catch (Exception ex) { ex.printStackTrace (); return null; } } + + // hack: hardcoded OpenIDE impl class name + field + private static final String OPENIDE_DELEGATE_ACTION = "org.openide.awt.GeneralAction$DelegateAction"; // NOI18N + private static Field KEY_FIELD; + + /** + * Hack, which allows to somehow extract actionId from OpenIDE actions. Public API + * does not exist for this. + * + * @param a + * @param prefix + * @param name + * @return + */ + private static GlobalAction createAction(Action a, String prefix, String name) { + String id = name; + + try { + if (a.getClass().getName().equals(OPENIDE_DELEGATE_ACTION)) { + if (KEY_FIELD == null) { + Class c = a.getClass(); + KEY_FIELD = c.getSuperclass().getDeclaredField("key"); // NOI18N + KEY_FIELD.setAccessible(true); + } + String key = (String)KEY_FIELD.get(a); + if (key != null) { + id = key; + } + } + } catch (NoSuchFieldException ex) { + Exceptions.printStackTrace(ex); + } catch (IllegalAccessException ex) { + Exceptions.printStackTrace(ex); + } + + return new GlobalAction(a, prefix, id); + } /** * converts: actionToShortcuts: Map (ShortcutAction > Set (String (shortcut AC-C X))) @@ -494,7 +632,7 @@ private static class GlobalAction implements ShortcutAction { private Action action; - private String name; + String name; private String id; /** @@ -539,12 +677,12 @@ @Override public boolean equals (Object o) { if (!(o instanceof GlobalAction)) return false; - return ((GlobalAction) o).action.equals (action); + return ((GlobalAction) o).action == action || ((GlobalAction) o).action.equals (action); } @Override public int hashCode () { - return action.hashCode (); + return action == null ? 111 : action.hashCode (); } @Override