You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@netbeans.apache.org by GitBox <gi...@apache.org> on 2018/08/09 11:09:39 UTC

[GitHub] sdedic closed pull request #652: Initial implementation of @ActionState support

sdedic closed pull request #652: Initial implementation of @ActionState support
URL: https://github.com/apache/incubator-netbeans/pull/652
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/openide.awt/apichanges.xml b/openide.awt/apichanges.xml
index 3c615af0b5..889caf4ca7 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 eb6bb7192e..c1027adc89 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 4e01fafa53..7d008a9f94 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 3ee6e66c7f..483e3c471e 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 @@
 
 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.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.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 @@
         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 @@ protected boolean handleProcess(
         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 @@ protected boolean handleProcess(
                 }
                 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 @@ protected boolean handleProcess(
                     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 @@ protected boolean handleProcess(
                     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 @@ protected boolean handleProcess(
         }
         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 @@ private void generateContext(Element e, File f, ActionRegistration ar) throws La
             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 @@ private void generateContext(Element e, File f, ActionRegistration ar) throws La
         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 1a585c2cbb..fa21822b7e 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.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 JMenuItem createMenuPresenter (Action action) {
             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 JMenuItem createMenuPresenter (Action action) {
         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 c2d1dbfa4b..b4448d0145 100644
--- a/openide.awt/src/org/openide/awt/ActionDefaultPerfomer.java
+++ b/openide.awt/src/org/openide/awt/ActionDefaultPerfomer.java
@@ -21,6 +21,8 @@
 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 @@ public void actionPerformed(ActionEvent ev, List<? extends Object> data, Provide
             }
         }
     }
+
+
+    @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 372dd0d12d..d897d27c70 100644
--- a/openide.awt/src/org/openide/awt/ActionRegistration.java
+++ b/openide.awt/src/org/openide/awt/ActionRegistration.java
@@ -127,4 +127,28 @@
      */
     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 0000000000..fda05bace2
--- /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 8675c3a695..afacb480cf 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.Utilities;
 import org.openide.util.WeakListeners;
 import org.openide.util.actions.BooleanStateAction;
+import org.openide.util.actions.Presenter;
 import org.openide.util.actions.SystemAction;
 
 
@@ -80,6 +81,15 @@
      */
     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
      */
@@ -179,7 +189,12 @@ public static void connect(JMenuItem item, Action action, boolean popup) {
                 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 static void connect(JMenuItem item, Action action, boolean popup) {
     * @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 static void connect(AbstractButton button, Action action) {
                 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 @@ static ContextAwareAction callback(Map fo) {
      *    }
      *  }
      * </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 static ContextAwareAction context(
         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 @@ protected boolean useTextIcons() {
     /** 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 BooleanButtonBridge(AbstractButton button, BooleanStateAction action) {
         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 @@ protected boolean useTextIcons() {
         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 @@ void setBridge(Actions.Bridge br) {
         *  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 6782dc537e..d57d65bc07 100644
--- a/openide.awt/src/org/openide/awt/AlwaysEnabledAction.java
+++ b/openide.awt/src/org/openide/awt/AlwaysEnabledAction.java
@@ -377,7 +377,7 @@ public Action createContextAwareInstance(Lookup actionContext) {
             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 fdcbeb99a2..bea899b0e2 100644
--- a/openide.awt/src/org/openide/awt/ContextAction.java
+++ b/openide.awt/src/org/openide/awt/ContextAction.java
@@ -21,36 +21,53 @@
 
 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 @@ public ContextAction(
         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 @@ public ContextAction(
         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 @@ public void actionPerformed(final java.awt.event.ActionEvent e) {
 
     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 @@ public synchronized void addPropertyChangeListener(PropertyChangeListener listen
         }
         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 @@ public Object getValue(String key) {
             // 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 @@ public boolean equals(Object obj) {
         }
         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 @@ public Performer(
             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 @@ public boolean enabled(List<? extends Object> data) {
             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 @@ public boolean equals(Object obj) {
             }
             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 256b973737..6ec2a8d407 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.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 @@ public boolean isSurvive() {
         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 @@ public boolean isSurvive() {
         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 815de55f09..960caacdf2 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 @@ public static ContextAwareAction callback(Map map) {
         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 @@ private static ContextSelection readSelection(Object obj) {
         }
         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 @@ private static ContextSelection readSelection(Object obj) {
                 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 @@ public DelegateAction(Map map, Action fallback) {
         }
     } // 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 @@ public DelegateAction(Map map, Action fallback) {
         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 @@ protected BaseDelAction(Map map, Object key, Lookup actionContext, Action fallba
             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 @@ protected BaseDelAction(Map map, Action fallback) {
         /** 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 @@ public boolean isEnabled() {
         
         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 @@ private Action findAction() {
             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 @@ public Action createContextAwareInstance(Lookup actionContext) {
             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 @@ public Action createContextAwareInstance(Lookup actionContext) {
 
         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 07f80187c4..e6f1e6f766 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 @@ public InjectorAny(Map fo) {
     }
 
     @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 @@ public void actionPerformed(ActionEvent ev, List<? extends Object> data, Provide
         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 42cdfc6517..7597e6ab88 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 @@ public InjectorExactlyOne(Map fo) {
     }
 
     @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 @@ public void actionPerformed(ActionEvent ev, List<? extends Object> data, Provide
             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 0000000000..7b36d4d6de
--- /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 0000000000..8c2cd1fa5f
--- /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 00cf5214f6..0b6456c3aa 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.util.ContextAwareAction;
 import org.openide.util.lookup.AbstractLookup;
 import org.openide.util.lookup.InstanceContent;
-import static org.junit.Assert.*;
 
 /**
  *
@@ -917,5 +916,5 @@ public void testErrorOnNonStaticInnerclasses() throws IOException {
             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 0000000000..2c594544ae
--- /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 35f9c11e30..c81a40aec1 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 7858e67794..4a6ac34359 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 d597af883a..4abc7f1eaf 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 @@
 * 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 */


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

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

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