netbeans-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From sde...@apache.org
Subject [incubator-netbeans] branch master updated: Initial implementation of @ActionState support (#652)
Date Thu, 09 Aug 2018 11:09:40 GMT
This is an automated email from the ASF dual-hosted git repository.

sdedic pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-netbeans.git


The following commit(s) were added to refs/heads/master by this push:
     new af373e7  Initial implementation of @ActionState support (#652)
af373e7 is described below

commit af373e7702d1f9a09a09c33b2ea1f15ae4bf6310
Author: Svatopluk Dedic <svatopluk.dedic@oracle.com>
AuthorDate: Thu Aug 9 13:09:37 2018 +0200

    Initial implementation of @ActionState support (#652)
    
    Initial implementation of @ActionState support
---
 openide.awt/apichanges.xml                         |   22 +
 openide.awt/manifest.mf                            |    2 +-
 openide.awt/nbproject/project.xml                  |    2 +-
 .../modules/openide/awt/ActionProcessor.java       |  233 +++-
 .../modules/openide/awt/DefaultAWTBridge.java      |    9 +-
 .../src/org/openide/awt/ActionDefaultPerfomer.java |   17 +
 .../src/org/openide/awt/ActionRegistration.java    |   24 +
 openide.awt/src/org/openide/awt/ActionState.java   |  273 ++++
 openide.awt/src/org/openide/awt/Actions.java       |  135 +-
 .../src/org/openide/awt/AlwaysEnabledAction.java   |    2 +-
 openide.awt/src/org/openide/awt/ContextAction.java |  346 ++++-
 .../src/org/openide/awt/ContextManager.java        |   60 +-
 openide.awt/src/org/openide/awt/GeneralAction.java |  193 ++-
 openide.awt/src/org/openide/awt/InjectorAny.java   |   30 +-
 .../src/org/openide/awt/InjectorExactlyOne.java    |   28 +-
 .../src/org/openide/awt/PropertyMonitor.java       |  651 +++++++++
 .../src/org/openide/awt/StatefulAction.java        |  140 ++
 .../modules/openide/awt/ActionProcessorTest.java   |    3 +-
 .../openide/awt/StatefulActionProcessorTest.java   | 1380 ++++++++++++++++++++
 openide.util.ui/apichanges.xml                     |   16 +
 openide.util.ui/manifest.mf                        |    2 +-
 .../openide/util/actions/BooleanStateAction.java   |    5 +-
 22 files changed, 3470 insertions(+), 103 deletions(-)

diff --git a/openide.awt/apichanges.xml b/openide.awt/apichanges.xml
index 3c615af..889caf4 100644
--- a/openide.awt/apichanges.xml
+++ b/openide.awt/apichanges.xml
@@ -26,6 +26,28 @@
 <apidef name="awt">AWT API</apidef>
 </apidefs>
 <changes>
+    <change id="ToggleActions">
+        <api name="awt"/>
+        <summary>Support model-based enabled and check state of actions</summary>
+        <version major="7" minor="71"/>
+        <date day="30" month="8" year="2018"/>
+        <author login="sdedic"/>
+        <compatibility addition="yes" binary="compatible" semantic="compatible" deprecation="yes" deletion="no" modification="no"/>
+        <description>
+            <p>
+                Context Actions which provide <code>Action.SELECTED_KEY</code> value will be presented as checkbox (menu) or toggle button (toolbar) and
+                will reflect the state of an underlying model bean property, tracking models in Lookup and updating to model changes. Similar support
+                was added for <code>enabled</code> Action property.
+            </p>
+            <p>
+                All that was required to be implemented by individual Actions in their <a href="@org-openide-util-ui@/org/openide/util/ContextAwareAction.html">ContextAwareAction</a>
+                implementations, this allows to use just annotation for the same behaviour in most cases.
+            </p>
+        </description>
+        <class package="org.openide.awt" name="Actions"/>
+        <class package="org.openide.awt" name="ActionState"/>
+        <class package="org.openide.awt" name="ActionReference"/>
+    </change>
     <change id="NotificationCategory">
         <api name="awt"/>
         <summary>Add notification category to the NotificationDisplayer API</summary>
diff --git a/openide.awt/manifest.mf b/openide.awt/manifest.mf
index eb6bb71..c1027ad 100644
--- a/openide.awt/manifest.mf
+++ b/openide.awt/manifest.mf
@@ -2,5 +2,5 @@ Manifest-Version: 1.0
 OpenIDE-Module: org.openide.awt
 OpenIDE-Module-Localizing-Bundle: org/openide/awt/Bundle.properties
 AutoUpdate-Essential-Module: true
-OpenIDE-Module-Specification-Version: 7.70
+OpenIDE-Module-Specification-Version: 7.71
 
diff --git a/openide.awt/nbproject/project.xml b/openide.awt/nbproject/project.xml
index 4e01faf..7d008a9 100644
--- a/openide.awt/nbproject/project.xml
+++ b/openide.awt/nbproject/project.xml
@@ -47,7 +47,7 @@
                     <build-prerequisite/>
                     <compile-dependency/>
                     <run-dependency>
-                        <specification-version>9.3</specification-version>
+                        <specification-version>9.11</specification-version>
                     </run-dependency>
                 </dependency>
                 <dependency>
diff --git a/openide.awt/src/org/netbeans/modules/openide/awt/ActionProcessor.java b/openide.awt/src/org/netbeans/modules/openide/awt/ActionProcessor.java
index 3ee6e66..483e3c4 100644
--- a/openide.awt/src/org/netbeans/modules/openide/awt/ActionProcessor.java
+++ b/openide.awt/src/org/netbeans/modules/openide/awt/ActionProcessor.java
@@ -21,6 +21,7 @@ package org.netbeans.modules.openide.awt;
 
 import java.awt.event.ActionListener;
 import java.util.Collections;
+import java.util.EventListener;
 import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
@@ -41,6 +42,8 @@ import javax.lang.model.element.TypeElement;
 import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.ArrayType;
 import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.MirroredTypeException;
+import javax.lang.model.type.TypeKind;
 import javax.lang.model.type.TypeMirror;
 import javax.lang.model.util.ElementFilter;
 import javax.swing.Action;
@@ -51,6 +54,8 @@ import org.openide.awt.ActionID;
 import org.openide.awt.ActionReference;
 import org.openide.awt.ActionReferences;
 import org.openide.awt.ActionRegistration;
+import org.openide.awt.ActionState;
+import org.openide.awt.Actions;
 import org.openide.awt.DynamicMenuContent;
 import org.openide.filesystems.annotations.LayerBuilder;
 import org.openide.filesystems.annotations.LayerBuilder.File;
@@ -83,6 +88,7 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
         hash.add(ActionID.class.getCanonicalName());
         hash.add(ActionReference.class.getCanonicalName());
         hash.add(ActionReferences.class.getCanonicalName());
+//        hash.add(ActionState.class.getCanonicalName());
         return hash;
     }
     
@@ -156,6 +162,11 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
         TypeMirror p3 = type(Presenter.Popup.class);
         TypeMirror caa = type(ContextAwareAction.class);
         TypeMirror dmc = type(DynamicMenuContent.class);
+        TypeMirror at = type(Action.class);
+        TypeMirror ot = type(Object.class);
+        TypeMirror lt = type(EventListener.class);
+        TypeMirror vt = type(Void.class);
+        
         for (Element e : roundEnv.getElementsAnnotatedWith(ActionRegistration.class)) {
             ActionRegistration ar = e.getAnnotation(ActionRegistration.class);
             if (ar == null) {
@@ -260,6 +271,7 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
                 }
                 f.instanceAttribute("instanceCreate", Action.class);
             } else {
+                TypeMirror selectType = null;
                 if (key.length() == 0) {
                     f.methodvalue("instanceCreate", "org.openide.awt.Actions", "alwaysEnabled");
                 } else {
@@ -273,7 +285,7 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
                     try {
                         f.instanceAttribute("delegate", ActionListener.class, ar, null);
                     } catch (LayerGenerationException ex) {
-                        generateContext(e, f, ar);
+                        selectType = generateContext(e, f, ar);
                     }
                 }
                 if (ar.iconBase().length() > 0) {
@@ -285,8 +297,10 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
                     f.boolvalue("asynchronous", true);
                 }
                 if (ar.surviveFocusChange()) {
-                    f.boolvalue("surviveFocusChange", true);
+                    f.boolvalue("surviveFocusChange", true); 
                 }
+                processActionState(e, ar.enabledOn(), f, selectType, true, at, ot, lt, vt);
+                processActionState(e, ar.checkedOn(), f, selectType, false, at, ot, lt, vt);
             }
             f.write();
             
@@ -344,13 +358,222 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
         }
         return true;
     }
+    
+    private void processActionState(Element e, ActionState as, File f, TypeMirror selectType, boolean enable, 
+            TypeMirror actionType, TypeMirror objectType, TypeMirror eventListenerType, TypeMirror voidType) 
+        throws LayerGenerationException {
+        String property = as.property();
+        TypeMirror enabledType = null;
+        try {
+            as.type();
+        } catch (MirroredTypeException mte) {
+            enabledType = mte.getTypeMirror();
+        }
+        if (enabledType == null || enabledType.getKind() != TypeKind.DECLARED) {
+            throw new LayerGenerationException("Invalid enabled-on type in @ActionState", e, processingEnv, as, "type");
+        }
+        if (processingEnv.getTypeUtils().isSameType(enabledType, voidType)) {
+            return;
+        }
+        if (!as.useActionInstance()) {
+            if (processingEnv.getTypeUtils().isSameType(enabledType, objectType) && "".equals(as.property())) {
+                if (!enable) {
+                    throw new LayerGenerationException("Property must be specified", e, processingEnv, as);
+                }
+            }
+        }
+        DeclaredType dt = (DeclaredType) enabledType;
+        if (processingEnv.getTypeUtils().isSameType(dt, objectType)) {
+            if (selectType == null) {
+                throw new LayerGenerationException("Property owner type must be specified", e, processingEnv, as);
+            }
+            dt = (DeclaredType)selectType;
+        }
+        String dtName = processingEnv.getElementUtils().getBinaryName((TypeElement)dt.asElement()).toString();
+
+        f.stringvalue(enable ? "enableOnType" : "checkedOnType", dtName);
+        
+        if (!enable) {
+            f.boolvalue(Actions.ACTION_VALUE_TOGGLE, true);
+        }
+
+        boolean isAction = processingEnv.getTypeUtils().isSameType(dt, actionType);
+        switch (property) {
+            case "": 
+                if (as.useActionInstance()) {
+                    property = null;
+                    break;
+                }
+                property = enable ? "enabled" : Action.SELECTED_KEY; break;
+            case ActionState.NULL_VALUE: property = null;
+        }
+
+        TypeElement tel = (TypeElement)dt.asElement();
+        if (property != null && !isAction) {
+            ExecutableElement getter = null;
+            ExecutableElement invalidGetter = null;
+
+            String capitalizedName = Character.toUpperCase(property.charAt(0)) + property.substring(1);
+            String isGetter = "is" + capitalizedName;
+            String getGetter = "get" + capitalizedName;
+            
+            for (ExecutableElement el : ElementFilter.methodsIn(processingEnv.getElementUtils().getAllMembers(tel))) {
+                if (el.getSimpleName().contentEquals(isGetter)) {
+                    if (!el.getParameters().isEmpty()) {
+                        invalidGetter = el;
+                    } else {
+                        getter = el;
+                        break;
+                    }
+                }
+                if (el.getSimpleName().contentEquals(getGetter)) {
+                    if (!el.getParameters().isEmpty()) {
+                        if (invalidGetter == null) {
+                            invalidGetter = el;
+                        }
+                    } else {
+                        getter = el;
+                    }
+                }
+            }
+
+            if (getter == null) {
+                if (invalidGetter != null) {
+                    throw new LayerGenerationException("Getter " + dtName + "." + invalidGetter.toString() + " must take no parameters", 
+                            e, processingEnv, as, "property");
+                } else {
+                    throw new LayerGenerationException("Property " + property + " not found in " + dtName + ".", 
+                            e, processingEnv, as, "property");
+                }
+            }
+
+            Set<Modifier> mods = getter.getModifiers();
+            if (!mods.contains(Modifier.PUBLIC)) {
+                    throw new LayerGenerationException("Getter " + dtName + "." + getter.toString() + " must be public", 
+                            e, processingEnv, as, "property");
+            }
+        }
+        if (property != null) {
+            f.stringvalue(enable ? "enableOnProperty" : "checkedOnProperty", property); // NOI18N
+        }
+        
+        TypeMirror listenType = null;
+        try {
+            as.listenOn();
+            return;
+        } catch (MirroredTypeException ex) {
+            listenType = ex.getTypeMirror();
+        }
+        boolean explicitListenerType = !processingEnv.getTypeUtils().isSameType(listenType, eventListenerType);
+        
+        TypeElement lfaceElement = (TypeElement)((DeclaredType)listenType).asElement();
+        String lfaceName = lfaceElement.getSimpleName().toString();
+        String lfaceFQN = processingEnv.getElementUtils().getBinaryName(lfaceElement).toString();
+        String addName = "add" + lfaceName;
+        String removeName = "remove" + lfaceName;
+
+        if (explicitListenerType) {
+            if (lfaceElement.getKind() != ElementKind.INTERFACE) {
+                throw new LayerGenerationException(lfaceFQN + " is not an interface", e, processingEnv, as, "listenOn");
+            }
+            if (!lfaceElement.getModifiers().contains(Modifier.PUBLIC)) {
+                throw new LayerGenerationException(lfaceFQN + " is not public", e, processingEnv, as, "listenOn");
+            }
+        }
+
+        ExecutableElement addMethod = null;
+        ExecutableElement addCandidate = null;
+        ExecutableElement removeMethod = null;
+        ExecutableElement removeCandidate = null;
+        for (ExecutableElement el : ElementFilter.methodsIn(processingEnv.getElementUtils().getAllMembers(tel))) {
+            if (el.getSimpleName().contentEquals(addName)) {
+                addCandidate = el;
+                if (!el.getModifiers().contains(Modifier.PUBLIC) || el.getModifiers().contains(Modifier.STATIC)) {
+                    continue;
+                }
+                if (el.getParameters().size() == 1 && 
+                    processingEnv.getTypeUtils().isSameType(listenType, el.getParameters().get(0).asType())) {
+                    addMethod = el;
+                }
+            } else if (el.getSimpleName().contentEquals(removeName)) {
+                removeCandidate = el;
+                if (!el.getModifiers().contains(Modifier.PUBLIC) || el.getModifiers().contains(Modifier.STATIC)) {
+                    continue;
+                }
+                if (el.getParameters().size() == 1 && 
+                    processingEnv.getTypeUtils().isSameType(listenType, el.getParameters().get(0).asType())) {
+                    removeMethod = el;
+                }
+            }
+        }
+        if (addMethod == null) {
+            if (addCandidate != null) {
+                throw new LayerGenerationException("Method add" + 
+                        addCandidate.getSimpleName() + " must be public and take exactly one parameter of type " +
+                        lfaceName + ".", e, processingEnv, as, "listenOn");
+            } else if (explicitListenerType) {
+                throw new LayerGenerationException("Method add" + 
+                        lfaceName + " not found on " + dtName, e, processingEnv, as, "listenOn");
+            }
+        }
+        if (removeMethod == null) {
+            if (removeCandidate != null) {
+                throw new LayerGenerationException("Method remove" + 
+                        removeCandidate.getSimpleName() + " must be public and take exactly one parameter of type " +
+                        lfaceName + ".", e,processingEnv, as, "listenOn");
+            } else if (explicitListenerType) {
+                throw new LayerGenerationException("Method remove" + 
+                        lfaceName + " not found on " + dtName, e, processingEnv, as, "listenOn");
+            }
+        }
+        boolean wantsListen = explicitListenerType || (addMethod != null && removeMethod != null);
+        if (wantsListen) {
+            f.stringvalue(enable ? "enableOnChangeListener" : "checkedOnChangeListener", lfaceFQN);
+        }
+        if (!"".equals(as.listenOnMethod())) {
+            if (!explicitListenerType) {
+                throw new LayerGenerationException("Cannot specify listenOnMethod() without listenOn().", e,processingEnv, as, "listenOnMethod");
+            }
+            String m = as.listenOnMethod();
+            boolean found = false;
+            for (ExecutableElement el : ElementFilter.methodsIn(processingEnv.getElementUtils().getAllMembers(lfaceElement))) {
+                if (el.getSimpleName().contentEquals(m)) {
+                    found = true;
+                    break;
+                }
+            }
+            if (!found) {
+                throw new LayerGenerationException("Interface " + lfaceFQN + " does not contain method " + m,
+                    e, processingEnv, as, "listenOnMethod");
+            }
+            f.stringvalue(enable ? "enableOnMethod" : "checkedOnMethod", m);
+        }
+        
+        if (!"".equals(as.checkedValue())) {
+            switch (as.checkedValue()) {
+                case ActionState.NULL_VALUE:
+                    f.boolvalue(enable ? "enableOnNull" : "checkedOnNull", true);
+                    break;
+                case ActionState.NON_NULL_VALUE:
+                    f.boolvalue(enable ? "enableOnNull" : "checkedOnNull", false);
+                    break;
+                default:
+                    f.stringvalue(enable ? "enableOnValue" : "checkedOnValue", as.checkedValue());
+                    break;
+            }
+        }
+        if (as.useActionInstance()) {
+            f.stringvalue(enable ? "enableOnActionProperty" : "checkedOnActionProperty", 
+                    enable ? "enabled" : Action.SELECTED_KEY);
+        }
+    }
 
     private TypeMirror type(Class<?> type) {
         final TypeElement e = processingEnv.getElementUtils().getTypeElement(type.getCanonicalName());
         return e == null ? null : e.asType();
     }
 
-    private void generateContext(Element e, File f, ActionRegistration ar) throws LayerGenerationException {
+    private TypeMirror generateContext(Element e, File f, ActionRegistration ar) throws LayerGenerationException {
         ExecutableElement ee = null;
         ExecutableElement candidate = null;
         for (ExecutableElement element : ElementFilter.constructorsIn(e.getEnclosedElements())) {
@@ -396,7 +619,7 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
             f.stringvalue("injectable", processingEnv.getElementUtils().getBinaryName((TypeElement) e).toString());
             f.stringvalue("selectionType", "ANY");
             f.methodvalue("instanceCreate", "org.openide.awt.Actions", "context");
-            return;
+            return dt.getTypeArguments().get(0);
         }
         if (!dt.getTypeArguments().isEmpty()) {
             throw new LayerGenerationException("No type parameters allowed in ", ee);
@@ -407,7 +630,9 @@ public final class ActionProcessor extends LayerGeneratingProcessor {
         f.stringvalue("injectable", processingEnv.getElementUtils().getBinaryName((TypeElement)e).toString());
         f.stringvalue("selectionType", "EXACTLY_ONE");
         f.methodvalue("instanceCreate", "org.openide.awt.Actions", "context");
+        return ctorType;
     }
+    
     private String binaryName(TypeMirror t) {
         Element e = processingEnv.getTypeUtils().asElement(t);
         if (e != null && (e.getKind().isClass() || e.getKind().isInterface())) {
diff --git a/openide.awt/src/org/netbeans/modules/openide/awt/DefaultAWTBridge.java b/openide.awt/src/org/netbeans/modules/openide/awt/DefaultAWTBridge.java
index 1a585c2..fa21822 100644
--- a/openide.awt/src/org/netbeans/modules/openide/awt/DefaultAWTBridge.java
+++ b/openide.awt/src/org/netbeans/modules/openide/awt/DefaultAWTBridge.java
@@ -36,6 +36,7 @@ import org.openide.util.actions.BooleanStateAction;
 import org.openide.util.actions.SystemAction;
 import org.openide.util.lookup.ServiceProvider;
 import org.openide.util.actions.ActionPresenterProvider;
+import org.openide.util.actions.Presenter;
 
 /** Default implementation of presenters for various action types.
  */
@@ -46,6 +47,9 @@ public final class DefaultAWTBridge extends ActionPresenterProvider {
             BooleanStateAction b = (BooleanStateAction)action;
             return new Actions.CheckboxMenuItem (b, true);
         }
+        if (action.getValue(Actions.ACTION_VALUE_TOGGLE) != null) {
+            return new Actions.CheckboxMenuItem(action, true);
+        }
         if (action instanceof SystemAction) {
             SystemAction s = (SystemAction)action;
             return new Actions.MenuItem (s, true);
@@ -68,11 +72,12 @@ public final class DefaultAWTBridge extends ActionPresenterProvider {
         return item;
     }
     
+    @Override
     public Component createToolbarPresenter(Action action) {
         AbstractButton btn;
-        if (action instanceof BooleanStateAction) {
+        if ((action instanceof BooleanStateAction) || (action.getValue(Actions.ACTION_VALUE_TOGGLE) != null)) {
             btn = new JToggleButton();
-            Actions.connect(btn, (BooleanStateAction) action);
+            Actions.connect(btn, action);
         } else {
             btn = new JButton();
             Actions.connect(btn, action);
diff --git a/openide.awt/src/org/openide/awt/ActionDefaultPerfomer.java b/openide.awt/src/org/openide/awt/ActionDefaultPerfomer.java
index c2d1dbf..b4448d0 100644
--- a/openide.awt/src/org/openide/awt/ActionDefaultPerfomer.java
+++ b/openide.awt/src/org/openide/awt/ActionDefaultPerfomer.java
@@ -21,6 +21,8 @@ package org.openide.awt;
 import java.awt.event.ActionEvent;
 import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
+import static javax.swing.Action.ACTION_COMMAND_KEY;
 import org.netbeans.api.actions.Closable;
 import org.netbeans.api.actions.Editable;
 import org.netbeans.api.actions.Openable;
@@ -61,4 +63,19 @@ final class ActionDefaultPerfomer extends ContextAction.Performer<Object> {
             }
         }
     }
+
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        Object o = delegate.get("key"); // NOI18N
+        if (o == null) {
+            o = delegate.get(ACTION_COMMAND_KEY);
+        }
+        Object d= instDelegate == null ? null : instDelegate.get();
+        sb.append("PerformerDefault{id = ").append(Objects.toString(o))
+                .append(", type = ").append(type)
+                .append("}");
+        return sb.toString();
+    }
 }
diff --git a/openide.awt/src/org/openide/awt/ActionRegistration.java b/openide.awt/src/org/openide/awt/ActionRegistration.java
index 372dd0d..d897d27 100644
--- a/openide.awt/src/org/openide/awt/ActionRegistration.java
+++ b/openide.awt/src/org/openide/awt/ActionRegistration.java
@@ -127,4 +127,28 @@ public @interface ActionRegistration {
      */
     boolean lazy() default true;
 
+    /**
+     * Specifies a property that enables the action for context-sensitive actions. 
+     * The property can be on  the context object (implies single selection mode), or on another object
+     * type in the context Lookup. The default enables the action if the context
+     * object is present (with no additional constraints).
+     * <p/>
+     * Specify the value if the action should be enabled based on <b>certain property</b> and
+     * its value. See {@link ActionState} for detailed explanation of the
+     * state evaluation and tracking.
+     * 
+     * @return the specification of enabled state
+     * @since 7.71
+     */
+    ActionState enabledOn() default @ActionState(type=Void.class);
+    
+    /**
+     * Controls action's enable state. If unspecified, the action will not represent the state value,
+     * and will be presented as normal item or button. If specified, the action will be presented as
+     * checkbox or toggle button. * Similar to {@link #enableOn}, type and its property can be used to determine whether the
+     * action is checked or unchecked. See {@link ActionState} for more details.
+     * @return specification of the checked state.
+     * @since 7.71
+     */
+    ActionState checkedOn() default @ActionState(type=Void.class);
 }
diff --git a/openide.awt/src/org/openide/awt/ActionState.java b/openide.awt/src/org/openide/awt/ActionState.java
new file mode 100644
index 0000000..fda05ba
--- /dev/null
+++ b/openide.awt/src/org/openide/awt/ActionState.java
@@ -0,0 +1,273 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.openide.awt;
+
+import java.beans.PropertyChangeListener;
+import javax.swing.event.ChangeListener;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.EventListener;
+import javax.swing.Action;
+import org.openide.util.actions.Presenter;
+
+/**
+ * Specifies that the action behaviour is conditional and how the action should obtain the
+ * state for its presentation. The annotation is used as value for {@link ActionRegistration#enabledOn}
+ * and {@link ActionRegistration#checkedOn} to control action's enabled or checked state. The annotation
+ * can be only applied on <b>context actions</b>, which have a single parameter constructor,
+ * which accept the model object - see {@link Actions#context(java.lang.Class, boolean, boolean, org.openide.util.ContextAwareAction, java.lang.String, java.lang.String, java.lang.String, boolean)}.
+ * <p/>
+ * When used as {@link ActionRegistration#checkedOn} value, the annotated action will change
+ * to <b>toggle on/off action</b>, represented by a checkbox (menu) or a toggle button (toolbar).
+ * The action state will track the model property specified by this
+ * annotation. Toggle actions become <b>enabled</b> when the model object is
+ * found in the Lookup, and <b>checked</b> (or toggled on) when the model property
+ * is set to a defined value (usually {@code true})
+ * <p/>
+ * The {@link #type} specifies type which is searched for in the {@link Lookup} and
+ * if an instance is found, it is used as the model object. If the {@link #type} is not set, 
+ * the <b>the type inferred from Action's
+ * constructor</b> (see {@link ActionRegistration}) will be used to find the model.
+ * <p/>
+ * The {@link #property} specifies bean property whose value should be used to
+ * determine checked state. The obtained value is compared using {@link #checkedValue}
+ * as follows:
+ * <ul>
+ * <li>a boolean or Boolean value is compared to {@link Boolean#TRUE} or the {@link #checkedValue},
+ * if present.
+ * <li>if {@link #checkValue} is {@link #NULL_VALUE}, the action is checked if and only if
+ * the value is {@code null}. 
+ * <li>if {@link #checkValue} is {@link #NON_NULL_VALUE}, the action is checked if and only if
+ * the value is not {@code null}. 
+ * <li>if the value type is an enum, its {@link Enum#name} is compared to {@link #checkValue}
+ * <li>the state will be {@code false} (unchecked) otherwise.
+ * <p/>
+ * If {@link #type} is set to {@link Action}.class, the annotated element <b>must
+ * be an {@link Action}</b> subclass. {@link Action#getValue} will be used to determine
+ * the state. The Action delegate <b>will not be instantiated eagerly</b>, but only
+ * after the necessary context type becomes available in Lookup. 
+ * This support minimizes from premature code loading for custom action implementations. 
+ * <b>Important note:</b> if your Action implements {@link ContextAwareAction},
+ * or one of the {@link Presenter} interfaces, it is eager and will be loaded immediately !
+ * <p/>
+ * Changes to the model object will be tracked using event listener pattern. The annotation-supplied
+ * delegate attempts to {@link PropertyChangeListener} and {@link ChangeListener} automatically; \
+ * other listener interfaces must be specified using {@link #listenOn}
+ * value. Finally, {@link #listenOnMethod} specifies which listener method will trigger
+ * state update; by default, all listener method calls will update the action state.
+ * <p/>
+ * The {@link ActionState} annotation may be also used as a value of {@link ActionRegistration#enabledOn()} 
+ * and causes the annotated Action to be <b>enabled</b> not only on presence of object of the context type,
+ * but also based on the model property. The property, enable value and listener is specified the
+ * same way as for "checked" state. See the above text.
+ * <p/>
+ * If a completely custom behaviour is desired, the system can finally delegate {@link Action#isEnabled} and
+ * {@link Action#getValue getValue}({@link Action#SELECTED_KEY}) to the action implementation itself: use {@link #useActionInstance()}
+ * value.
+ * <p/>
+ * Here are several examples of {@code @ActionState} usage:
+ * <p/>
+ * To define action, which <b>enables on modified DataObjects</b> do the following
+ * registration:
+ * <code><pre>
+ * &#64;ActionID(category = "Example", id = "example.SaveAction")
+ * &#64;ActionRegistration(displayName = "Save modified",
+ *     enabledOn = @ActionState(property = "modified")
+ * )
+ * public class ExampleAction implements ActionListener {
+ *     public ExampleAction(DataObject d) {
+ *         // ...
+ *     }
+ *     
+ *     public void actionPerformed(ActionEvent e) {
+ *         // ...
+ *     }
+ * }
+ * </pre></code>
+ * The action will be instantiated and run only after:
+ * <ul>
+ * <li>DataObject becomes available, and
+ * <li>its {@code modified} property becomes true
+ * </ul>
+ * 
+ * To create "toggle" action in toolbar or a menu, which changes state based on some property,
+ * you can code:
+ * <code><pre>
+ * enum SelectionMode {
+ *     Rectangular,
+ *     normal
+ * }
+ * &#64;ActionID(category = "Example", id = "example.RectSelection")
+ * &#64;ActionRegistration(displayName = "Toggle rectangular selection", checkedOn = &#64;ActionState(
+ *     property = "selectionMode", checkedValue = "Rectangular", listenOn = EditorStateListener.class)
+ * )
+ * public class RectangularSelectionAction implements ActionListener {
+ *     public RectangularSelectionAction(EditorInterface editor) {
+ *         // ...
+ *     }
+ *     &#64;Override
+ *     public void actionPerformed(ActionEvent e) {
+ *     }
+ * }
+ * </pre></code>
+ * The action enables when {@code EditorInterface} appears in the action Lookup. Then,
+ * its state will be derived from {@code EditorInterface.selectionMode} property. Since
+ * there's a custom listener interface for this value, it must be specified using {@link #listenOn}.
+ * <p/>
+ * Finally, if the action needs to perform its own special magic to check enable state, we 
+ * hand over final control to the action, but the annotation-introduced wrappers will still
+ * create action instance for a new model object, attach and detach listeners on it and ensure
+ * that UI will not be strongly referenced from the model for proper garbage collection:
+ * <code><pre>
+ * &#64;ActionID(category = "Example", id = "example.SelectPrevious")
+ * &#64;ActionRegistration(displayName = "Selects previous item", checkedOn = &#64;ActionState(
+ *     listenOn = ListSelectionListener.class, useActionInstance = true)
+ * )
+ * public class SelectPreviousAction extends AbstractAction {
+ *     private final ListSelectionModel model;
+ *     
+ *     public SelectPreviousAction(ListSelectionModel model) {
+ *         this.model = model;
+ *     }
+ *     &#64;Override
+ *     public boolean isEnabled() {
+ *         return model.getAnchorSelectionIndex() > 0;
+ *     }
+ *     &#64;Override
+ *     public void actionPerformed(ActionEvent e) {
+ *     }
+ * }
+ * </pre></code>
+ * The system will do the necessary bookkeeping, but the action decides using its
+ * {@link Action#isEnabled} implementation. 
+ * 
+ * @author sdedic
+ * @since 7.71
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.FIELD})
+public @interface ActionState {
+
+    /**
+     * The type which the action will look for in its context. The action
+     * becomes checked (enabled) if and only if there's at least one instance of such type
+     * present in the context. There are some special values that modify behaviour:
+     * <ul>
+     * <li><code>Object.class</code> (the default) the context object will be used and the property will be read
+     * from the context object. Only applicable if the action accepts single object.
+     * <li><code>Action.class</code>: the {@link #property} action value will be used, 
+     * as obtained by {@link Action#getValue}.
+     * </ul>
+     * If {@code @ActionState} is used in {@link ActionRegistration#enabledOn()}, the
+ * {@code type} can be left unspecified, defaulting to the context type for the action.
+     *
+     * @return type to work with.
+     */
+    public Class<?> type() default Object.class;
+
+    /**
+     * Property name whose value represents the state. The property must be a
+     * property on the {@link #type()} class; read-only properties are
+     * supported. If the target class supports attaching
+     * {@link PropertyChangeListener} or {@link ChangeListener}, the action will
+     * attach a listener ({@link PropertyChangeListener} takes precedence) and will fire
+     * appropriate state events when the property property changes.
+     * <p/>
+     * In the case that checked state is delegated to {@link Action}, the property
+     * default is different depending on the context the annotation is used:
+     * <ul>
+     * <li>if used to specify enable state ({@link ActionRegistration#enabledOn()}, the property defaults to "enabled"
+     * <li>if used as checked state ({@code @ActionState} directly annotates to element}, the property defaults to {@link Action#SELECTED_KEY}.
+     * <li>if the model is {@link Action}, {@link Action#getValue} is also used
+     * to obtain the value. 
+     * </ul>
+     * Note that although this value gives more flexibility than {@link #useActionInstance()} for Actions, 
+     * in the case where {@link #type}.{@link #property} is used to specify necessary guard condition,
+     * {@link #useActionInstance()} is necessary to perform custom check.
+     * @return property name.
+     */
+    public String property() default ""; // NOI18N
+    
+    /**
+     * The value which makes the action checked. Can be one of:
+     * <ul>
+     * <li><code>"true"</code>, <code>"false"</code> to represent boolean or Boolean values
+     * <li>String representation of an enum value, as obtained by {@link Enum#name()}
+     * <li><code>{@link #NULL_VALUE}</code> to indicate <code>null</code> value
+     * <li><code>{@link #NON_NULL_VALUE}</code> to indicate any non-null value
+     * <li>String representation of the value object, as obtained by {@link Object#toString}
+     * <li>
+     * </ul>
+     * @return value which indicates "set" state
+     */
+    public String checkedValue() default ""; // NOI18N
+    
+    /**
+     * Custom listener interface to monitor for changes. If undefined, then
+     * either {@link PropertyChangeListener} or {@link ChangeListener} will be 
+     * auto-detected from {@link #type} class.
+     * <p/>
+     * All listener methods will cause the system to re-evaluate enable and on/off 
+     * (if applicable) state for the action, unless {@link #listenOnMethod} is 
+     * also used.
+     * 
+     * @return custom listener interface.
+     */
+    public Class listenOn() default EventListener.class;
+    
+    /**
+     * Allows to pick one listener method, which will trigger action state update.
+     * The update will re-check both enable and on/off state (if applicable). 
+     * The action will however fire change events only if the state actually changes
+     * from the previous one.
+     * <p/>
+     * The default (empty) value means that all listener methods will cause
+     * state update. The value can be only specified together with {@link #listenOn} value.
+     * 
+     * @return listener method.
+     */
+    public String listenOnMethod() default ""; // NOI18N
+    
+    /**
+     * If true, the target system will delegate to the action instance itself.
+     * The action instance will not be created until the context object (or {@link #type()}
+     * becomes available and the guard {@link #property()} has the {@link #checkedValue() appropriate value}.
+     * <p/>
+     * After that, the system will delegate to {@link Action#isEnabled()} for enablement, or
+     * to {@link Action#getValue getValue}({@link Action#SELECTED_KEY}) for on/off state of the action.
+     * <p/>
+     * The annotated element <b>must</b> implement {@link Action} interface in order to use
+     * this value.
+     * @return whether the action instance itself should be ultimately for enable/check status
+     */
+    public boolean useActionInstance() default false;
+    
+    /**
+     * An explicit {@code null} value for {@link #checkedValue}, represents {@code null}
+     */
+    public static final String NULL_VALUE = "#null";
+
+    /**
+     * An explicit {@code null} value for {@link #checkedValue}, represents {@code non-null} 
+     */
+    public static final String NON_NULL_VALUE = "#non-null";
+}
diff --git a/openide.awt/src/org/openide/awt/Actions.java b/openide.awt/src/org/openide/awt/Actions.java
index 8675c3a..afacb480 100644
--- a/openide.awt/src/org/openide/awt/Actions.java
+++ b/openide.awt/src/org/openide/awt/Actions.java
@@ -65,6 +65,7 @@ import org.openide.util.LookupListener;
 import org.openide.util.Utilities;
 import org.openide.util.WeakListeners;
 import org.openide.util.actions.BooleanStateAction;
+import org.openide.util.actions.Presenter;
 import org.openide.util.actions.SystemAction;
 
 
@@ -81,6 +82,15 @@ public class Actions {
     public static final String ACTION_VALUE_VISIBLE = "openide.awt.actionVisible"; // NOI18N
     
     /**
+     * Key for {@link Action#getValue} to indicate that the action should be presented
+     * as toggle, if possible. Presenters may create checkbox item, toggle button etc.
+     * This is to avoid accessing the {@link Action#SELECTED_KEY} actual value during
+     * presenter construction, as evaluation may be expensive.
+     * @since 7.71
+     */
+    public static final String ACTION_VALUE_TOGGLE = "openide.awt.actionToggle"; // NOI18N
+    
+    /**
      * @deprecated should not be used
      */
     @Deprecated
@@ -179,7 +189,12 @@ public class Actions {
                 return;
             }
         }
-        Bridge b = new MenuBridge(item, action, popup);
+        Bridge b;
+        if ((item instanceof JCheckBoxMenuItem) && (action.getValue(Actions.ACTION_VALUE_TOGGLE) != null)) {
+            b = new CheckMenuBridge((JCheckBoxMenuItem)item, action, popup);
+        } else {
+            b = new MenuBridge(item, action, popup);
+        }
         b.prepare();
 
         if (item instanceof Actions.MenuItem) {
@@ -192,12 +207,28 @@ public class Actions {
     * @param item menu item
     * @param action action
     * @param popup create popup or menu item
+    * @deprecated Please use {@link #connect(javax.swing.JCheckBoxMenuItem, javax.swing.Action, boolean)}. 
+    * Have your action to implement properly {@link Action#getValue} for {@link Action#SELECTED_KEY}
     */
+    @Deprecated
     public static void connect(JCheckBoxMenuItem item, BooleanStateAction action, boolean popup) {
         Bridge b = new CheckMenuBridge(item, action, popup);
         b.prepare();
     }
 
+    /** Attaches checkbox menu item to boolean state action. The presenter connects to the
+     * {@link Action#SELECTED_KEY} action value
+     * 
+    * @param item menu item
+    * @param action action
+    * @param popup create popup or menu item
+    * @since 7.71
+    */
+    public static void connect(JCheckBoxMenuItem item, Action action, boolean popup) {
+        Bridge b = new CheckMenuBridge(item, action, popup);
+        b.prepare();
+    }
+
     /** Connects buttons to action.
     * @param button the button
     * @param action the action
@@ -240,8 +271,17 @@ public class Actions {
                 return;
             }
         }
-        Bridge b = new ButtonBridge(button, action);
+        Bridge b;
+        if (action instanceof BooleanStateAction) {
+            b = new BooleanButtonBridge(button, (BooleanStateAction)action);
+        }
+        if (action.getValue(Actions.ACTION_VALUE_TOGGLE) != null) {
+            b = new BooleanButtonBridge(button, action);
+        } else {
+            b = new ButtonBridge(button, action);
+        }
         b.prepare();
+        button.putClientProperty(DynamicMenuContent.HIDE_WHEN_DISABLED, action.getValue(DynamicMenuContent.HIDE_WHEN_DISABLED));
     }
 
     /** Connects buttons to action.
@@ -634,6 +674,37 @@ public class Actions {
      *    }
      *  }
      * </pre>
+     * <p/>
+     * Further attributes are defined to control action's enabled and checked state. 
+     * Attributes which control enable state are prefixed by "{@code enableOn}". Attributes
+     * controlling checked state have prefix "{@code checkedOn}":
+     * <code><pre>
+     * &lt;file name="action-pkg-ClassName.instance"&gt;
+     *   &lt;!-- Enable on certain type in Lookup --&gt;
+     *   &lt;attr name="enableOnType" stringvalue="qualified.type.name"/&gt;
+     * 
+     *   &lt;!-- Monitor specific property in that type --&gt;
+     *   &lt;attr name="enableOnProperty" stringvalue="propertyName"/&gt;
+     * 
+     *   &lt;!-- The property value, which corresponds to enabled action.
+     *           Values "#null" and "#non-null" are treated specially.
+     *   --&gt;
+     *   &lt;attr name="enableOnValue" stringvalue="propertyName"/&gt;
+     * 
+     *   &lt;!-- Name of custom listener interface --&gt;
+     *   &lt;attr name="enableOnChangeListener" stringvalue="qualifier.listener.interface"/&gt;
+     * 
+     *   &lt;!-- Name of listener method that triggers state re-evaluation  --&gt;
+     *   &lt;attr name="enableOnMethod" stringvalue="methodName"/&gt;
+     * 
+     *   &lt;!-- Delegate to the action instance for final decision --&gt;
+     *   &lt;attr name="enableOnActionProperty" stringvalue="actionPropertyName"/&gt;
+     * 
+     *   &lt;!-- ... --&gt;
+     * 
+     * &lt;/file&gt;
+     * 
+     * </pre></code>
      *
      * @param type the object to seek for in the active context
      * @param single shall there be just one or multiple instances of the object
@@ -665,7 +736,7 @@ public class Actions {
         map.put("displayName", displayName); // NOI18N
         map.put("iconBase", iconBase); // NOI18N
         map.put("noIconInMenu", noIconInMenu); // NOI18N
-        return GeneralAction.context(map);
+        return GeneralAction.context(map, true);
     }
     static Action context(Map fo) {
         Object context = fo.get("context");
@@ -1155,8 +1226,24 @@ public class Actions {
     /** Bridge for button and boolean action.
     */
     private static class BooleanButtonBridge extends ButtonBridge {
-        public BooleanButtonBridge(AbstractButton button, BooleanStateAction action) {
+        private final BooleanStateAction stateAction;
+        private final PropertyChangeListener bsaL;
+        
+        public BooleanButtonBridge(AbstractButton button, BooleanStateAction bsa) {
+            super(button, bsa);
+            this.stateAction = bsa;
+            if (bsa != null && bsa != action) {
+                bsaL = WeakListeners.propertyChange(this, BooleanStateAction.PROP_BOOLEAN_STATE, bsa);
+                bsa.addPropertyChangeListener(bsaL);
+            } else {
+                bsaL = null;
+            }
+        }
+
+        public BooleanButtonBridge(AbstractButton button, Action action) {
             super(button, action);
+            this.stateAction = null;
+            this.bsaL = null;
         }
 
         /** @param changedProperty the name of property that has changed
@@ -1166,9 +1253,19 @@ public class Actions {
         public void updateState(String changedProperty) {
             super.updateState(changedProperty);
 
-            if ((changedProperty == null) || changedProperty.equals(BooleanStateAction.PROP_BOOLEAN_STATE)) {
-                button.setSelected(((BooleanStateAction) action).getBooleanState());
+            if ((changedProperty == null) || 
+                    changedProperty.equals(BooleanStateAction.PROP_BOOLEAN_STATE) ||
+                    (bsaL == null && changedProperty.equals(Action.SELECTED_KEY))) {
+                button.setSelected(getBooleanState());
+            }
+        }
+        
+        protected boolean getBooleanState() {
+            if (action instanceof AlwaysEnabledAction.CheckBox) {
+                return ((AlwaysEnabledAction.CheckBox)action).isPreferencesSelected();
             }
+            return stateAction != null ? stateAction.getBooleanState() :
+                    Boolean.TRUE.equals(action.getValue(Action.SELECTED_KEY));
         }
     }
 
@@ -1329,8 +1426,17 @@ public class Actions {
         private boolean hasOwnIcon = false;
 
         /** Popup menu */
-        public CheckMenuBridge(JCheckBoxMenuItem item, BooleanStateAction action, boolean popup) {
+        public CheckMenuBridge(JCheckBoxMenuItem item, BooleanStateAction bsa, boolean popup) {
+            super(item, bsa);
+            init(item, popup);
+        }
+        
+        public CheckMenuBridge(JCheckBoxMenuItem item, Action action, boolean popup) {
             super(item, action);
+            init(item, popup);
+        }
+        
+        private void init(JCheckBoxMenuItem item, boolean popup) {
             this.popup = popup;
 
             if (popup) {
@@ -1585,10 +1691,25 @@ public class Actions {
         *  and connects it to the given BooleanStateAction.
         * @param aAction the action to which this menu item should be connected
         * @param useMnemonic if true, the menu try to find mnemonic in action label
+        * @deprecated use {@link #CheckboxMenuItem(javax.swing.Action, boolean)}. 
+        * Have your action to implement properly {@link Action#getValue} for {@link Action#SELECTED_KEY}
         */
         public CheckboxMenuItem(BooleanStateAction aAction, boolean useMnemonic) {
             Actions.connect(this, aAction, !useMnemonic);
         }
+        
+        
+        /** Constructs a new ActCheckboxMenuItem with the specified label
+        *  and connects it to the given Action and its {@link Action#SELECTED_KEY}
+        * value.
+        * 
+        * @param aAction the action to which this menu item should be connected
+        * @param useMnemonic if true, the menu try to find mnemonic in action label
+        * @since 7.71
+        */
+        public CheckboxMenuItem(Action aAction, boolean useMnemonic) {
+            Actions.connect(this, aAction, !useMnemonic);
+        }
     }
 
     /** Component shown in toolbar, representing an action.
diff --git a/openide.awt/src/org/openide/awt/AlwaysEnabledAction.java b/openide.awt/src/org/openide/awt/AlwaysEnabledAction.java
index 6782dc5..d57d65b 100644
--- a/openide.awt/src/org/openide/awt/AlwaysEnabledAction.java
+++ b/openide.awt/src/org/openide/awt/AlwaysEnabledAction.java
@@ -377,7 +377,7 @@ implements PropertyChangeListener, ContextAwareAction {
             return new CheckBox(map, this, actionContext, equals);
         }
 
-        private boolean isPreferencesSelected() {
+        boolean isPreferencesSelected() {
             String key = (String) getValue(PREFERENCES_KEY);
             Preferences prefs = prefs();
             boolean value;
diff --git a/openide.awt/src/org/openide/awt/ContextAction.java b/openide.awt/src/org/openide/awt/ContextAction.java
index fdcbeb9..bea899b 100644
--- a/openide.awt/src/org/openide/awt/ContextAction.java
+++ b/openide.awt/src/org/openide/awt/ContextAction.java
@@ -21,36 +21,53 @@ package org.openide.awt;
 
 import java.awt.EventQueue;
 import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
 import java.beans.PropertyChangeListener;
 import java.beans.PropertyChangeSupport;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 import javax.swing.Action;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
 import org.openide.util.ContextAwareAction;
 import org.openide.util.Lookup;
+import org.openide.util.Mutex;
+import org.openide.util.WeakListeners;
 
 /** A delegate action that is usually associated with a specific lookup and
  * listens on certain classes to appear and disappear there.
  */
-final class ContextAction<T> extends Object 
-implements Action, ContextAwareAction {
+class ContextAction<T> extends Object 
+implements Action, ContextAwareAction, ChangeListener, Runnable {
     //, Presenter.Menu, Presenter.Popup, Presenter.Toolbar, PropertyChangeListener {
+    static final Logger LOG = Logger.getLogger(ContextAction.class.getName());
+    
     /** type to check */
-    private final Class<T> type;
+    final Class<T> type;
     /** selection mode */
     final ContextSelection selectMode;
     /** performer to call */
-    private final ContextAction.Performer<? super T> performer;
+    final ContextAction.Performer<? super T> performer;
 
     /** global lookup to work with */
-    private final ContextManager global;
+    final ContextManager global;
 
     /** support for listeners */
     private PropertyChangeSupport support;
 
     /** was this action enabled or not*/
     private boolean previousEnabled;
+    
+    protected final StatefulMonitor enableMonitor;
 
     /** Constructs new action that is bound to given context and
      * listens for changes of <code>ActionMap</code> in order to delegate
@@ -61,7 +78,7 @@ implements Action, ContextAwareAction {
         ContextSelection selectMode,
         Lookup actionContext, 
         Class<T> type,
-        boolean surviveFocusChange
+        boolean surviveFocusChange, StatefulMonitor enableMonitor
     ) {
         if (performer == null) {
             throw new NullPointerException("Has to provide a key!"); // NOI18N
@@ -70,8 +87,13 @@ implements Action, ContextAwareAction {
         this.selectMode = selectMode;
         this.performer = performer;
         this.global = ContextManager.findManager(actionContext, surviveFocusChange);
+        this.enableMonitor = enableMonitor;
+        if (enableMonitor != null) {
+            LOG.log(Level.FINE, "Setting enable monitor {0}: {1}", new Object[] {
+                    this, enableMonitor} );
+        }
     }
-
+    
     /** Overrides superclass method, adds delegate description. */
     @Override
     public String toString() {
@@ -86,11 +108,49 @@ implements Action, ContextAwareAction {
 
     public boolean isEnabled() {
         assert EventQueue.isDispatchThread();
-        boolean r = global.isEnabled(type, selectMode, performer);
+        boolean r;
+        if (enableMonitor != null) {
+            r = fetchEnabledValue();
+        } else {
+            r = global.isEnabled(type, selectMode, performer);
+        }
         previousEnabled = r;
         return r;
     }
+    
+    private boolean fetchEnabledValue() {
+        return global.runEnabled(type, selectMode, (all, everything) -> {
+            Supplier<Action> af = () -> (Action)performer.delegate0(everything, all, true);
+            if (enableMonitor.getType() == Action.class) {
+                // special case for monitoring the action itself
+                Action dele = (Action)performer.delegate(everything, all);
+                // delegate to the action
+                return enableMonitor.enabled(Collections.singletonList(dele), () -> dele);
+            } else if (enableMonitor.getType() != type) {
+                return global.runEnabled(enableMonitor.getType(), selectMode, 
+                    (all2, everything2) -> {
+                        // run enable monitor for the other type and the original action
+                        return enableMonitor.enabled(all2, af);
+                    }
+                );
+            } else {
+                return enableMonitor.enabled(all, af);
+            }
+        });
+    }
+
+    @Override
+    public void stateChanged(ChangeEvent e) {
+        // remap PropertyMonitor change events into EDT
+        Mutex.EVENT.readAccess(this);
+    }
+
+    @Override
+    public void run() {
+        updateStateProperties();
+    }
 
+    @Override
     public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
         boolean first = false;
         if (support== null) {
@@ -99,19 +159,37 @@ implements Action, ContextAwareAction {
         }
         support.addPropertyChangeListener(listener);
         if (first) {
-            global.registerListener(type, this);
+            startListeners();
         }
     }
 
+    @Override
     public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
         if( null != support ) {
             support.removePropertyChangeListener(listener);
             if (!support.hasListeners(null)) {
-                global.unregisterListener(type, this);
+                stopListeners();
                 support = null;
             }
         }
     }
+    
+    protected void startListeners() {
+        performer.startListeners();
+        global.registerListener(type, this);
+        if (enableMonitor != null) {
+            fetchEnabledValue();
+            enableMonitor.addChangeListener(this);
+        }
+    }
+    
+    protected void stopListeners() {
+        global.unregisterListener(type, this);
+        performer.stopListeners();
+        if (enableMonitor != null) {
+            enableMonitor.removeChangeListener(this);
+        }
+    }
 
     public void putValue(String key, Object o) {
     }
@@ -121,28 +199,79 @@ implements Action, ContextAwareAction {
             // special API to support re-enablement
             assert EventQueue.isDispatchThread();
             updateState();
+        } else if (ACTION_COMMAND_KEY.equals(key)) {
+            Object o = performer.delegate.get(ACTION_COMMAND_KEY);
+            if (o == null) {
+                o = performer.delegate.get("key"); // NOI18N
+            }
+            if (o != null) {
+                return o.toString();
+            }
         }
         return null;
     }
 
     public void setEnabled(boolean b) {
     }
+    
+    void clearState() {
+        performer.clear();
+        if (enableMonitor != null) {
+            enableMonitor.clear();
+        }
+    }
 
+    /**
+     * Called from context manager, when the objects watched for in
+     * Lookup change.
+     */
     void updateState() {
+        clearState();
+        if (!isListening()) {
+            return;
+        }
+        updateStateProperties();
+    }
+    
+    void updateStateProperties() {
+        boolean prev = previousEnabled;
+        boolean now = isEnabled();
+        if (prev != now) {
+            updateEnabledState(now);
+        }
+    }
+    
+    boolean wasEnabled() {
+        return previousEnabled;
+    }
+    
+    protected boolean isListening() {
+        synchronized (this) {
+            return support != null;
+        }
+    }
+    
+    protected void firePropertyChange(String property, Boolean old, Boolean current) {
         PropertyChangeSupport s;
         synchronized (this) {
             s = support;
+            if (s == null) {
+                return;
+            }
         }
-        boolean prev = previousEnabled;
-        if (s != null && prev != isEnabled()) {
-            s.firePropertyChange("enabled", Boolean.valueOf(prev), Boolean.valueOf(!prev)); // NOI18N
-        }
+        s.firePropertyChange(property, old, current);
+    }
+    
+    protected void updateEnabledState(boolean enabled) {
+        this.previousEnabled = enabled;
+        firePropertyChange("enabled", !enabled, enabled); // NOI18N
     }
 
     /** Clones itself with given context.
      */
     public Action createContextAwareInstance(Lookup actionContext) {
-        return new ContextAction<T>(performer, selectMode, actionContext, type, global.isSurvive());
+        return  new ContextAction<T>(performer, selectMode, actionContext, type, global.isSurvive(),
+            enableMonitor == null ? null : enableMonitor.createContextMonitor(actionContext));
     }
 
     @Override
@@ -168,10 +297,17 @@ implements Action, ContextAwareAction {
         }
         return false;
     }
-
-    static class Performer<Data> {
+    
+    private static final Reference<Object> NONE = new WeakReference<>(null);
+    
+    static class Performer<Data> implements ChangeListener {
         final Map delegate;
-
+        Reference<ContextAction>    owner;
+        Reference<Object>   instDelegate = null;
+        StatefulMonitor enabler = null;
+        ChangeListener weakEnableListener;
+        PropertyChangeListener weakActionListener;
+        
         public Performer(Map delegate) {
             this.delegate = delegate;
         }
@@ -181,36 +317,90 @@ implements Action, ContextAwareAction {
             ContextActionEnabler<Data> e
         ) {
             Map<Object, Object> map = new HashMap<Object, Object>();
-            map.put("delegate", p);
-            map.put("enabler", e);
+            map.put("delegate", p); // NOI18N
+            map.put("enabler", e);  // NOI18N
             this.delegate = map;
         }
-
-        @SuppressWarnings("unchecked")
-        public void actionPerformed(
-            ActionEvent ev, List<? extends Data> data, Lookup.Provider everything
-        ) {
-            Object obj = delegate.get("delegate"); // NOI18N
-            if (obj instanceof ContextActionPerformer) {
-                ContextActionPerformer<Data> perf = (ContextActionPerformer<Data>)obj;
-                perf.actionPerformed(ev, data);
-                return;
+        
+        void clear() {
+            stopListeners();
+            Reference r = instDelegate;
+            instDelegate = null;
+            if (r != null) {
+                Object o = r.get();
+                if (o instanceof Performer) {
+                    ((Performer)o).clear();
+                }
             }
-            if (obj instanceof Performer) {
-                Performer<Data> perf = (Performer<Data>)obj;
-                perf.actionPerformed(ev, data, everything);
-                return;
+        }
+        
+        void attach(ContextAction a) {
+            this.owner = new WeakReference<>(a);
+        }
+        
+        /**
+         * Creates a delegate. 
+         * @param everything
+         * @param data
+         * @return 
+         */
+        Object delegate(Lookup.Provider everything, List<?> data) {
+            return delegate0(everything, data, true);
+        }
+        
+        private Object delegate0(Lookup.Provider everything, List<?> data, boolean getAction) {
+            Object d = instDelegate != null ? instDelegate.get() : null;
+            if (d != null) {
+                if (getAction && (d instanceof Performer)) {
+                    return ((Performer)d).delegate0(everything, data, getAction);
+                }
+                return d;
             }
-            if (obj instanceof ContextAwareAction) {
-                Action a = ((ContextAwareAction)obj).createContextAwareInstance(everything.getLookup());
-                a.actionPerformed(ev);
-                return;
+            d = createDelegate(everything, data);
+            if (d != null) {
+                if (getAction && (d instanceof Performer)) {
+                    final Object fd = d;
+                    instDelegate = new WeakReference<Object>(d) { private Object hardRef = fd; };
+                    return ((Performer)d).delegate0(everything, data, getAction);
+                }
+                if (d instanceof ContextAwareAction) {
+                    d = ((ContextAwareAction)d).createContextAwareInstance(everything.getLookup());
+                }
+                instDelegate = new WeakReference<>(d);
+            } else {
+                instDelegate = NONE;
             }
-                
-            GeneralAction.LOG.warning("No 'delegate' for " + delegate); // NOI18N
+            return d;
         }
+        
+        void stopListeners() {
+            if (enabler != null) {
+                enabler.removeChangeListener(weakEnableListener);
+                weakEnableListener = null;
+            }
+        }
+
+        void startListeners() {
+            if (enabler != null) {
+                weakEnableListener = WeakListeners.change(this, enabler);
+                enabler.addChangeListener(weakEnableListener);
+            }
+        }
+        
+        /**
+         * Called when the manager decides that the action should not be enabled at all.
+         * The Performer should detach from the delegate and enabler.
+         */
+        void detach() {
+            stopListeners();
+            Object inst = instDelegate != null ? instDelegate.get() : null;
+            if (inst instanceof Action) {
+                ((Action)inst).removePropertyChangeListener(weakActionListener);
+            }
+        }
+
         @SuppressWarnings("unchecked")
-        public boolean enabled(List<? extends Object> data) {
+        public boolean enabled(List<? extends Object> data, Lookup.Provider everything) {
             Object o = delegate.get("enabler"); // NOI18N
             if (o == null) {
                 return true;
@@ -224,7 +414,41 @@ implements Action, ContextAwareAction {
             GeneralAction.LOG.warning("Wrong enabler for " + delegate + ":" + o);
             return false;
         }
+        
+        protected Object createDelegate(Lookup.Provider everything, List<?> data) {
+            Object obj = delegate.get("delegate"); // NOI18N
+            if (obj instanceof ContextActionPerformer) {
+                return obj;
+            }
+            if (obj instanceof Performer) {
+                return obj;
+            }
+            if (!(obj instanceof ActionListener)) {
+                GeneralAction.LOG.warning("Wrong delegate for " + delegate + ":" + obj);
+            }
+            return obj;
+        }
 
+        @SuppressWarnings("unchecked")
+        public void actionPerformed(
+            ActionEvent ev, List<? extends Data> data, Lookup.Provider everything
+        ) {
+            Object obj = delegate0(everything, data, false);
+            if (obj instanceof ContextActionPerformer) {
+                ContextActionPerformer<Data> perf = (ContextActionPerformer<Data>)obj;
+                perf.actionPerformed(ev, data);
+                return;
+            }
+            if (obj instanceof Performer) {
+                Performer<Data> perf = (Performer<Data>)obj;
+                perf.actionPerformed(ev, data, everything);
+                return;
+            }
+            if (obj instanceof ActionListener) {
+                ((ActionListener)obj).actionPerformed(ev);
+            }
+        }
+        
         @Override
         public int hashCode() {
             return delegate.hashCode() + 117;
@@ -241,7 +465,47 @@ implements Action, ContextAwareAction {
             }
             return false;
         }
+
+        @Override
+        public void stateChanged(ChangeEvent e) {
+            ContextAction a = owner.get();
+            if (a != null) {
+                a.updateState();
+            }
+        }
+        
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            Object o = delegate.get(ACTION_COMMAND_KEY);
+            if (o == null) {
+                o = delegate.get("key"); // NOI18N
+            }
+            Object d = instDelegate == null ? null : instDelegate.get();
+            sb.append("Performer{id = ").append(Objects.toString(o))
+                    .append(", del = ").append(Objects.toString(d))
+                    .append("}");
+            return sb.toString();
+        }
+    }
+
+    /**
+     * Interface between Performer and value monitors.
+     * @param <T> 
+     */
+    static interface StatefulMonitor<T> {
+        public void clear();
+        public void addChangeListener(ChangeListener l);
+        public void removeChangeListener(ChangeListener l);
         
+        /**
+         * Factory interface allows first to evaluate guard conditions, then
+         * query action; delays action creation.
+         */
+        public boolean enabled(List<? extends T> data, Supplier<Action> actionFactory);
+        public Class<?> getType();
+        public StatefulMonitor<T> createContextMonitor(Lookup context);
     }
+    
 }
 
diff --git a/openide.awt/src/org/openide/awt/ContextManager.java b/openide.awt/src/org/openide/awt/ContextManager.java
index 256b973..6ec2a8d 100644
--- a/openide.awt/src/org/openide/awt/ContextManager.java
+++ b/openide.awt/src/org/openide/awt/ContextManager.java
@@ -32,8 +32,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.function.BiFunction;
 import java.util.logging.Level;
 import java.util.logging.Logger;
+import javax.swing.Action;
 import org.openide.util.Lookup;
 import org.openide.util.Lookup.Item;
 import org.openide.util.Lookup.Provider;
@@ -137,13 +139,32 @@ class ContextManager extends Object {
         Lookup.Result<T> result = findResult(type);
         
         boolean e = isEnabledOnData(result, type, selectMode);
-        if (e && enabler != null) {
-            e = enabler.enabled(listFromResult(result));
+        if (enabler != null) {
+            if (e) {
+                List<? extends T> all = listFromResult(result);
+                e = enabler.enabled(all, new LkpAE(all, type));
+            } else if (enabler != null) {
+                enabler.detach();
+            }
         }
         
         return e;
     }
     
+    /** Checks whether a type is enabled.
+     */
+    public <T> boolean runEnabled(Class<T> type, ContextSelection selectMode,  BiFunction<List<? extends T>, Lookup.Provider, Boolean> callback) {
+        Lookup.Result<T> result = findResult(type);
+        
+        boolean e = isEnabledOnData(result, type, selectMode);
+        if (e) {
+            List<? extends T> all = listFromResult(result);
+            e = callback.apply(all, new LkpAE(all, type));
+        }
+        
+        return e;
+    }
+
     private <T> boolean isEnabledOnData(Lookup.Result<T> result, Class<T> type, ContextSelection selectMode) {
         boolean res = isEnabledOnDataImpl(result, type, selectMode);
         LOG.log(Level.FINE, "isEnabledOnData(result, {0}, {1}) = {2}", new Object[]{type, selectMode, res});
@@ -215,24 +236,31 @@ class ContextManager extends Object {
         return res;
     }
     
+    private final class LkpAE<T> implements Lookup.Provider {
+        final List<? extends T> all;
+        final Class<T> type;
+        public LkpAE(List<? extends T> all, Class<T> type) {
+            this.all = all;
+            this.type = type;
+        }
+        
+        private Lookup lookup;
+        public Lookup getLookup() {
+            if (lookup == null) {
+                lookup = new ProxyLookup(
+                    Lookups.fixed(all.toArray()),
+                    Lookups.exclude(ContextManager.this.lookup, type)
+                );
+            }
+            return lookup;
+        }
+    }
+    
     public <T> void actionPerformed(final ActionEvent e, ContextAction.Performer<? super T> perf, final Class<T> type, ContextSelection selectMode) {
         Lookup.Result<T> result = findResult(type);
         final List<? extends T> all = listFromResult(result);
 
-        class LkpAE implements Lookup.Provider {
-            private Lookup lookup;
-            public Lookup getLookup() {
-                if (lookup == null) {
-                    lookup = new ProxyLookup(
-                        Lookups.fixed(all.toArray()),
-                        Lookups.exclude(ContextManager.this.lookup, type)
-                    );
-                }
-                return lookup;
-            }
-        }
-
-        perf.actionPerformed(e, Collections.unmodifiableList(all), new LkpAE());
+        perf.actionPerformed(e, Collections.unmodifiableList(all), new LkpAE(all, type));
     }
 
     private <T> List<? extends T> listFromResult(Lookup.Result<T> result) {
diff --git a/openide.awt/src/org/openide/awt/GeneralAction.java b/openide.awt/src/org/openide/awt/GeneralAction.java
index 815de55..960caac 100644
--- a/openide.awt/src/org/openide/awt/GeneralAction.java
+++ b/openide.awt/src/org/openide/awt/GeneralAction.java
@@ -19,22 +19,28 @@
 
 package org.openide.awt;
 
+import java.awt.Component;
 import java.awt.EventQueue;
 import java.beans.PropertyChangeEvent;
 import java.beans.PropertyChangeListener;
 import java.beans.PropertyChangeSupport;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.swing.Action;
 import javax.swing.ActionMap;
+import javax.swing.JMenuItem;
 import org.openide.awt.ContextAction.Performer;
+import org.openide.awt.ContextAction.StatefulMonitor;
 import org.openide.util.ContextAwareAction;
 import org.openide.util.Lookup;
 import org.openide.util.Parameters;
 import org.openide.util.Utilities;
 import org.openide.util.WeakListeners;
 import org.openide.util.actions.ActionInvoker;
+import org.openide.util.actions.ActionPresenterProvider;
+import org.openide.util.actions.Presenter;
 
 /**
  *
@@ -74,24 +80,76 @@ final class GeneralAction {
         Lookup context, 
         Class<T> dataType
     ) {
-        return new ContextAction<T>(perf, selectionType, context, dataType, false);
+        return new ContextAction<T>(perf, selectionType, context, dataType, false, null);
     }
     
     public static ContextAwareAction context(Map map) {
+        return context(map, false);
+    }
+    
+    static ContextAwareAction context(Map map, boolean instanceReady) {
         Class<?> dataType = readClass(map.get("type")); // NOI18N
-        return new DelegateAction(map, _context(map, dataType, Utilities.actionsGlobalContext()));
+        ContextAwareAction ca = _context(map, dataType, Utilities.actionsGlobalContext(), instanceReady);
+        // autodetect on/off actions
+        if (ca.getValue(Action.SELECTED_KEY) != null) {
+            return new StateDelegateAction(map, ca);
+        } else {
+            return new DelegateAction(map, ca);
+        }
     }
+    
     public static Action bindContext(Map map, Lookup context) {
         Class<?> dataType = readClass(map.get("type")); // NOI18N
-        return new BaseDelAction(map, _context(map, dataType, context));
+        return new BaseDelAction(map, _context(map, dataType, context, false));
     }
-    private static <T> ContextAwareAction _context(Map map, Class<T> dataType, Lookup context) {
+    
+    private static <T> ContextAwareAction _context(Map map, Class<T> dataType, Lookup context, boolean instanceReady) {
         ContextSelection sel = readSelection(map.get("selectionType")); // NOI18N
         Performer<T> perf = new Performer<T>(map);
         boolean survive = Boolean.TRUE.equals(map.get("surviveFocusChange")); // NOI18N
-        return new ContextAction<T>(
-            perf, sel, context, dataType, survive
-        );
+        StatefulMonitor enableMonitor = null;
+        StatefulMonitor checkMonitor = null;
+        Class enableType = tryReadClass(map.get("enableOnType"));
+        if (enableType == null) {
+            enableType = dataType;
+        }
+        Object del = map.get("enableOnActionProperty");
+        Object o = map.get("enableOnProperty"); // NOI18N
+        
+        if (o instanceof String || (o == null && (del instanceof String))) {
+            enableMonitor = new PropertyMonitor(enableType, (String)o, "enableOn", map);
+        }
+        o = map.get("checkedOnProperty"); // NOI18N
+        if (o instanceof String) {
+            Class c = tryReadClass(map.get("checkedOnType")); // NOI18N
+            if (c != null) {
+                checkMonitor = new PropertyMonitor(c, (String)o, "checkedOn", map);
+            }
+        }
+        // special case to hook on existing action instances
+        if (instanceReady) { // NOI18N
+            enableMonitor = new PropertyMonitor(Action.class, "enabled"); // NOI18N
+            Object ao = map.get("delegate");
+            if (ao instanceof Action) {
+                if (((Action)ao).getValue(Action.SELECTED_KEY) != null) {
+                    checkMonitor = new PropertyMonitor(Action.class, Action.SELECTED_KEY);
+                }
+            }
+        }
+        
+        ContextAction a;
+        
+        if (checkMonitor == null) {
+            a = new ContextAction<T>(
+                perf, sel, context, dataType, survive, enableMonitor
+            );
+        } else {
+            a = new StatefulAction<>(perf, sel, context, dataType, survive, enableMonitor, checkMonitor);
+            LOG.log(Level.FINE, "Created stateful delegate for {0}, instance {1}, value monitor {2}", 
+                    new Object[] { map, a, checkMonitor });
+        }
+        
+        return a;
     }
     
     private static ContextSelection readSelection(Object obj) {
@@ -103,7 +161,16 @@ final class GeneralAction {
         }
         throw new IllegalStateException("Cannot parse 'selectionType' value: " + obj); // NOI18N
     }
-    private static Class<?> readClass(Object obj) {
+    
+    static Class<?> readClass(Object obj) {
+        Class<?> r = tryReadClass(obj);
+        if (r == null) {
+            throw new IllegalStateException("Cannot read 'type' value: " + obj); // NOI18N   
+        }
+        return r;
+    }
+    
+    static Class<?> tryReadClass(Object obj) {
         if (obj instanceof Class) {
             return (Class)obj;
         }
@@ -121,7 +188,7 @@ final class GeneralAction {
                 throw new IllegalStateException(ex);
             }
         }
-        throw new IllegalStateException("Cannot read 'type' value: " + obj); // NOI18N
+        return null;
     }
     static final Object extractCommonAttribute(Map fo, Action action, String name) {
         return AlwaysEnabledAction.extractCommonAttribute(fo, name);
@@ -146,6 +213,81 @@ final class GeneralAction {
         }
     } // end of DelegateAction
     
+    /**
+     * Specialization that handles {@link #SELECTED_KEY} action value. Delegats to either the {@link #fallback} or the
+     * action delegated to by the {@link #key}. Uses toggle button as Toolbar presenter and checkbox as menu presenter.
+     */
+    static final class StateDelegateAction extends BaseDelAction implements ContextAwareAction, 
+            Presenter.Toolbar, Presenter.Menu, Presenter.Popup, PropertyChangeListener {
+
+        public StateDelegateAction(Map map, Object key, Lookup actionContext, Action fallback, boolean surviveFocusChange, boolean async) {
+            super(map, key, actionContext, fallback, surviveFocusChange, async);
+            putValue(SELECTED_KEY, fallback.getValue(SELECTED_KEY));
+        }
+
+        public StateDelegateAction(Map map, Action fallback) {
+            super(map, fallback);
+        }
+        
+        @Override
+        public Component getToolbarPresenter() {
+            return ActionPresenterProvider.getDefault().createToolbarPresenter(this);
+        }
+
+        @Override
+        public JMenuItem getMenuPresenter() {
+            return ActionPresenterProvider.getDefault().createMenuPresenter(this);
+        }
+
+        @Override
+        public JMenuItem getPopupPresenter() {
+            return ActionPresenterProvider.getDefault().createPopupPresenter(this);
+        }
+
+        @Override
+        void updateState(ActionMap prev, ActionMap now, boolean fire) {
+            super.updateState(prev, now, fire); 
+            if (key == null) {
+                return;
+            }
+            Action pa = prev.get(key);
+            Action na = now.get(key);
+            if (pa == na) {
+                return;
+            }
+            Boolean os;
+            Boolean ns;
+            if (pa != null) {
+                os = Boolean.TRUE.equals(pa.getValue(SELECTED_KEY));
+            } else {
+                os = Boolean.TRUE.equals(fallback.getValue(SELECTED_KEY));
+            }
+            if (na != null) {
+                ns = Boolean.TRUE.equals(na.getValue(SELECTED_KEY));
+            } else {
+                ns = Boolean.TRUE.equals(fallback.getValue(SELECTED_KEY));
+            }
+            if (os != ns) {
+                putValue(SELECTED_KEY, ns);
+            }
+        }
+
+
+        @Override
+        public void propertyChange(PropertyChangeEvent evt) {
+            super.propertyChange(evt);
+            if (SELECTED_KEY.equals(evt.getPropertyName())) {
+                Object o = evt.getNewValue();
+                putValue(SELECTED_KEY, o != null ? fallback.getValue(SELECTED_KEY) : o);
+            }
+        }
+
+        @Override
+        protected BaseDelAction copyDelegate(Action f, Lookup actionContext) {
+            return new StateDelegateAction(map, key, actionContext, f, global.isSurvive(), async);
+        }
+    }
+    
     static class BaseDelAction extends Object 
     implements Action, PropertyChangeListener {
         /** file object, if we are associated to any */
@@ -164,7 +306,7 @@ final class GeneralAction {
         private PropertyChangeSupport support;
 
         /** listener to check listen on state of action(s) we delegate to */
-        final PropertyChangeListener weakL;
+        PropertyChangeListener weakL;
         Map<String,Object> attrs;
         
         /** Constructs new action that is bound to given context and
@@ -179,6 +321,9 @@ final class GeneralAction {
             this.weakL = WeakListeners.propertyChange(this, fallback);
             this.async = async;
             if (fallback != null) {
+                LOG.log(Level.FINER, "Action {0}: Attaching propchange to {1}", new Object[] {
+                    this, fallback
+                });
                 fallback.addPropertyChangeListener(weakL);
             }
         }
@@ -197,7 +342,7 @@ final class GeneralAction {
         /** Overrides superclass method, adds delegate description. */
         @Override
         public String toString() {
-            return super.toString() + "[key=" + key + "]"; // NOI18N
+            return super.toString() + "[key=" + key + ", map=" + map + "]"; // NOI18N
         }
 
         /** Invoked when an action occurs.
@@ -218,32 +363,45 @@ final class GeneralAction {
         
         public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
             boolean first = false;
-            if (support== null) {
+            if (support == null) {
                 support = new PropertyChangeSupport(this);
                 first = true;
             }
             support.addPropertyChangeListener(listener);
             if (first) {
+                LOG.log(Level.FINER, "Action {0}: Adding global listener for key {1}", new Object[]{this, key});
                 global.registerListener(key, this);
             }
         }
 
         public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
-            if( support != null ) {
+            if (support != null) {
                 support.removePropertyChangeListener(listener);
                 if (!support.hasListeners(null)) {
                     global.unregisterListener(key, this);
+                    LOG.log(Level.FINER, "Action {0}: Removed global listener for key {1}", new Object[]{this, key});
                     support = null;
                 }
             }
         }
 
-
         public void putValue(String key, Object value) {
             if (attrs == null) {
                 attrs = new HashMap<String,Object>();
             }
+            PropertyChangeSupport s;
+            
+            synchronized (this) {
+                s = support;
+            }
+            Object old = null;
+            if (s != null) {
+                old = getValue(key);
+            }
             attrs.put(key, value);
+            if (s != null) {
+                s.firePropertyChange(key, old, old != null ? value : null);
+            }
         }
 
         public Object getValue(String key) {
@@ -299,6 +457,10 @@ final class GeneralAction {
             return a == null ? fallback : a;
         }
 
+        protected BaseDelAction copyDelegate(Action f, Lookup actionContext) {
+            return new DelegateAction(map, key, actionContext, f, global.isSurvive(), async);
+        }
+        
         /** Clones itself with given context.
          */
         public Action createContextAwareInstance(Lookup actionContext) {
@@ -306,7 +468,7 @@ final class GeneralAction {
             if (f instanceof ContextAwareAction) {
                 f = ((ContextAwareAction)f).createContextAwareInstance(actionContext);
             }
-            DelegateAction other = new DelegateAction(map, key, actionContext, f, global.isSurvive(), async);
+            BaseDelAction other = copyDelegate(f, actionContext);
             if (attrs != null) {
                 other.attrs = new HashMap<String,Object>(attrs);
             }
@@ -315,6 +477,7 @@ final class GeneralAction {
 
         public void propertyChange(PropertyChangeEvent evt) {
             if ("enabled".equals(evt.getPropertyName())) { // NOI18N
+                LOG.log(Level.FINE, "Action {0}: got property change from fallback {1}", new Object[] { this, fallback });
                 PropertyChangeSupport sup;
                 synchronized (this) {
                     sup = support;
diff --git a/openide.awt/src/org/openide/awt/InjectorAny.java b/openide.awt/src/org/openide/awt/InjectorAny.java
index 07f8018..e6f1e6f 100644
--- a/openide.awt/src/org/openide/awt/InjectorAny.java
+++ b/openide.awt/src/org/openide/awt/InjectorAny.java
@@ -19,11 +19,12 @@
 
 package org.openide.awt;
 
-import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.lang.reflect.Constructor;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import static javax.swing.Action.ACTION_COMMAND_KEY;
 import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
 import org.openide.util.Lookup.Provider;
@@ -34,7 +35,7 @@ final class InjectorAny extends ContextAction.Performer<Object> {
     }
 
     @Override
-    public void actionPerformed(ActionEvent ev, List<? extends Object> data, Provider everything) {
+    protected Object createDelegate(Provider everything, List<?> data) {
         String clazz = (String) delegate.get("injectable"); // NOI18N
         ClassLoader l = Lookup.getDefault().lookup(ClassLoader.class);
         if (l == null) {
@@ -46,10 +47,27 @@ final class InjectorAny extends ContextAction.Performer<Object> {
         try {
             Class<?> clazzC = Class.forName(clazz, true, l);
             Constructor c = clazzC.getConstructor(List.class);
-            ActionListener action = (ActionListener) c.newInstance(data);
-            action.actionPerformed(ev);
+            return (ActionListener) c.newInstance(data);
         } catch (Exception ex) {
-                Exceptions.printStackTrace(ex);
-            }
+            Exceptions.printStackTrace(ex);
         }
+        return null;
     }
+
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        Object o = delegate.get("key"); // NOI18N
+        if (o == null) {
+            o = delegate.get(ACTION_COMMAND_KEY);
+        }
+        Object d= instDelegate == null ? null : instDelegate.get();
+        sb.append("PerformerANY{id = ").append(Objects.toString(o))
+                .append(", del = ").append(Objects.toString(d))
+                .append(", injectable = ").append(delegate.get("injectable"))
+                .append(", type = ").append(delegate.get("type"))
+                .append("}");
+        return sb.toString();
+    }
+}
diff --git a/openide.awt/src/org/openide/awt/InjectorExactlyOne.java b/openide.awt/src/org/openide/awt/InjectorExactlyOne.java
index 42cdfc6..7597e6a 100644
--- a/openide.awt/src/org/openide/awt/InjectorExactlyOne.java
+++ b/openide.awt/src/org/openide/awt/InjectorExactlyOne.java
@@ -19,11 +19,12 @@
 
 package org.openide.awt;
 
-import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.lang.reflect.Constructor;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import static javax.swing.Action.ACTION_COMMAND_KEY;
 import org.openide.util.Exceptions;
 import org.openide.util.Lookup;
 import org.openide.util.Lookup.Provider;
@@ -34,9 +35,9 @@ final class InjectorExactlyOne extends ContextAction.Performer<Object> {
     }
 
     @Override
-    public void actionPerformed(ActionEvent ev, List<? extends Object> data, Provider everything) {
+    protected Object createDelegate(Provider everything, List<?> data) {
         if (data.size() != 1) {
-            return;
+            return null;
         }
         String clazz = (String) delegate.get("injectable"); // NOI18N
         String type = (String) delegate.get("type"); // NOI18N
@@ -52,9 +53,26 @@ final class InjectorExactlyOne extends ContextAction.Performer<Object> {
             Class<?> clazzC = Class.forName(clazz, true, l);
             Constructor c = clazzC.getConstructor(typeC);
             ActionListener action = (ActionListener) c.newInstance(data.get(0));
-            action.actionPerformed(ev);
+            return action;
         } catch (Exception ex) {
-                Exceptions.printStackTrace(ex);
+            Exceptions.printStackTrace(ex);
+            return null;
         }
     }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        Object o = delegate.get("key"); // NOI18N
+        if (o == null) {
+            o = delegate.get(ACTION_COMMAND_KEY);
+        }
+        Object d= instDelegate == null ? null : instDelegate.get();
+        sb.append("PerformerONE{id = ").append(Objects.toString(o))
+                .append(", del = ").append(Objects.toString(d))
+                .append(", injectable = ").append(delegate.get("injectable"))
+                .append(", type = ").append(delegate.get("type"))
+                .append("}");
+        return sb.toString();
+    }
 }
diff --git a/openide.awt/src/org/openide/awt/PropertyMonitor.java b/openide.awt/src/org/openide/awt/PropertyMonitor.java
new file mode 100644
index 0000000..7b36d4d
--- /dev/null
+++ b/openide.awt/src/org/openide/awt/PropertyMonitor.java
@@ -0,0 +1,651 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.openide.awt;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EventListener;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import javax.swing.Action;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import org.openide.awt.ContextAction.StatefulMonitor;
+import org.openide.util.Exceptions;
+import org.openide.util.Lookup;
+import org.openide.util.Utilities;
+import org.openide.util.WeakListeners;
+
+/**
+ * Enabler, which will work against a certain property on a target object. It attaches to the object when it
+ * changes, and monitors its property's state using either {@link PropertyChangeListener} or {@link ChangeListener}.
+ * If the monitored property's state changes, it fires a change to alert the owner Action.
+ * 
+ * @param <T> data type
+ */
+class PropertyMonitor<T> implements ContextAction.StatefulMonitor<T>, PropertyChangeListener, ChangeListener {
+    private static final Logger LOG = Logger.getLogger(PropertyMonitor.class.getName());
+    
+    static final String KEY_CHECKED_VALUE = "Value"; // NOI18N
+    static final String KEY_LISTEN_INTERFACE = "ChangeListener"; // NOI18N
+    static final String KEY_INTERFACE_METHOD = "Method"; // NOI18N
+    static final String KEY_CUSTOM_CHECK = "ActionProperty"; // NOI18N
+    static final String KEY_NULL = "Null"; // NO18N
+            
+    /**
+     * Reflection not initialized
+     */
+    private static final int UNINITIALIZED = -1;
+    /**
+     * Listeners not supported
+     */
+    private static final int NONE = 0;
+    /**
+     * Property change listener registered against specific property name
+     */
+    private static final int PROPERTY_NAME = 1;
+    /**
+     * Listener registered using general add method
+     */
+    private static final int PROPERTY_ALL = 2;
+    /**
+     * ChangeListener is used,
+     */
+    private static final int CHANGE = 3;
+
+    /**
+     * Custom listener interace.
+     */
+    private static final int CUSTOM = 4;
+    
+    /**
+     * Type being monitored, {@link Action} treated specially.
+     */
+    private final Class<T> type;
+    
+    /**
+     * The property being monitored
+     */
+    private final String property;
+    
+    /**
+     * The value which makes the action selected.
+     */
+    private final Object checkedValue;
+    
+    private Class valType;
+    
+    /**
+     * Reflective access to the property's value
+     */
+    private Method refGetter;
+    
+    /**
+     * Reflective access to add listener
+     */
+    private Method refAddListener;
+    
+    /**
+     * Reflective access to remove listener
+     */
+    private Method refRemoveListener;
+    
+    /**
+     * Detected listener type
+     */
+    private int listenerType = UNINITIALIZED;
+    
+    /**
+     * The Weak listener attached to the monitored data
+     */
+    private EventListener weakListener;
+    
+    /**
+     * The last data being monitored
+     */
+    private Reference<T> attachedTo;
+
+    /**
+     * Change Listeners added to this monitor.
+     */
+    private List<ChangeListener> listeners = null;
+    
+    /**
+     * Listener interface to listen
+     */
+    private Class listenerInterface;
+    
+    /**
+     * Method name to intercept; null for all methods
+     */
+    private final String methodName;
+    
+    private final StatefulMonitor actionMonitor;
+    
+    private final Function<Object, Object> valueFactory;
+    
+    public PropertyMonitor(Class<T> type, String property) {
+        this(type, property, "", Collections.emptyMap());
+    }
+    
+    public PropertyMonitor(Class<T> type, String property, String keyPrefix, Map data) {
+        this.type = type;
+        this.property = property;
+        
+        Object cv = data.get(keyPrefix + KEY_CHECKED_VALUE);
+        if (cv == null) {
+            Object b= data.get(keyPrefix + KEY_NULL);
+            if (b instanceof Boolean) {
+                cv = ((Boolean)b).booleanValue() ? ActionState.NULL_VALUE : ActionState.NON_NULL_VALUE;
+            }
+        }
+        checkedValue = cv;
+
+        valueFactory = initValueAccess();
+
+        Object o = data.get(keyPrefix + KEY_LISTEN_INTERFACE);
+        String mn = null;
+        if (o instanceof String) {
+            listenerInterface = GeneralAction.readClass(o);
+            o = data.get(keyPrefix + KEY_INTERFACE_METHOD);
+            if (o instanceof String) {
+                mn = (String)o;
+            }
+        }
+        Object customCheck = data.get(keyPrefix + KEY_CUSTOM_CHECK);
+        if (customCheck != null) {
+            actionMonitor = new PropertyMonitor(Action.class, customCheck.toString());
+        } else {
+            if (property == null) {
+                throw new IllegalArgumentException("Delegate or guard property must be specified");
+            }
+            actionMonitor = null;
+        }
+        methodName = mn;
+    }
+    
+    public Class<T> getType() {
+        return type;
+    }
+
+    private T data() {
+        synchronized (this) {
+            return attachedTo != null ? attachedTo.get() : null;
+        }
+    }
+
+    public void clear() {
+        Object o = data();
+        if (o != null) {
+            clearListeners(o);
+        }
+        if (actionMonitor != null) {
+            actionMonitor.clear();
+        }
+        synchronized (this) {
+            attachedTo = null;
+        }
+    }
+
+    public void addChangeListener(ChangeListener l) {
+        boolean start = false;
+        synchronized (this) {
+            if (listeners == null) {
+                listeners = new ArrayList<>();
+                start = true;
+            }
+            listeners.add(l);
+        }
+        if (start) {
+            T d = data();
+            LOG.log(Level.FINER, "{0}: attaching listener to {1}", new Object[] { this, d });
+            if (d != null) {
+                addListeners(d);
+            }
+        }
+    }
+
+    public void removeChangeListener(ChangeListener l) {
+        boolean stop = false;
+        synchronized (this) {
+            if (listeners == null) {
+                return;
+            }
+            listeners.remove(l);
+            stop = listeners.isEmpty();
+            if (stop) {
+                listeners = null;
+            }
+        }
+        if (stop) {
+            T d = data();
+            if (d != null) {
+                clearListeners(d);
+            }
+        }
+    }
+    
+    private void clearListeners(Object data) {
+        if (weakListener == null || refRemoveListener == null) {
+            return;
+        }
+        LOG.log(Level.FINER, "{0}: adding listener to {1}", new Object[] { this, data });
+        try {
+            switch (listenerType) {
+                case PROPERTY_NAME:
+                    refRemoveListener.invoke(data, property, weakListener);
+                    break;
+                case CUSTOM:
+                    ((ProxyListener)Proxy.getInvocationHandler(weakListener)).unregister(data);
+                    break;
+                case PROPERTY_ALL:
+                case CHANGE:
+                    refRemoveListener.invoke(data, weakListener);
+                    break;
+                case NONE:
+                    break;
+                default:
+                    throw new IllegalStateException();
+            }
+        } catch (ReflectiveOperationException | IllegalArgumentException ex) {
+            ex.printStackTrace();
+        }
+        weakListener = null;
+        if (actionMonitor != null) {
+            actionMonitor.removeChangeListener(this);
+        }
+    }
+
+    /**
+     * Initializes listener reflective access.
+     * @param data
+     */
+    private void initListenerReflection() {
+        if (listenerType != UNINITIALIZED) {
+            return;
+        }
+        Method add = null;
+        try {
+            if (listenerInterface != null) {
+                add = type.getMethod("add" + listenerInterface.getSimpleName(), listenerInterface);
+                listenerType = CUSTOM;
+            } else {
+                try {
+                    if (property != null) {
+                        add = type.getMethod("addPropertyChangeListener", String.class, PropertyChangeListener.class);
+                        listenerType = PROPERTY_NAME;
+                    }
+                } catch (NoSuchMethodException ex) {
+                    // expected, ignore
+                }
+                if (add == null) {
+                    try {
+                        add = type.getMethod("addPropertyChangeListener", PropertyChangeListener.class);
+                        listenerType = PROPERTY_ALL;
+                    } catch (NoSuchMethodException ex2) {
+                        add = type.getMethod("addChangeListener", ChangeListener.class);
+                        listenerType = CHANGE;
+                    }
+                }
+            }
+        } catch (NoSuchMethodException | SecurityException ex3) {
+            listenerType = NONE;
+            return;
+        }
+        Method remove = null;
+        try {
+            switch (listenerType) {
+                case PROPERTY_NAME:
+                    remove = type.getMethod("removePropertyChangeListener", String.class, PropertyChangeListener.class);
+                    break;
+                case PROPERTY_ALL:
+                    remove = type.getMethod("removePropertyChangeListener", PropertyChangeListener.class);
+                    break;
+                case CHANGE:
+                    remove = type.getMethod("removeChangeListener", ChangeListener.class);
+                    break;
+                case CUSTOM:
+                    remove = type.getMethod("remove" + listenerInterface.getSimpleName(), listenerInterface);
+                    break;
+            }
+        } catch (NoSuchMethodException | SecurityException ex) {
+            listenerType = -1;
+            return;
+        }
+        refAddListener = add;
+        refRemoveListener = remove;
+    }
+
+    // method accessed by reflection
+    public boolean falseGetter(Object data) {
+        return false;
+    }
+
+    // method accessed by reflection
+    public boolean trueGetter(Object data) {
+        return true;
+    }
+    
+    private void addListeners(Object data) {
+        if (weakListener != null || listenerType == NONE) {
+            return;
+        }
+        initListenerReflection();
+        synchronized (this) {
+            if (listeners == null) {
+                return;
+            }
+        }
+        PropertyChangeListener pcl;
+        ChangeListener chl;
+        LOG.log(Level.FINER, "{0}: adding listener to {1}", new Object[] { this, data });
+        try {
+            switch (listenerType) {
+                case PROPERTY_NAME:
+                    weakListener = pcl = WeakListeners.propertyChange(this, property, data);
+                    refAddListener.invoke(data, property, pcl);
+                    break;
+                case PROPERTY_ALL:
+                    weakListener = pcl = WeakListeners.propertyChange(this, data);
+                    refAddListener.invoke(data, pcl);
+                    break;
+                case CHANGE:
+                    weakListener = chl = WeakListeners.change(this, data);
+                    refAddListener.invoke(data, chl);
+                    break;
+                case NONE:
+                    return;
+                case CUSTOM: {
+                    ProxyListener pl = new ProxyListener(data, methodName, refRemoveListener, this);
+                    Object o = Proxy.newProxyInstance(listenerInterface.getClassLoader(), new Class[] { listenerInterface, EventListener.class }, pl);
+                    pl.proxy = weakListener = (EventListener)o;
+                    refAddListener.invoke(data, weakListener);
+                    break;
+                }
+                    
+                default:
+                    throw new IllegalStateException();
+            }
+        } catch (ReflectiveOperationException | IllegalArgumentException ex) {
+            listenerType = NONE;
+        }
+
+        if (actionMonitor != null) {
+            actionMonitor.addChangeListener(this);
+        }
+    }
+
+    private Function<Object, Object> initValueAccess() {
+        Method getter = null;
+        if (property != null) {
+            String capitalizedName = Character.toUpperCase(property.charAt(0)) + property.substring(1);
+            String isGetter = "is" + capitalizedName; // NOI18N
+            String getGetter = "get" + capitalizedName; // NOI18N
+            try {
+                try {
+                    getter = type.getMethod(isGetter);
+                } catch (NoSuchMethodException ex) {
+                    getter = type.getMethod(getGetter);
+                }
+                Class c = getter.getReturnType();
+                if (!(c != Boolean.TYPE || c != Boolean.class || c != String.class || !c.isEnum()) && 
+                    !(checkedValue == ActionState.NULL_VALUE || checkedValue == ActionState.NON_NULL_VALUE)) {
+                    getter = null;
+                }
+                valType = c;
+                this.refGetter = getter;
+                return (o) -> reflectiveGet(o);
+            } catch (SecurityException | NoSuchMethodException ex) {
+            }
+        }
+        if (type == Action.class) {
+            return (o) -> inspectAction((Action)o);
+        } else {
+            return (o) -> property == null;
+        }
+    }
+
+    private void update() {
+        ChangeListener[] ll;
+        synchronized (this) {
+            if (listeners == null) {
+                return;
+            }
+            ll = listeners.toArray(new ChangeListener[listeners.size()]);
+        }
+        ChangeEvent ev = new ChangeEvent(this);
+        for (ChangeListener l : ll) {
+            l.stateChanged(ev);
+        }
+    }
+
+    private void refreshListeners(T data) {
+        Object prevData = data();
+        if (prevData == data) {
+            return;
+        }
+        if (actionMonitor != null) {
+            actionMonitor.clear();
+        }
+        if (prevData != null) {
+            clearListeners(prevData);
+        }
+        if (data != null) {
+            addListeners(data);
+        }
+        attachedTo = new WeakReference<>(data);
+    }
+    
+    private Object reflectiveGet(Object instance) {
+        try {
+            return refGetter.invoke(instance);
+        } catch (ReflectiveOperationException | IllegalArgumentException ex) {
+            return false;
+        }
+    }
+    
+    public boolean enabled(List<? extends T> data, Supplier<Action> aFactory) {
+        T first = data.isEmpty() ? null : data.get(0);
+        if (data.isEmpty()) {
+            return false;
+        }
+        refreshListeners(first);
+        if (first == null) {
+            return false;
+        }
+        if (type == Action.class) {
+            return inspectAction((Action)first);
+        }
+        
+        Object o = valueFactory.apply(first);
+        if (!interpretAsBoolean(o)) {
+            return false;
+        }
+        if (aFactory != null && actionMonitor != null) {
+            return actionMonitor.enabled(Collections.singletonList(aFactory.get()), null);
+        } else {
+            return true;
+        }
+    }
+    
+    public boolean inspectAction(Action a) {
+        if (a == null) {
+            return false;
+        }
+        if ("enabled".equals(property)) { // NOI18N
+            return a.isEnabled();
+        }
+        return a.getValue(property) == Boolean.TRUE;
+    }
+    
+    private boolean interpretAsBoolean(Object v) {
+        if (v == null) {
+            if (checkedValue == ActionState.NULL_VALUE) {
+                return true;
+            }
+            return false;
+        } 
+        if (valType == null || valType == Boolean.TYPE || valType == Boolean.class) {
+            if (checkedValue == null) {
+                return Boolean.TRUE.equals(v);
+            } else {
+                return checkedValue.equals(v.toString());
+            }
+        }
+        if (checkedValue == null) {
+            return false;
+        }
+        if (checkedValue == ActionState.NON_NULL_VALUE) {
+            return true;
+        }
+        if (!(checkedValue instanceof String)) {
+            return checkedValue.equals(v);
+        }
+        return checkedValue.equals(v.toString());
+    }
+
+    @Override
+    public void propertyChange(PropertyChangeEvent evt) {
+        if (evt.getPropertyName() != null && property != null && !property.equals(evt.getPropertyName())) {
+            return;
+        }
+        update();
+    }
+
+    @Override
+    public void stateChanged(ChangeEvent e) {
+        update();
+    }
+
+    public PropertyMonitor(PropertyMonitor other) {
+        this.type = other.type;
+        this.property = other.property;
+        this.checkedValue = other.checkedValue;
+        this.listenerType = other.listenerType;
+        this.refGetter = other.refGetter;
+        this.valueFactory = other.valueFactory;
+        this.refAddListener = other.refAddListener;
+        this.refRemoveListener = other.refRemoveListener;
+        this.listenerInterface = other.listenerInterface;
+        this.methodName = other.methodName;
+        if (other.actionMonitor == null) {
+            this.actionMonitor = null;
+        } else {
+            this.actionMonitor = other.actionMonitor.createContextMonitor(Lookup.EMPTY);
+        }
+    }
+
+    @Override
+    public ContextAction.StatefulMonitor<T> createContextMonitor(Lookup context) {
+        return new PropertyMonitor<>(this);
+    }
+    
+    private static final Method OBJECT_EQUALS = getObjectMethod("equals", Object.class); // NOI18N
+    private static final Method OBJECT_HASHCODE = getObjectMethod("hashCode"); // NOI18N
+    
+    private static Method getObjectMethod(String name, Class... types) {
+        try {
+            return Object.class.getMethod(name, types);
+        } catch (ReflectiveOperationException | SecurityException ex) {
+            throw new IllegalStateException(ex);
+        }
+    }
+
+    private static class ProxyListener extends WeakReference<ChangeListener> implements EventListener, InvocationHandler, Runnable {
+        private final Reference   theData;
+        private final String  methodName;
+        private final Method  removeMethod;
+        volatile EventListener proxy;
+        
+        public ProxyListener(Object theData, String methodName, Method removeMethod, ChangeListener referent) {
+            super(referent, Utilities.activeReferenceQueue());
+            this.theData = new WeakReference<>(theData);
+            this.methodName = methodName;
+            this.removeMethod = removeMethod;
+        }
+
+        @Override
+        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+            if (method.getDeclaringClass() == Object.class) {
+                // a method from object => call it on your self
+                if (method == OBJECT_EQUALS) {
+                    return equals(args[0]);
+                }  else if (method == OBJECT_HASHCODE) {
+                    return proxy.hashCode();
+                }
+                return method.invoke(this, args);
+            }
+            ChangeListener target = get();
+            Object data = theData.get();
+            if (data == null) {
+                return null;
+            }
+            if (target == null) {
+                return null;
+            }
+            if (methodName == null || method.getName().equals(methodName)) {
+                ChangeEvent ev = new ChangeEvent(data);
+                target.stateChanged(ev);
+            }
+            return null;
+        }
+        
+        private void unregister(Object data) {
+            if (data == null) {
+                return;
+            }
+            if (removeMethod != null) {
+                try {
+                    removeMethod.invoke(data, proxy);
+                } catch (ReflectiveOperationException | SecurityException ex) {
+                    Exceptions.printStackTrace(ex);
+                }
+            }
+            theData.clear();
+        }
+
+        @Override
+        public void run() {
+            unregister(theData.get());
+        }
+    }
+    
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("PropertyMonitor@").append(System.identityHashCode(this)).append("{")
+                .append("class = ").append(type.getName())
+                .append(", property = ").append(property)
+                .append(", valtype = ").append(valType == null ? "null" : valType.getName())
+                .append(", checkval = ").append(checkedValue)
+                .append("}");
+        return sb.toString();
+    }
+} 
diff --git a/openide.awt/src/org/openide/awt/StatefulAction.java b/openide.awt/src/org/openide/awt/StatefulAction.java
new file mode 100644
index 0000000..8c2cd1f
--- /dev/null
+++ b/openide.awt/src/org/openide/awt/StatefulAction.java
@@ -0,0 +1,140 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.openide.awt;
+
+import java.util.Collections;
+import java.util.logging.Level;
+import javax.swing.Action;
+import org.openide.util.Lookup;
+
+/**
+ * Represents stateful context-aware action.
+ * @author sdedic
+ */
+final class StatefulAction<T> extends ContextAction<T> {
+    /**
+     * Monitor for "checked" property
+     */
+    private final StatefulMonitor checkValueMonitor;
+    
+    /**
+     * The last selected value.
+     */
+    private boolean selValue;
+    
+    /**
+     * Tracks first attach
+     */
+    private boolean first = true;
+    
+    public StatefulAction(Performer performer, ContextSelection selectMode, Lookup actionContext, Class type, boolean surviveFocusChange, 
+            StatefulMonitor enableMonitor, StatefulMonitor valueMonitor) {
+        super(performer, selectMode, actionContext, type, surviveFocusChange, enableMonitor);
+        this.checkValueMonitor = valueMonitor;
+    }
+
+    @Override
+    void updateStateProperties() {
+        super.updateStateProperties();
+        if (!wasEnabled()) {
+            LOG.log(Level.FINE, "Action {0} disabled, unchecked", this);
+            putValue(SELECTED_KEY, false);
+            return;
+        }
+        boolean nowState = fetchStateValue();
+        boolean oldState = this.selValue;
+        this.selValue = nowState;
+        LOG.log(Level.FINE, "Action {0}: old check state {1}, new check state {2}", new Object[] { 
+            this, oldState, nowState
+        });
+        firePropertyChange(SELECTED_KEY, oldState, nowState);
+    }
+    
+    private boolean fetchStateValue() {
+        first = false;
+        if (checkValueMonitor.getType() == Action.class) {
+            return global.runEnabled(type, selectMode, (all, everything) -> {
+                return checkValueMonitor.enabled(
+                        Collections.singletonList(performer.delegate(everything, all)),
+                        () -> (Action)performer.delegate(everything, all));
+            });
+        } else {
+            return global.runEnabled(checkValueMonitor.getType(), selectMode, (all, everything) -> {
+                return checkValueMonitor.enabled(all, () -> (Action)performer.delegate(everything, all));
+            });
+        }
+    }
+
+    @Override
+    public Object getValue(String key) {
+        if (SELECTED_KEY.equals(key)) {
+            LOG.log(Level.FINER, "Action {0} state: {1}", new Object[] {
+                this, selValue
+            });
+            return selValue;
+        }
+        return super.getValue(key);
+    }
+
+    @Override
+    public Action createContextAwareInstance(Lookup actionContext) {
+        StatefulMonitor checkMon = checkValueMonitor.createContextMonitor(actionContext);
+        StatefulMonitor enableMon = enableMonitor == null ? null : enableMonitor.createContextMonitor(actionContext);
+        Action a = new StatefulAction<>(performer, 
+                selectMode, 
+                actionContext, 
+                type, 
+                global.isSurvive(),
+                enableMon,
+                checkMon);
+        LOG.log(Level.FINE, "Created context Stateful instance: {0} from {1}, check monitor {2}, enable monitor {3}", new Object[] {
+            a, this, checkMon, enableMon
+        });
+        return a;
+    }
+
+    @Override
+    void clearState() {
+        super.clearState();
+        checkValueMonitor.clear();
+    }
+
+    @Override
+    protected void stopListeners() {
+        Class c = checkValueMonitor.getType();
+        if (c != Action.class) {
+            global.unregisterListener(checkValueMonitor.getType(), this);
+        }
+        checkValueMonitor.removeChangeListener(this);
+        super.stopListeners(); 
+    }
+
+    @Override
+    protected void startListeners() {
+        super.startListeners();
+        Class c = checkValueMonitor.getType();
+        if (c != Action.class) {
+            global.registerListener(checkValueMonitor.getType(), this);
+        }
+        if (first) {
+            selValue = fetchStateValue();
+        }
+        checkValueMonitor.addChangeListener(this);
+    }
+}
diff --git a/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/ActionProcessorTest.java b/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/ActionProcessorTest.java
index 00cf521..0b6456c 100644
--- a/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/ActionProcessorTest.java
+++ b/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/ActionProcessorTest.java
@@ -48,7 +48,6 @@ import org.openide.filesystems.FileUtil;
 import org.openide.util.ContextAwareAction;
 import org.openide.util.lookup.AbstractLookup;
 import org.openide.util.lookup.InstanceContent;
-import static org.junit.Assert.*;
 
 /**
  *
@@ -917,5 +916,5 @@ public class ActionProcessorTest extends NbTestCase {
             fail("B has to be static:\n" + os);
         }
     }
-    
+
 }
diff --git a/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/StatefulActionProcessorTest.java b/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/StatefulActionProcessorTest.java
new file mode 100644
index 0000000..2c59454
--- /dev/null
+++ b/openide.awt/test/unit/src/org/netbeans/modules/openide/awt/StatefulActionProcessorTest.java
@@ -0,0 +1,1380 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.netbeans.modules.openide.awt;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.beans.PropertyChangeListener;
+import java.beans.PropertyChangeSupport;
+import java.io.ByteArrayOutputStream;
+import java.lang.ref.Reference;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.ListSelectionModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.ListSelectionListener;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.fail;
+import org.netbeans.junit.NbTestCase;
+import org.openide.awt.ActionID;
+import org.openide.awt.ActionRegistration;
+import org.openide.awt.ActionState;
+import org.openide.awt.Actions;
+import org.openide.util.ContextAwareAction;
+import org.openide.util.ContextGlobalProvider;
+import org.openide.util.Lookup;
+import org.openide.util.NbBundle;
+import org.openide.util.Utilities;
+import org.openide.util.lookup.AbstractLookup;
+import org.openide.util.lookup.InstanceContent;
+import org.openide.util.lookup.Lookups;
+import org.openide.util.lookup.ProxyLookup;
+import org.openide.util.test.AnnotationProcessorTestUtils;
+import org.openide.util.test.MockLookup;
+
+/**
+ * Checks that stateful action support works as designed.
+ * 
+ * @author sdedic
+ */
+public class StatefulActionProcessorTest extends NbTestCase implements ContextGlobalProvider {
+    MockLookup mockLookup;
+    static Action instance;
+    static ActionListener instance2;
+    static int created;
+    static ActionEvent received;
+    
+    static {
+        System.setProperty("java.awt.headless", "true");
+    }
+
+    public StatefulActionProcessorTest(String n) {
+        super(n);
+        MockLookup.init();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        created = 0;
+        instance = null;
+        instance2 = null;
+        received = null;
+        
+        Field f = Utilities.class.getDeclaredField("global");
+        f.setAccessible(true);
+        f.set(null, null);
+
+        super.tearDown(); 
+    }
+    
+    private InstanceContent lookupContent;
+    private AbstractLookup testLookup;
+    private PL actionLookup = new PL();
+    
+    private static class PL extends ProxyLookup {
+        void setLookupsAccessor(Lookup... lookups) {
+            super.setLookups(lookups);
+        }
+    }
+    
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        created = 0;
+        instance = null;
+        instance2 = null;
+        received = null;
+        
+        reinitActionLookup();
+        ClassLoader l = MockLookup.class.getClassLoader();
+        MockLookup.setLookup(Lookups.fixed(this), Lookups.metaInfServices(l), Lookups.singleton(l));
+    }
+    
+    void reinitActionLookup() {
+        lookupContent = new InstanceContent();
+        testLookup = new AbstractLookup(lookupContent);
+        actionLookup.setLookupsAccessor(testLookup);
+    }
+
+    @Override
+    public Lookup createGlobalContext() {
+        return actionLookup;
+    }
+
+    @Override
+    protected boolean runInEQ() {
+        return true;
+    }
+    
+    
+    public static enum EnValue {
+        ONE, TWO
+    }
+    
+    static interface NonpublicListener {
+        public void callback();
+    }
+    
+    public static interface CustomListener {
+        public void callback();
+    }
+    
+    public static class ClassListener {
+        
+    }
+    
+    public static class ActionModel {
+        boolean boolProp;
+        boolean bool2Prop;
+        Boolean boolObjectProp;
+        EnValue enumProp;
+        String  prop;
+        Object  anyProp;
+        boolean noneBoolProp;
+        int intProp;
+        
+        PropertyChangeSupport supp = new PropertyChangeSupport(this);
+        
+        public boolean getBool2Prop() {
+            return bool2Prop;
+        }
+
+        public boolean isBoolProp() {
+            return boolProp;
+        }
+
+        public Boolean getBoolObjectProp() {
+            return boolObjectProp;
+        }
+
+        public String getProp() {
+            return prop;
+        }
+
+        public String getAnyProp() {
+            return null;
+        }
+        
+        int getIntProp() {
+            return 0;
+        }
+    }
+    
+    public static class DefaultActionModel {
+        boolean boolProp;
+        boolean bool2Prop;
+
+        PropertyChangeSupport supp = new PropertyChangeSupport(this);
+
+        public boolean isEnabled() {
+            return boolProp;
+        }
+        
+        public void setEnabled(boolean e) {
+            this.boolProp = e;
+            supp.firePropertyChange("enabled", null, null);
+            
+        }
+        public boolean getSwingSelectedKey() {
+            return bool2Prop;
+        }
+        
+        public void setSwinSelectedKey(boolean s) {
+            this.bool2Prop = s;
+            supp.firePropertyChange(Action.SELECTED_KEY, null, null);
+        }
+    }
+    
+    public static class ActionModel2 extends ActionModel {
+        public void addPropertyChangeListener(String prop, PropertyChangeListener p) {
+            supp.addPropertyChangeListener(prop, p);
+        }
+
+        public void removePropertyChangeListener(String prop, PropertyChangeListener p) {
+            supp.removePropertyChangeListener(prop, p);
+        }
+    }
+
+    public static class ActionModel3 extends ActionModel {
+        public void addPropertyChangeListener(PropertyChangeListener p) {
+            supp.addPropertyChangeListener(p);
+        }
+
+        public void removePropertyChangeListener(PropertyChangeListener p) {
+            supp.removePropertyChangeListener(prop, p);
+        }
+    }
+    
+    public static class ActionModel4 extends ActionModel {
+        List<ChangeListener> listeners = new ArrayList<>();
+        
+        public void fire() {
+            ChangeEvent e = new ChangeEvent(this);
+            for (ChangeListener l : listeners) {
+                l.stateChanged(e);
+            }
+        }
+        
+        public void addChangeListener(ChangeListener p) {
+            listeners.add(p);
+        }
+
+        public void removeChangeListener(ChangeListener p) {
+            listeners.remove(p);
+        }
+    }
+    
+    public static class ActionModel5 extends ActionModel {
+        List<CustomListener> listeners = new ArrayList<>();
+        
+        public void fire() {
+            for (CustomListener l : listeners) {
+                l.callback();
+            }
+        }
+
+        public void addCustomListener(CustomListener p) {
+            listeners.add(p);
+        }
+
+        public void removeCustomListener(CustomListener p) {
+            listeners.remove(p);
+        }
+    }
+    
+    /**
+     * Checks that without type() the default is used, but the property must be specified
+     */
+    public void testCheckOnTypeNoProperty() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState()) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        assertFalse("Compilation has to fail:\n" + os, r);
+        if (!os.toString().contains("Property must be specified")) {
+            fail("Property must be specified:\n" + os);
+        }
+    }
+
+    /**
+     * Checks that missing getter is reported
+     */
+    public void testCheckOnGetterNotExist() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"rumcajs\")) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        assertFalse("Compilation has to fail:\n" + os, r);
+        if (!os.toString().contains("Property rumcajs not found")) {
+            fail("Property must be specified:\n" + os);
+        }
+    }
+
+    /**
+     * Property getter must be public
+     */
+    public void testCheckOnNonpublicGetter() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"intProp\")) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        assertFalse("Compilation has to fail:\n" + os, r);
+        if (!os.toString().contains("must be public")) {
+            fail("Property must be checked for public access:\n" + os);
+        }
+    }
+
+    /**
+     * Checks that boolean "isXX" getter is found
+     */
+    public void testCheckBooleanGetter1() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"boolProp\")) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        assertTrue("Compilation must be successful", r);
+    }
+
+    /**
+     * Checks that boolean getXXX getter is found
+     */
+    public void testCheckBooleanGetter2() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"bool2Prop\")) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        assertTrue("Compilation must be successful", r);
+    }
+
+    /**
+     * listenOn type must be an interface
+     */
+    public void testInvalidListenerClass() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import javax.swing.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"prop\", listenOn=ClassListener.class)) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        if (!os.toString().contains("is not an interface")) {
+            fail("class as listener type must be reported" + os);
+        }
+    }
+
+    /**
+     * listenOn type must be public
+     */
+    public void testNonpublicListenerClass() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import javax.swing.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"prop\", listenOn=NonpublicListener.class)) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        if (!os.toString().contains("is not public")) {
+            fail("Nonpublic listener type must be reported" + os);
+        }
+    }
+
+    /**
+     * no addXxxxListener is present for the specified type
+     */
+    public void testMissingAddListener() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import javax.swing.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = @ActionState(type = ActionModel2.class, property=\"prop\", listenOn=ChangeListener.class)) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        if (!os.toString().contains("Method addChangeListener not found")) {
+            fail("Missing add listener must be reported" + os);
+        }
+    }
+
+    /**
+     * issues an error if the specified trigger method does not exist
+     */
+    public void testMissingListenerMethod() throws Exception {
+        clearWorkDir();
+        AnnotationProcessorTestUtils.makeSource(getWorkDir(), "test.A", 
+            "import org.openide.awt.ActionRegistration;\n" +
+            "import org.openide.awt.ActionReference;\n" +
+            "import org.openide.awt.ActionState;\n" +
+            "import org.openide.awt.ActionID;\n" +
+            "import org.openide.util.actions.Presenter;\n" +
+            "import java.awt.event.*;\n" +
+            "import javax.swing.event.*;\n" +
+            "import java.util.List;\n" +
+            "import javax.swing.*;\n" +
+            "import org.netbeans.modules.openide.awt.StatefulActionProcessorTest.*;\n" +
+                    
+            "public class A {\n" +
+            "    @ActionID(category=\"Tools\",id=\"test.action\")" +
+            "    @ActionRegistration(displayName=\"AAA\", key=\"K\", checkedOn = "
+                    + "@ActionState(type = ActionModel5.class, property=\"prop\", listenOn=CustomListener.class, listenOnMethod=\"bubu\")) " +
+            "    @ActionReference(path=\"manka\", position=11)" +
+            "    " +
+            "    public static class B implements ActionListener {\n" +
+            "       public B(ActionModel mdl) {} \n" +
+            "      public void actionPerformed(ActionEvent e) {}\n" +
+            "    }\n" +
+            "}\n"
+        );
+        ByteArrayOutputStream os = new ByteArrayOutputStream();
+        boolean r = AnnotationProcessorTestUtils.runJavac(getWorkDir(), null, getWorkDir(), null, os);
+        if (!os.toString().contains("does not contain method bubu")) {
+            fail("Missing listener method must be reported" + os);
+        }
+    }
+    
+    @ActionID(id = "test.DefEnableAction", category="Foo")
+    @ActionRegistration(displayName = "TestAction", 
+            enabledOn = @ActionState()
+    )
+    public static class DefEnableAction implements ActionListener {
+
+        public DefEnableAction(DefaultActionModel model) {
+            instance2 = this;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    /**
+     * Checks that the "enabled" property is used by default if property is not specified
+     * @throws Exception 
+     */
+    public void testDefaulPropertyEnable() throws Exception {
+        Action a = Actions.forID("Foo", "test.DefEnableAction");
+        assertFalse(a.isEnabled());
+        
+        
+        DefaultActionModel mod = new DefaultActionModel();
+        lookupContent.add(mod);
+        
+        assertFalse(a.isEnabled());
+        
+        mod.setEnabled(true);
+        assertTrue(a.isEnabled());
+    }
+    
+    public static class NonNullModel {
+        Collection prop1;
+        
+        PropertyChangeSupport supp = new PropertyChangeSupport(this);
+        
+        public Collection getProp1() {
+            return prop1;
+        }
+        
+        public void setProp1(Collection c) {
+            prop1 = c;
+            supp.firePropertyChange("prop1", null, null);
+        }
+    }
+    
+    @ActionID(id = "test.NonNull", category="Foo")
+    @ActionRegistration(displayName = "TestAction", 
+            enabledOn = @ActionState(property = "prop1", checkedValue = ActionState.NON_NULL_VALUE)
+    )
+    public static class NonNullAction implements ActionListener {
+
+        public NonNullAction(NonNullModel model) {
+            instance2 = this;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    @ActionID(id = "test.Null", category="Foo")
+    @ActionRegistration(displayName = "TestAction", 
+            enabledOn = @ActionState(property = "prop1", checkedValue = ActionState.NULL_VALUE)
+    )
+    public static class NullAction extends NonNullAction {
+
+        public NullAction(NonNullModel model) {
+            super(model);
+        }
+    }
+    
+    /**
+     * Checks that the action enables on null property value
+     */
+    public void testEnableOnNull() throws Exception {
+        Action a = Actions.forID("Foo", "test.Null");
+        assertNotNull(a);
+        assertFalse(a.isEnabled());
+        
+        // now provide model with non-null set up already
+        NonNullModel mdl = new NonNullModel();
+        
+        lookupContent.add(mdl);
+        
+        assertTrue("Must be enabled after model with property arrives", a.isEnabled());
+        
+        mdl.setProp1(new ArrayList<>());
+        assertFalse("Must disable when property becomes null", a.isEnabled());
+    }
+
+    /**
+     * Checks that the action enables on non-null property value
+     */
+    public void testEnableOnNonNull() throws Exception {
+        Action a = Actions.forID("Foo", "test.NonNull");
+        assertNotNull(a);
+        assertFalse(a.isEnabled());
+        
+        // now provide model with non-null set up already
+        NonNullModel mdl = new NonNullModel();
+        mdl.setProp1(new ArrayList<>());
+        
+        lookupContent.add(mdl);
+        
+        assertTrue("Must be enabled after model with property arrives", a.isEnabled());
+        
+        mdl.setProp1(null);
+        assertFalse("Must disable when property becomes null", a.isEnabled());
+    }
+    
+    @NbBundle.Messages({
+        "TestAction=Test action"
+    })
+    @ActionID(id = "test.InstAction", category="Foo")
+    @ActionRegistration(displayName = "#TestAction", 
+            enabledOn = @ActionState(property = "boolProp")
+    )
+    public static class InstAction implements ActionListener {
+        public InstAction(ActionModel3 model) {
+            created++;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    /**
+     * Checks that the stateful action is instantiated only just before it is
+     * invoked. Enablement should be evaluated by the framework without any
+     * user code loaded.
+     */
+    public void testEnableActionInstantiation() {
+        assertEquals("Not pre-created", 0, created);
+        Action a = Actions.forID("Foo", "test.InstAction");
+        assertNotNull(a);
+        assertEquals("Not direcly created from layer", 0, created);
+
+        assertFalse("No data in lookup", a.isEnabled());
+        assertEquals("Not created unless data present", 0, created);
+        
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        
+        // still not enabled
+        assertFalse("Property not sets", a.isEnabled());
+        assertEquals("Not created unless guard is set", 0, created);
+        
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertTrue("Property not set", a.isEnabled());
+        assertEquals("Not created before invocation", 0, created);
+
+        a.actionPerformed(new ActionEvent(this, 0, "cmd"));
+        assertEquals("Not created before invocation", 1, created);
+        assertNotNull(received);
+        assertEquals("cmd", received.getActionCommand());
+    }
+    
+    /**
+     * Checks that an action model is freed, after the actionPerformed is called,
+     * and then the focus shifts so the model is not in Lookup.
+     */
+    public void testActionModelFreed() {
+        Action a = Actions.forID("Foo", "test.InstAction");
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd"));
+        
+        reinitActionLookup();
+        
+        Reference r = new WeakReference(mod);
+        mod = null;
+        assertGC("Action model must be GCed", r);
+    }
+    
+    @ActionID(id = "test.CustomEnableAction", category="Foo")
+    @ActionRegistration(displayName = "TestAction", 
+            enabledOn = @ActionState(property = "boolProp", useActionInstance = true)
+    )
+    public static class CustomEnableAction extends AbstractAction {
+        final ActionModel3 model;
+        
+        public CustomEnableAction(ActionModel3 model) {
+            created++;
+            instance = this;
+            setEnabled(false);
+            this.model = model;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+            instance = this;
+        }
+    }
+    
+    /**
+     * Checks that custom enable action is enabled on time, instantiated
+     * only when the guard becomes true.
+     */
+    public void testCustomEnableAction() {
+        Action a = Actions.forID("Foo", "test.CustomEnableAction");
+        assertNotNull(a);
+        assertFalse("No data in lookup", a.isEnabled());
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        // still not enabled
+        assertFalse("Property not set", a.isEnabled());
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertFalse("Action property not set", a.isEnabled());
+        Action inst = instance;
+        inst.setEnabled(true);
+        assertTrue("Delegate must update enable", a.isEnabled());
+    }
+    
+    /**
+     * Checks that an action model is freed, after the actionPerformed is called,
+     * and then the focus shifts so the model is not in Lookup.
+     */
+    public void testCustomActionModelFreed() {
+        Action a = Actions.forID("Foo", "test.CustomEnableAction");
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd"));
+        
+        reinitActionLookup();
+        
+        Reference r = new WeakReference(mod);
+        instance = null;
+        mod = null;
+        assertGC("Action model must be GCed", r);
+    }
+    
+    /**
+     * Check that action with action check enables only after the actual instance enables.
+     */
+    public void testCustomEnableActionInstantiation() {
+        assertEquals("Not pre-created", 0, created);
+        Action a = Actions.forID("Foo", "test.CustomEnableAction");
+        assertNotNull(a);
+        assertEquals("Not direcly created from layer", 0, created);
+        a.isEnabled();
+        assertEquals("Not created unless data present", 0, created);
+        
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        
+        // still not enabled
+        assertFalse(a.isEnabled());
+        assertEquals("Not created unless guard is set", 0, created);
+        
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        // should instantiate the action just because of the property change on guard,
+        // now the action decides the final state.
+        assertEquals("Must be created to evaluate enabled state", 1, created);
+        
+        Action inst = instance;
+        assertNotNull(inst);
+        
+        inst.setEnabled(true);
+        assertSame("Same instance for repeated enable", inst, instance);
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd"));
+        assertSame("Same instance for invocation and enable eval", inst, instance);
+        assertNotNull(received);
+        assertEquals("cmd", received.getActionCommand());
+    }
+    
+    /**
+     * Checks that when the context object is changed, the old custom
+     * action instance is trashed and a new one is created.
+     */
+    public void testCustomEnableActionChangesWithContext() {
+        Action a = Actions.forID("Foo", "test.CustomEnableAction");
+        ActionModel3 mod = new ActionModel3();
+        mod.boolProp = true;
+
+        lookupContent.add(mod);
+        CustomEnableAction inst = (CustomEnableAction)instance;
+        assertNotNull(inst);
+        assertSame(mod, inst.model);
+        assertFalse(a.isEnabled());
+        
+        inst.setEnabled(true);
+        assertTrue(a.isEnabled());
+        
+        ActionModel3 mod2 = new ActionModel3();
+        instance = null;
+        lookupContent.remove(mod);
+        lookupContent.add(mod2);
+        assertNull(instance);
+
+        mod2.boolProp = true;
+        mod2.supp.firePropertyChange("boolProp", null, null);
+        
+        assertNotNull(instance);
+        Action save2 = instance;
+        
+        assertFalse(a.isEnabled());
+        ((AbstractAction)instance).setEnabled(true);
+        a.actionPerformed(new ActionEvent(this, 0, "x"));
+        
+        assertSame(instance, save2);
+        assertNotSame(instance, inst);
+    }
+    
+    @ActionID(id = "test.ToggleAction", category="Foo")
+    @ActionRegistration(displayName = "TestAction", checkedOn = @ActionState(property = "boolProp"))
+    public static class ToggleAction implements ActionListener {
+        
+        public ToggleAction(ActionModel3 model) {
+            instance2 = this;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    /**
+     * Checks that toggle action carries the selected key
+     */
+    public void testToggleAction() throws Exception {
+        Action a = Actions.forID("Foo", "test.ToggleAction");
+        assertNotNull(a.getValue(Action.SELECTED_KEY));
+        assertTrue(Boolean.TRUE.equals(a.getValue(Actions.ACTION_VALUE_TOGGLE)));
+    }
+
+    /**
+     * Checks that the toggle action changes state according to the model
+     * property. 
+     * @throws Exception 
+     */
+    public void testToggleActionEabledStatePropChange1() throws Exception {
+        Action a = Actions.forID("Foo", "test.ToggleAction");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertFalse("Action must be disabled without data", a.isEnabled());
+        assertNull("Must not eagerly instantiate", instance2);
+        
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+
+        assertNull("Must not be created on data presence", instance2);
+        assertTrue("Must be enabled when data is ready", a.isEnabled());
+        assertFalse("Must not be checked unless guard is true", (Boolean)a.getValue(Action.SELECTED_KEY));
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("bool2Prop", null, null);
+        assertFalse("Unrelated property change should be ignored", (Boolean)a.getValue(Action.SELECTED_KEY));
+        
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertTrue("Must become checked after prop change", (Boolean)a.getValue(Action.SELECTED_KEY));
+        
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        
+        assertNotNull(received);
+        assertEquals("cmd2", received.getActionCommand());
+    }
+    
+    /**
+     * Checks that an action model is freed, after the actionPerformed is called,
+     * and then the focus shifts so the model is not in Lookup.
+     */
+    public void testToggleActionModelFreed() {
+        ActionModel3 mod = new ActionModel3();
+        mod.boolProp = true;
+        lookupContent.add(mod);
+        
+        Action a = Actions.forID("Foo", "test.ToggleAction");
+        
+        assertTrue("Must become checked after prop change", (Boolean)a.getValue(Action.SELECTED_KEY));
+        a.actionPerformed(new ActionEvent(this, 0, "cmd"));
+        
+        reinitActionLookup();
+        
+        Reference r = new WeakReference(mod);
+        mod = null;
+        assertGC("Action model must be GCed", r);
+    }
+    
+
+    @ActionID(id = "test.ToggleAction3", category="Foo")
+    @ActionRegistration(displayName = "TestAction", checkedOn = @ActionState(property = "boolProp"))
+    public static class ToggleAction3 implements ActionListener {
+        public ToggleAction3(ActionModel4 model) {
+            instance2 = this;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    /**
+     * Checks that the action framework reacts uses {@code addChangeListener}
+     * @throws Exception 
+     */
+    public void testToggleActionEabledStateChange() throws Exception {
+        Action a = Actions.forID("Foo", "test.ToggleAction3");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertFalse("Action must be disabled without data", a.isEnabled());
+        assertNull("Must not eagerly instantiate", instance2);
+        
+        ActionModel4 mod = new ActionModel4();
+        lookupContent.add(mod);
+
+        assertNull("Must not be created on data presence", instance2);
+        assertTrue("Must be enabled when data is ready", a.isEnabled());
+        assertFalse("Must not be checked unless guard is true", (Boolean)a.getValue(Action.SELECTED_KEY));
+
+        mod.boolProp = true;
+        mod.fire();
+        assertTrue("Must become checked after prop change", (Boolean)a.getValue(Action.SELECTED_KEY));
+        
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        
+        assertNotNull(received);
+        assertEquals("cmd2", received.getActionCommand());
+    }
+
+    @ActionID(id = "test.ToggleCustomCallback", category="Foo")
+    @ActionRegistration(displayName = "TestAction", checkedOn = @ActionState(property = "boolProp", listenOn = CustomListener.class))
+    public static class ToggleCustomCallback implements ActionListener {
+        public ToggleCustomCallback(ActionModel5 model) {
+            instance2 = this;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    /**
+     * Checks that the action framework reacts uses custom listener
+     * @throws Exception 
+     */
+    public void testToggleActionEabledCustomIface() throws Exception {
+        Action a = Actions.forID("Foo", "test.ToggleCustomCallback");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertFalse("Action must be disabled without data", a.isEnabled());
+        assertNull("Must not eagerly instantiate", instance2);
+        
+        ActionModel5 mod = new ActionModel5();
+        lookupContent.add(mod);
+
+        assertNull("Must not be created on data presence", instance2);
+        assertTrue("Must be enabled when data is ready", a.isEnabled());
+        assertFalse("Must not be checked unless guard is true", (Boolean)a.getValue(Action.SELECTED_KEY));
+
+        mod.boolProp = true;
+        mod.fire();
+        assertTrue("Must become checked after prop change", (Boolean)a.getValue(Action.SELECTED_KEY));
+        
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        
+        assertNotNull(received);
+        assertEquals("cmd2", received.getActionCommand());
+    }
+
+
+    @ActionID(id = "test.ToggleAction2", category="Foo")
+    @ActionRegistration(displayName = "TestAction", checkedOn = @ActionState(property = "boolProp"))
+    public static class ToggleAction2 implements ActionListener {
+        
+        public ToggleAction2(ActionModel2 model) {
+            instance2 = this;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+    
+    /**
+     * Checks that the toggle action changes state according to the model
+     * property. Checks usage of {@code addPropertyChange(prop, listener)}.
+     * @throws Exception 
+     */
+    public void testToggleActionEabledStatePropChange2() throws Exception {
+        Action a = Actions.forID("Foo", "test.ToggleAction2");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertFalse("Action must be disabled without data", a.isEnabled());
+        assertNull("Must not eagerly instantiate", instance2);
+        
+        ActionModel2 mod = new ActionModel2();
+        lookupContent.add(mod);
+
+        assertNull("Must not be created on data presence", instance2);
+        assertTrue("Must be enabled when data is ready", a.isEnabled());
+        assertFalse("Must not be checked unless guard is true", (Boolean)a.getValue(Action.SELECTED_KEY));
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("bool2Prop", null, null);
+        assertFalse("Unrelated property change should be ignored", (Boolean)a.getValue(Action.SELECTED_KEY));
+
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertTrue("Must become checked after prop change", (Boolean)a.getValue(Action.SELECTED_KEY));
+        
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        
+        assertNotNull(received);
+        assertEquals("cmd2", received.getActionCommand());
+    }
+    
+    /**
+     * Checks that toggle action is not instantiated prematurely
+     */
+    public void testToggleActionInstantiate() throws Exception {
+        Action a = Actions.forID("Foo", "test.ToggleAction");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertNull("Must not eagerly instantiate", instance2);
+        
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        assertNull("Must not instantiate just on data presence", instance2);
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertNull("Must not instantiate just when guard goes true", instance2);
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        
+        // instantiated
+        assertNotNull(instance2);
+    }
+    
+    @ActionID(id = "test.CustomToggleAction", category="Foo")
+    @ActionRegistration(displayName = "TestAction", checkedOn = @ActionState(property = "boolProp", useActionInstance = true))
+    public static class CustomToggleAction extends AbstractAction {
+        ActionModel3 aModel;
+        
+        public CustomToggleAction(ActionModel3 model) {
+            created++;
+            instance = this;
+            setEnabled(false);
+            this.aModel = model;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+
+        @Override
+        public boolean isEnabled() {
+            return super.isEnabled(); //To change body of generated methods, choose Tools | Templates.
+        }
+        
+    }
+    
+    /**
+     * Checks that the toggle action will be queried for its checked state
+     */
+    public void testToggleActionCustomState() throws Exception {
+        Action a = Actions.forID("Foo", "test.CustomToggleAction");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertNull("Must not eagerly instantiate", instance);
+        
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        assertNull("Must not instantiate just on data presence", instance);
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        Action save = instance;
+        assertNotNull("Must instantiate for check evaluation", instance);
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        
+        // still same instance
+        assertNotNull(instance);
+        assertSame(save, instance);
+    }
+    
+    /**
+     * Checks that the action instance changes as the context object changes,
+     * similar to the test for enabling action.
+     */
+    public void testCustomActionChangesWithContext() throws Exception {
+        Action a = Actions.forID("Foo", "test.CustomToggleAction");
+        assertFalse("Action must be unchecked", (Boolean)a.getValue(Action.SELECTED_KEY));
+        assertNull("Must not eagerly instantiate", instance);
+        
+        ActionModel3 mod = new ActionModel3();
+        ActionModel3 mod2 = new ActionModel3();
+        
+        lookupContent.add(mod);
+        assertNull("Must not instantiate just on data presence", instance);
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+
+        assertNotNull("Must instantiate for check evaluation", instance);
+        assertTrue(a.isEnabled());
+        
+        CustomToggleAction saveTA = (CustomToggleAction)instance;
+        assertSame(mod, saveTA.aModel);
+        instance = null;
+        lookupContent.remove(mod);
+        lookupContent.add(mod2);
+        // not created, the guard condition is not true yet
+        assertNull(instance);
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        // still not created, fired on old model
+        assertNull(instance);
+        
+        mod2.boolProp = true;
+        mod2.supp.firePropertyChange("boolProp", null, null);
+        // now guard becomes true, action must be created
+        assertNotNull(instance);
+        assertNotSame(saveTA, instance);
+
+        CustomToggleAction nowTA = (CustomToggleAction)instance;
+        assertSame(mod2, nowTA.aModel);
+    }
+    
+    /**
+     * Checks how Action.isEnabled() tracks changes in the context and
+     * property changes of the context objects
+     */
+    public void testContextActionEnableChanges() throws Exception {
+        InstanceContent localContent1 = new InstanceContent();
+        AbstractLookup localLookup1 = new AbstractLookup(localContent1);
+        InstanceContent localContent2 = new InstanceContent();
+        AbstractLookup localLookup2 = new AbstractLookup(localContent2);
+        
+        ActionModel3 mdlGlobal = new ActionModel3();
+        ActionModel3 mdlGlobal2 = new ActionModel3();
+        lookupContent.add(mdlGlobal);
+        
+        Action a = Actions.forID("Foo", "test.InstAction");
+        assertFalse("Must be disabled before guard is set", a.isEnabled());
+        
+        mdlGlobal.boolProp = true;
+        mdlGlobal.supp.firePropertyChange("boolProp", null, null);
+        assertTrue("Must turn enabled after guard change", a.isEnabled());
+        
+        // adopt into local context
+        localContent1.add(mdlGlobal);
+        Action localA = ((ContextAwareAction)a).createContextAwareInstance(localLookup1);
+        assertTrue("Context action enable must initialize", localA.isEnabled());
+        
+        // turn to false
+        mdlGlobal.boolProp = false;
+        mdlGlobal.supp.firePropertyChange("boolProp", null, null);
+        assertFalse("Global action must follow guard", a.isEnabled());
+        assertFalse("Context action must follow guard", localA.isEnabled());
+        
+        mdlGlobal.boolProp = true;
+        mdlGlobal.supp.firePropertyChange("boolProp", null, null);
+        assertTrue(a.isEnabled());
+        assertTrue(localA.isEnabled());
+
+        // remove/replace the model in global Lookup
+        lookupContent.remove(mdlGlobal);
+        assertFalse("Global action must follow its Lookup", a.isEnabled());
+        assertTrue("Context action must listen on its Lookup", localA.isEnabled());
+        
+        lookupContent.add(mdlGlobal2);
+        assertFalse(a.isEnabled());
+        assertTrue(localA.isEnabled());
+        
+        mdlGlobal2.boolProp = true;
+        mdlGlobal2.supp.firePropertyChange("boolProp", null, null);
+        
+        assertTrue("Global action must enbale on new global guard", a.isEnabled());
+        assertTrue(localA.isEnabled());
+        
+        mdlGlobal.boolProp = false;
+        mdlGlobal.supp.firePropertyChange("boolProp", null, null);
+        assertFalse("Context action must follow remembered guard", localA.isEnabled());
+        
+        ActionModel3 mdl3 = new ActionModel3();
+        localContent2.add(mdl3);
+        
+        Action localB = ((ContextAwareAction)a).createContextAwareInstance(localLookup2);
+        assertFalse(localB.isEnabled());
+        
+        mdl3.boolProp = true;
+        mdl3.supp.firePropertyChange("boolProp", null, null);
+        assertTrue(localB.isEnabled());
+        assertFalse(localA.isEnabled());
+    }
+
+    /**
+     * Checks that the state object that goes out of lookup is not strong-held
+     * by action system
+     */
+    public void testStateObjectWillGC() throws Exception {
+        Action a = Actions.forID("Foo", "test.CustomToggleAction");
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        assertNull("Must not instantiate just on data presence", instance);
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertNotNull(instance);
+        assertTrue(a.isEnabled());
+
+        lookupContent.remove(mod);
+        
+        mod = null;
+        instance = null;
+
+        Reference<Object> r = new WeakReference<>(mod);
+        assertGC("Obsolete model object must GC", r);
+    }
+    
+    /**
+     * Checks that an obsolete custom action instance is released when
+     * its context object goes away form Lookup and the action instance
+     * can be GCed.
+     */
+    public void testOldContextActionWillGC() throws Exception {
+        Action a = Actions.forID("Foo", "test.CustomToggleAction");
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        assertNull("Must not instantiate just on data presence", instance);
+
+        mod.boolProp = true;
+        mod.supp.firePropertyChange("boolProp", null, null);
+        assertNotNull(instance);
+        assertTrue(a.isEnabled());
+
+        lookupContent.remove(mod);
+        Reference<Object> r = new WeakReference<>(instance);
+        instance = null;
+        mod = null;
+
+        assertGC("Obsolete model object must GC", r);
+    }
+    
+    @ActionID(id = "test.CustomEnableAction2", category="Foo")
+    @ActionRegistration(displayName = "TestAction", enabledOn = @ActionState(useActionInstance = true))
+    public static class CustomEnableAction2 extends AbstractAction {
+        final ActionModel3 model;
+
+        public CustomEnableAction2(ActionModel3 model) {
+            this.model = model;
+            setEnabled(false);
+            instance = this;
+        }
+
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            instance = this;
+        }
+    }
+
+    /**
+     * Checks that custom action isEnable is called when no property is specified
+     */
+    public void testCustomEnableActionNoPropety() {
+        Action a = Actions.forID("Foo", "test.CustomEnableAction2");
+        assertFalse(a.isEnabled());
+        
+        ActionModel3 mod = new ActionModel3();
+        lookupContent.add(mod);
+        assertNotNull(instance);
+        
+        CustomEnableAction2 save = (CustomEnableAction2)instance;
+        instance.setEnabled(true);
+        assertTrue(a.isEnabled());
+        
+        a.actionPerformed(new ActionEvent(this, 0, "cmd2"));
+        assertSame(save, instance);
+    }
+    
+    @ActionID(id = "test.ListAction", category="Foo")
+    @ActionRegistration(displayName = "TestAction", checkedOn = @ActionState(
+            property = "minSelectionIndex", listenOn = ListSelectionListener.class, listenOnMethod="valueChanged"
+    ))
+    public static class ListAction implements ActionListener {
+        public ListAction(ListSelectionModel model) {
+            instance2 = this;
+        }
+        
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            received = e;
+        }
+    }
+}
diff --git a/openide.util.ui/apichanges.xml b/openide.util.ui/apichanges.xml
index 35f9c11..c81a40a 100644
--- a/openide.util.ui/apichanges.xml
+++ b/openide.util.ui/apichanges.xml
@@ -27,6 +27,22 @@
     <apidef name="actions">Actions API</apidef>
 </apidefs>
 <changes>
+    <change id="DeprecateBooleanStateAction">
+        <api name="util"/>
+        <summary><code>BooleanStateAction</code> deprecated in favour of <code>Actions</code> API and <code>@ActionState</code> annotation.</summary>
+        <version major="9" minor="11"/>
+        <date day="1" month="8" year="2018"/>
+        <author login="sdedic"/>
+        <compatibility deprecation="yes"/>
+        <description>
+            <p>
+                The <a href="@TOP@/org/openide/util/actions/BooleanStateAction.html">BooleanStateAction</a> base class was deprecated, as
+                there's a programatic API in <a href="@org-openide-awt@/org/openide/awt/Actions.html">Actions</a>
+                and a declarative <a href="@org-openide-awt@/org/openide/awt/ActionState.html">@ActionState</a> annotation which fully supersede the deprecated class.
+            </p>
+        </description>
+        <class package="org.openide.util.actions" name="BooleanStateAction"/>
+    </change>
     <change id="GetAuthenticationPassword">
         <api name="util"/>
         <summary>API <code>NetworkSettings.getAuthenticationPassword</code> added</summary>
diff --git a/openide.util.ui/manifest.mf b/openide.util.ui/manifest.mf
index 7858e67..4a6ac34 100644
--- a/openide.util.ui/manifest.mf
+++ b/openide.util.ui/manifest.mf
@@ -1,5 +1,5 @@
 Manifest-Version: 1.0
 OpenIDE-Module: org.openide.util.ui
 OpenIDE-Module-Localizing-Bundle: org/openide/util/Bundle.properties
-OpenIDE-Module-Specification-Version: 9.10
+OpenIDE-Module-Specification-Version: 9.11
 
diff --git a/openide.util.ui/src/org/openide/util/actions/BooleanStateAction.java b/openide.util.ui/src/org/openide/util/actions/BooleanStateAction.java
index d597af8..4abc7f1 100644
--- a/openide.util.ui/src/org/openide/util/actions/BooleanStateAction.java
+++ b/openide.util.ui/src/org/openide/util/actions/BooleanStateAction.java
@@ -27,10 +27,13 @@ package org.openide.util.actions;
 * This action is not the most effective way to implement checkbox in
 * a menu. Consider using more modern alternative:
 * <a href="@org-openide-awt@/org/openide/awt/Actions.html#checkbox(java.lang.String,%20java.lang.String,%20java.lang.String,%20java.lang.String,%20boolean)">
-* Actions.checkbox</a>.
+* Actions.checkbox</a>, or declarative <a href="@org-openide-awt@/org/openide/awt/ActionState.html">ActionState annotation</a>.
 *
 * @author   Ian Formanek, Petr Hamernik
+* @deprecated Use new support for stateful actions in <a href="@org-openide-awt@/org/openide/awt/Actions.html">Actions</a> or <a href="@org-openide-awt@/org/openide/awt/ActionState.html">ActionState annotation</a>
+*
 */
+@Deprecated
 public abstract class BooleanStateAction extends SystemAction implements Presenter.Menu, Presenter.Popup,
     Presenter.Toolbar {
     /** serialVersionUID */


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@netbeans.apache.org
For additional commands, e-mail: commits-help@netbeans.apache.org

For further information about the NetBeans mailing lists, visit:
https://cwiki.apache.org/confluence/display/NETBEANS/Mailing+lists


Mime
View raw message