diff -r cfdd8230bdb4 openide.util/src/org/openide/util/ContextAction.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.util/src/org/openide/util/ContextAction.java Wed Nov 19 02:57:18 2008 +0000 @@ -0,0 +1,557 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, 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-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ +package org.openide.util; + +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ImageIcon; + +/** + * An action which operates in the global selection context (a + * Lookup such as the return value of Utilities.actionsGlobalContext()). + * Context actions are sensitive to the presence or absence of a particular + * Java type in a Lookup. The presence or absence of objects of that type + * can changed based on things like what the user has selected, or what + * logical window contains focus. + *

+ * Context actions make it possible to have a global action (i.e. it doesn't + * have to know anything in particular about what it's going to operate on + * to be created) which nonetheless acts on whatever happens to be selected + * at some moment in time. + *

+ * This class is a replacement for most uses of NodeAction and CookieAction. + * @see org.openide.util.ContextAwareAction + * @see org.openide.util.Lookup + * @see org.openide.util.Utilities.actionsGlobalContext() + * @author Tim Boudreau + */ +public abstract class ContextAction extends AbstractAction implements ContextAwareAction, HelpCtx.Provider { + private final Class type; + private final StubListener stubListener = new StubListener(); + //A context aware instance which we use internally to trigger + //enabled changes as long as there is at least one property change + //listener attached to us. + //By having the same thing we return from createContextAwareInstance handle + //all internal state, we make it easy to make a survives-focus-change + //subclass just by overriding createStub() to make a stub which retains + //the last usable collection of objects + ActionStub stub; + static boolean unitTest; + + /** + * Create a new ContextAction which will operate on instances of + * type type + * @param type The type this action needs in its context in order to be + * invoked + */ + protected ContextAction(Class type) { + this (type, null, null); + } + + /** + * Create a new ContextAction which will operate on a type type, + * with the specified display name and icon. + * + * @param type The type this action needs in its context in order to be + * invoked + * @param displayName A localized display name + * @param icon An image to use as the action's icon + */ + protected ContextAction(Class type, String displayName, Image icon) { + this.type = type; + if (type == null) { + throw new NullPointerException ("Type null"); + } + if (displayName != null) { + putValue (Action.NAME, displayName); + } + if (icon != null) { + putValue (Action.SMALL_ICON, new ImageIcon (icon)); + } + putValue ("noIconInMenu", true); + } + + /** + * Whether or not the action is enabled. By default, determines if there + * are any instances of type type in the selection context + * lookup, and if there are, returns true. To refine this behavior further, + * override enabled (java.util.Collection). + * @return + */ + @Override + public final boolean isEnabled() { + ActionStub stubAction = getStub(); + Collection targets = stubAction.collection(); + boolean result = checkQuantity(targets) && stubAction.isEnabled(); + return result; + } + + boolean checkQuantity(Collection targets) { + return !targets.isEmpty(); + } + + /** + * Determine if this action should be enabled. This method will only be + * called if the size of the collection is > 0. The default implementation + * returns true. If you need to do some further + * test on the collection of objects to determine if the action should + * really be enabled or not, override this method do that here. + * + * @param targets A collection of objects of type type + * @return Whether or not the action should be enabled. + */ + protected boolean enabled(Collection targets) { + return true; + } + + /** + * Overridden to throw an UnsupportedOperationException. Do not call. + * @param newValue + */ + @Override + public final void setEnabled(boolean newValue) { + throw new UnsupportedOperationException(); + } + + /** + * Override to actually do whatever this action does. This method is + * passed the current collection of objects present in the selection + * context. + * @param targets The objects of type type, which + * this action will use to do whatever it does + */ + protected abstract void actionPerformed(Collection targets); + + /** + * Fetches the collection of objects this action will act on and passes + * them to actionPerformed(Collection). + * @param e The action event. Ignored. + */ + public final void actionPerformed(ActionEvent ignored) { + getStub().actionPerformed(null); + } + + /** + * Create an instance of this action over a particular context. This is + * used to handle cases such as popup menus, where a popup menu is created + * against whatever the selection is at the time of its creation; if the + * selection changes while the popup is onscreen, we do not want + * the popup to operate on the new selection; it should operate on the thing + * that the user right-clicked. So for a popup menu, an instance of this + * action is created over a snapshot-lookup - a snapshot + * of the context at the moment it is created. + * @param actionContext The context this action instance should operate on. + * @return + */ + public final Action createContextAwareInstance(Lookup actionContext) { + return createStub (actionContext); + } + + ActionStub createStub(Lookup actionContext) { + return new ActionStub(actionContext, this); + } + + private ActionStub createInternalStub () { + //Don't synchronize, just ensure we are only called from sync methods + assert Thread.holdsLock(this); + ActionStub result = (ActionStub) + createContextAwareInstance (Utilities.actionsGlobalContext()); + return result; + } + + private synchronized ActionStub getStub() { + if (stub == null && attached) { + stub = createInternalStub(); + stub.addPropertyChangeListener(stubListener); + } + return stub == null ? createInternalStub() : stub; + } + /** + * Get the help context, if any. Returns HelpCtx.DEFAULT_HELP + * by default. + * @return A help context + */ + public HelpCtx getHelpCtx() { + return HelpCtx.DEFAULT_HELP; + } + + @Override + public String toString() { + return super.toString() + "[type=" + type.getName() + "]"; + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ContextAction other = (ContextAction) obj; + if (this.type != other.type && (this.type == null || !this.type.equals(other.type))) { + return false; + } + return true; + } + + @Override + public int hashCode() { + int hash = 7; + hash = 47 * hash + (this.type != null ? this.type.hashCode() : 0); + return hash; + } + + @Override + public synchronized void addPropertyChangeListener(PropertyChangeListener listener) { + super.addPropertyChangeListener(listener); + int count = getPropertyChangeListeners().length; + if (count == 1) { + addNotify(); + } + } + + @Override + public synchronized void removePropertyChangeListener(PropertyChangeListener listener) { + super.removePropertyChangeListener(listener); + int count = getPropertyChangeListeners().length; + if (count == 0) { + removeNotify(); + } + } + + volatile boolean attached; + void addNotify() { + assert Thread.holdsLock(this); + attached = true; + stub = getStub(); + } + + void removeNotify() { + assert Thread.holdsLock(this); + attached = false; + stub.removePropertyChangeListener(stubListener); + stub = null; + } + + //for unit tests + synchronized Collection stubCollection() { + return stub == null ? null : stub.collection(); + } + + private class StubListener implements PropertyChangeListener { + + public void propertyChange(PropertyChangeEvent evt) { + if ("enabled".equals(evt.getPropertyName())) { //NOI18N + firePropertyChange (evt.getPropertyName(), + evt.getOldValue(), evt.getNewValue()); + } + } + + } + + //Inner stub action class which delegates to the parent action's methods. + //Used both for context aware instances, and for internal state for + //ContextAction instances + private static class ActionStub implements Action, LookupListener { + private final Lookup.Result lkpResult; + private final Map pairs = new HashMap(); + private final PropertyChangeSupport supp = new PropertyChangeSupport(this); + private final Lookup context; + protected final ContextAction parent; + protected boolean enabled; + + ActionStub(Lookup context, ContextAction parent) { + assert context != null; + this.context = context; + this.parent = parent; + lkpResult = context.lookupResult(parent.type); + lkpResult.addLookupListener(this); + if (getClass() == ActionStub.class) { + //avoid superclass call to Retained.collection() before + //it has initialized + enabled = isEnabled(); + } + } + + public Object getValue(String key) { + Object result = pairs.get(key); + if (result == null) { + result = parent.getValue(key); + } + return result; + } + + Collection collection() { + return lkpResult.allInstances(); + } + + public void putValue(String key, Object value) { + Object old = pairs.put(key, value); + boolean fire = (old == null) != (value == null); + if (fire) { + fire = value != null && !value.equals(old); + if (fire) { + supp.firePropertyChange (key, old, value); + } + } + } + + public void setEnabled(boolean b) { + //Will throw exception + parent.setEnabled(b); + } + + public boolean isEnabled() { + Collection targets = collection(); + assert targets != null; + assert parent != null; + return targets.isEmpty() ? false : parent.checkQuantity(targets) && + parent.enabled(targets); + } + + public void addPropertyChangeListener(PropertyChangeListener listener) { + supp.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + supp.removePropertyChangeListener(listener); + } + + public void actionPerformed(ActionEvent e) { + assert isEnabled() : "Not enabled: " + this; + Collection targets = collection(); + parent.actionPerformed(targets); + } + + void enabledChanged(final boolean enabled) { + Mutex.EVENT.readAccess(new Runnable() { + public void run() { + supp.firePropertyChange("enabled", !enabled, enabled); //NOI18N + if (unitTest) { + synchronized (parent) { + parent.notifyAll(); + } + synchronized (this) { + this.notifyAll(); + } + } + } + }); + } + + public void resultChanged(LookupEvent ev) { + boolean old = enabled; + enabled = isEnabled(); + if (old != enabled) { + enabledChanged (enabled); + } + } + + @Override + public String toString() { + return super.toString() + "[name=" + getValue (NAME) + //NOI18N + "delegating={"+ parent + "} context=" + //NOI18N + context +"]"; //NOI18N + } + } + + /** + * Subclass of ContextAction which does not support multi-selection - + * like ContextAction, it is sensitive to a particular type. However, + * it only is enabled if there is exactly one object of type type + * in the selection. + * @param The type this action is sensitive to + */ + public static abstract class Single extends ContextAction { + protected Single (Class type) { + super (type); + } + + protected Single(Class type, String displayName, Image icon) { + super (type, displayName, icon); + } + + /** + * Delegates to actionePerformed(T) with the first and + * only element of the collection. + * @param targets The objects this action may operate on + */ + @Override + protected final void actionPerformed(Collection targets) { + actionPerformed (targets.iterator().next()); + } + + /** + * Actually perform the action. + * @param target The only instance of T in the action + * context. + */ + protected abstract void actionPerformed (T target); + + @Override + boolean checkQuantity(Collection targets) { + return targets.size() == 1; + } + + /** + * Determine if this action should be enabled. This method will only be + * called if the size of the collection == 1. The default implementation + * returns true. If you need to do some further + * test on the collection of objects to determine if the action should + * really be enabled or not, override this method do that here. + * + * @param targets A collection of objects of type type + * @return Whether or not the action should be enabled. + */ + @Override + protected boolean enabled(Collection targets) { + //Overridden only in order to have different javadoc + return super.enabled(targets); + } + } + + /** + * A context action which, once enabled, remains enabled. + *

+ * The canonical example of this sort of action in the NetBeans IDE is + * NextErrorAction: It becomes enabled when the output window gains + * focus. But it should remain enabled when focus goes back to the + * editor, and still work against whatever context the output window + * gave it to work on. Such cases are rare but legitimate. + *

+ * Use judiciously - such actions are temporary memory + * leaks - the action will retain the last usable collection of + * objects it had to work on as long as there are any property + * change listeners attached to it. + * + * @param The type this object is sensitive to + */ + public static abstract class SurviveSelectionChange extends Single { + protected SurviveSelectionChange (Class type) { + super (type); + } + + protected SurviveSelectionChange(Class type, String displayName, Image icon) { + super (type, displayName, icon); + } + + @Override + ActionStub createStub(Lookup actionContext) { + return new RetainingStub (actionContext, this); + } + + @Override + boolean checkQuantity(Collection targets) { + return super.checkQuantity(targets) || stub != null && + ((RetainingStub) stub).retained.size() == 1; + } + + + private static final class RetainingStub extends ActionStub { + Collection retained; + RetainingStub(Lookup context, SurviveSelectionChange parent) { + super (context, parent); + assert parent != null; + assert context != null; + retained = super.collection(); + assert retained != null; + enabled = isEnabled(); + } + + @Override + Collection collection() { + boolean wasEnabled = enabled; + if (wasEnabled) { + Collection nue = super.collection(); + assert nue != null; + //If we were enabled, and now we have too many objects, + //become disabled, don't keep the old single object + if (!parent.checkQuantity(nue)) { + retained = nue; + } + if (!nue.isEmpty()) { + retained = nue; + } + } + return retained; + } + } + } + + /** + * ContextAction subclass which requires a specific number of objects + * (presumably > 1) in the context and no more or no less. + *

+ * The canonical example in the IDE is DiffAction, which can only + * be enabled if exactly two files are selected. + * + * @param The type of object this action is sensitive to + */ + public static abstract class ExactCount extends ContextAction { + private final int count; + /** + * Create a new action sensitive to exactly count + * instances of type present in the selection + * context. + * @param type The type this action is sensitive too + * @param count Precisely how many instances of type + * need to be in the selection context for isEnabled() to be true + * or actionPerformed() to be called. + */ + protected ExactCount (Class type, int count) { + super (type); + this.count = count; + } + + @Override + boolean checkQuantity(Collection targets) { + return targets.size() == count; + } + } +} diff -r cfdd8230bdb4 openide.util/test/unit/src/org/openide/util/ContextActionTest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.util/test/unit/src/org/openide/util/ContextActionTest.java Wed Nov 19 02:57:18 2008 +0000 @@ -0,0 +1,401 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, 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-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ + +package org.openide.util; + +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import javax.swing.Action; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.netbeans.junit.MockServices; +import org.netbeans.junit.NbTestCase; +import org.openide.util.lookup.AbstractLookup; +import org.openide.util.lookup.InstanceContent; +import org.openide.util.lookup.Lookups; +import static org.junit.Assert.*; + +/** + * + * @author Tim Boudreau + */ +public class ContextActionTest extends NbTestCase { + + public ContextActionTest(String x) { super (x); } + + @BeforeClass + public static void setUpClass() throws Exception { + } + + @AfterClass + public static void tearDownClass() throws Exception { + } + + private InstanceContent content; + private Lookup lkp; + private static final int TIMEOUT = 100; + + + @Before + @Override + public void setUp() { + //This will cause ContextAction instances to invoke notifyAll() + //when their enablement changes + ContextAction.unitTest = true; + MockServices.setServices(Provider.class); + ContextGlobalProvider x = Lookup.getDefault().lookup (ContextGlobalProvider.class); + assertNotNull (x); + assertTrue (x instanceof Provider); + Provider p = (Provider) x; + content = p.content; + lkp = p.lkp; + //some sanity checks + setContent ("hello"); + assertEquals ("hello", lkp.lookupAll(String.class).iterator().next()); + assertEquals ("hello", Utilities.actionsGlobalContext().lookupAll(String.class).iterator().next()); + clearContent(); + assertEquals (null, lkp.lookup(String.class)); + assertEquals (null, Utilities.actionsGlobalContext().lookup(String.class)); + assertEquals (0, Utilities.actionsGlobalContext().lookupAll(Object.class).size()); + } + + static Provider instance; + public static class Provider implements ContextGlobalProvider { + private final Lookup lkp; + private final InstanceContent content; + public Provider () { + lkp = new AbstractLookup (content = new InstanceContent()); + instance = this; + } + + public Lookup createGlobalContext() { + return lkp; + } + + } + + void setContent(Object... stuff) { + content.set(Arrays.asList(stuff), null); + } + + void clearContent() { + content.set(Collections.EMPTY_SET, null); + } + + public void testEnablement() { + assertEquals (0, lkp.lookupAll(String.class).size()); //sanity check + A a = new A(); + assertFalse (a.isEnabled()); + setContent ("testEnablement"); + assertEquals (1, lkp.lookupAll(String.class).size()); //sanity check + assertTrue (a.isEnabled()); + clearContent(); + assertFalse (a.isEnabled()); + } + + public void testEnablementFired() throws Exception { + System.out.println("testEnablementFired"); + A a = new A(); + assertEquals (0, lkp.lookupAll(String.class).size()); + PCL pcl = new PCL(); + a.addPropertyChangeListener(pcl); + assertFalse (a.isEnabled()); + setContent ("testEnablementFired"); + synchronized (a) { + a.wait(TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + assertTrue (a.isEnabled()); + pcl.assertEnabledChangedTo(true); + clearContent(); + assertFalse (a.isEnabled()); + synchronized (a) { + a.wait (TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + pcl.assertEnabledChangedTo(false); + setContent("woo"); + synchronized (a) { + a.wait (TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + pcl.assertEnabledChangedTo(true); + setContent ("hello", "goodbye"); + synchronized (a) { + a.wait (TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + pcl.assertEnabledChangedTo(false); + assertFalse (a.isEnabled()); + clearContent(); + synchronized (a) { + a.wait (TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + pcl.assertNotFired(); + a.removePropertyChangeListener(pcl); + setContent("hmm"); + assertTrue (a.isEnabled()); + pcl.assertNotFired(); + } + + public void testContextInstancesDoNotInterfereWithEachOtherOrParent() throws Exception { + System.out.println("testContextInstancesDoNotInterfereWithEachOtherOrParent"); + A a = new A(); + assertNull (Utilities.actionsGlobalContext().lookup(String.class)); //sanity check + assertEquals ("A", a.getValue(Action.NAME)); + Action a1 = a.createContextAwareInstance(Lookup.EMPTY); + assertFalse (a.isEnabled()); + assertEquals ("A", a1.getValue(Action.NAME)); + Action a2 = a.createContextAwareInstance(Lookups.fixed("testGeneralBehavior")); + assertTrue (a2.isEnabled()); + assertFalse (a.isEnabled()); + setContent ("foo"); + assertTrue (a.isEnabled()); + assertFalse (a1.isEnabled()); + assertTrue (a2.isEnabled()); + clearContent(); + assertFalse (a.isEnabled()); + assertTrue (a2.isEnabled()); + } + + public void testContextInstancesAreIndependent() throws Exception { + System.out.println("testContextInstancesAreIndependent"); + A a = new A(); + assertNull (Utilities.actionsGlobalContext().lookup(String.class)); //sanity check + InstanceContent ic = new InstanceContent(); + Lookup l = new AbstractLookup (ic); + Action a3 = a.createContextAwareInstance(l); + assertFalse (a3.isEnabled()); + PCL pcl = new PCL(); + a3.addPropertyChangeListener(pcl); + setContent ("fuddle"); + a.assertNotPerformed(); + assertTrue (a.isEnabled()); + assertFalse (a3.isEnabled()); + synchronized (a3) { + //should time out if test is going to pass + a3.wait(TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + pcl.assertNotFired(); + ic.set(Collections.singleton("boo"), null); + synchronized (a3) { + a3.wait(TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + pcl.assertEnabledChangedTo(true); + clearContent(); + assertTrue (a3.isEnabled()); + assertFalse (a.isEnabled()); + } + + public void testGetSetValue() { + System.out.println("testGetSetValue"); + A a = new A(); + Action a1 = a.createContextAwareInstance(Lookup.EMPTY); + Action a2 = a.createContextAwareInstance(Lookups.fixed("testGeneralBehavior")); + a.putValue ("foo", "bar"); + assertEquals("bar", a.getValue("foo")); + assertEquals("bar", a1.getValue("foo")); + assertEquals("bar", a2.getValue("foo")); + + a1.putValue ("x", "y"); + assertNull (a.getValue("x")); + assertEquals ("y", a1.getValue("x")); + a.putValue("x", "z"); + assertEquals ("y", a1.getValue("x")); + assertEquals ("z", a.getValue("x")); + } + + public void testSurviveFocusChange() throws InterruptedException { + System.out.println("testSurviveFocusChange"); + B b = new B(); + assertFalse (b.isEnabled()); + setContent ("testSurviveFocusChange"); + assertTrue (b.isEnabled()); + PCL pcl = new PCL(); + b.addPropertyChangeListener(pcl); + setContent ("x"); + assertTrue (b.isEnabled()); + b.actionPerformed((ActionEvent)null); + b.assertPerformed("x"); + clearContent(); + assertTrue (b.isEnabled()); + b.actionPerformed((ActionEvent) null); + b.assertPerformed("x"); + setContent ("a", "b"); + assertFalse (b.isEnabled()); + synchronized (b) { + b.wait(TIMEOUT); + } + synchronized (pcl) { + pcl.wait(TIMEOUT); + } + assertFalse (b.createContextAwareInstance(Lookup.EMPTY).isEnabled()); + assertTrue (b.createContextAwareInstance(Lookups.fixed("foo")).isEnabled()); + assertFalse (b.createContextAwareInstance(Lookups.fixed("moo", "goo")).isEnabled()); + } + + public void testSingleNotEnabledOnMoreThanOne() throws Exception { + System.out.println("testSingleNotEnabledOnMoreThanOne"); + A a = new A(); + assertFalse (a.createContextAwareInstance(Lookups.fixed("moo", "goo")).isEnabled()); + assertTrue (a.createContextAwareInstance(Lookups.fixed("foo")).isEnabled()); + } + + public void testExactCount() throws Exception { + System.out.println("testExactCount"); + C c = new C(); + assertFalse (c.isEnabled()); + setContent ("1"); + assertFalse (c.isEnabled()); + setContent ("1", "2", "3", "4", "5"); + assertTrue (c.isEnabled()); + clearContent(); + assertFalse (c.isEnabled()); + setContent ("1", "2", "3", "4", "5"); + assertTrue (c.isEnabled()); + setContent ("1", "2", "3", "4", "5", "6"); + assertFalse (c.isEnabled()); + } + + private class PCL implements PropertyChangeListener { + PropertyChangeEvent evt; + + public void propertyChange(PropertyChangeEvent evt) { + if ("enabled".equals (evt.getPropertyName())) { + this.evt = evt; + } + synchronized (this) { + notifyAll(); + } + } + + void assertEnabledChangedTo (boolean val) { + PropertyChangeEvent old = this.evt; + this.evt = null; + assertNotNull (old); + Boolean b = Boolean.valueOf(val); + assertEquals (b, old.getNewValue()); + } + + void assertNotFired() { + assertNull (evt); + } + } + + private static class A extends ContextAction.Single { + String perfString; + A() { super (String.class, "A", null); } + + @Override + protected void actionPerformed(String target) { + perfString = target; + } + + void assertPerformed (String expected) { + String old = perfString; + perfString = null; + assertNotNull (old); + assertEquals (expected, old); + } + + void assertNotPerformed () { + assertNull (perfString); + } + } + + private static class B extends ContextAction.SurviveSelectionChange { + String perfString; + B() { super (String.class, "B", null); } + + @Override + protected void actionPerformed(String target) { + perfString = target; + } + + void assertPerformed (String expected) { + String old = perfString; + perfString = null; + assertNotNull (old); + assertEquals (expected, old); + } + } + + private static class C extends ContextAction.ExactCount { + Set perfStrings; + C() { super (String.class, 5); } + + + + void assertPerformed (Set expected) { + Set old = perfStrings; + perfStrings = null; + assertNotNull (old); + assertEquals (expected, old); + } + + @Override + protected void actionPerformed(Collection targets) { + perfStrings = new HashSet (targets); + } + } +} \ No newline at end of file