This Bugzilla instance is a read-only archive of historic NetBeans bug reports. To report a bug in NetBeans please follow the project's instructions for reporting issues.

View | Details | Raw Unified | Return to bug 153442
Collapse All | Expand All

(-)cfdd8230bdb4 (+557 lines)
Added Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2008 Sun Microsystems, Inc. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of either the GNU
7
 * General Public License Version 2 only ("GPL") or the Common
8
 * Development and Distribution License("CDDL") (collectively, the
9
 * "License"). You may not use this file except in compliance with the
10
 * License. You can obtain a copy of the License at
11
 * http://www.netbeans.org/cddl-gplv2.html
12
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
 * specific language governing permissions and limitations under the
14
 * License.  When distributing the software, include this License Header
15
 * Notice in each file and include the License file at
16
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
17
 * particular file as subject to the "Classpath" exception as provided
18
 * by Sun in the GPL Version 2 section of the License file that
19
 * accompanied this code. If applicable, add the following below the
20
 * License Header, with the fields enclosed by brackets [] replaced by
21
 * your own identifying information:
22
 * "Portions Copyrighted [year] [name of copyright owner]"
23
 *
24
 * If you wish your version of this file to be governed by only the CDDL
25
 * or only the GPL Version 2, indicate your decision by adding
26
 * "[Contributor] elects to include this software in this distribution
27
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
28
 * single choice of license, a recipient has the option to distribute
29
 * your version of this file under either the CDDL, the GPL Version 2 or
30
 * to extend the choice of license to its licensees as provided above.
31
 * However, if you add GPL Version 2 code and therefore, elected the GPL
32
 * Version 2 license, then the option applies only if the new code is
33
 * made subject to such option by the copyright holder.
34
 *
35
 * Contributor(s):
36
 *
37
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
38
 */
39
package org.openide.util;
40
41
import java.awt.Image;
42
import java.awt.event.ActionEvent;
43
import java.beans.PropertyChangeEvent;
44
import java.beans.PropertyChangeListener;
45
import java.beans.PropertyChangeSupport;
46
import java.util.Arrays;
47
import java.util.Collection;
48
import java.util.HashMap;
49
import java.util.Map;
50
import javax.swing.AbstractAction;
51
import javax.swing.Action;
52
import javax.swing.ImageIcon;
53
54
/**
55
 * An action which operates in the global <i>selection context</i> (a
56
 * Lookup such as the return value of Utilities.actionsGlobalContext()).
57
 * Context actions are sensitive to the presence or absence of a particular
58
 * Java type in a Lookup.  The presence or absence of objects of that type
59
 * can changed based on things like what the user has selected, or what
60
 * logical window contains focus.
61
 * <p/>
62
 * Context actions make it possible to have a global action (i.e. it doesn't
63
 * have to know anything in particular about what it's going to operate on
64
 * to be created) which nonetheless acts on whatever happens to be selected
65
 * at some moment in time.
66
 * <p/>
67
 * This class is a replacement for most uses of NodeAction and CookieAction.
68
 * @see org.openide.util.ContextAwareAction
69
 * @see org.openide.util.Lookup
70
 * @see org.openide.util.Utilities.actionsGlobalContext()
71
 * @author Tim Boudreau
72
 */
73
public abstract class ContextAction<T> extends AbstractAction implements ContextAwareAction, HelpCtx.Provider {
74
    private final Class<T> type;
75
    private final StubListener stubListener = new StubListener();
76
    //A context aware instance which we use internally to trigger
77
    //enabled changes as long as there is at least one property change
78
    //listener attached to us.
79
    //By having the same thing we return from createContextAwareInstance handle
80
    //all internal state, we make it easy to make a survives-focus-change
81
    //subclass just by overriding createStub() to make a stub which retains
82
    //the last usable collection of objects
83
    ActionStub stub;
84
    static boolean unitTest;
85
    
86
    /**
87
     * Create a new ContextAction which will operate on instances of
88
     * type <code>type</code>
89
     * @param type The type this action needs in its context in order to be
90
     * invoked
91
     */
92
    protected ContextAction(Class<T> type) {
93
        this (type, null, null);
94
    }
95
96
    /**
97
     * Create a new ContextAction which will operate on a type <code>type</code>,
98
     * with the specified display name and icon.
99
     *
100
     * @param type The type this action needs in its context in order to be
101
     * invoked
102
     * @param displayName A localized display name
103
     * @param icon An image to use as the action's icon
104
     */
105
    protected ContextAction(Class<T> type, String displayName, Image icon) {
106
        this.type = type;
107
        if (type == null) {
108
            throw new NullPointerException ("Type null");
109
        }
110
        if (displayName != null) {
111
            putValue (Action.NAME, displayName);
112
        }
113
        if (icon != null) {
114
            putValue (Action.SMALL_ICON, new ImageIcon (icon));
115
        }
116
        putValue ("noIconInMenu", true);
117
    }
118
119
    /**
120
     * Whether or not the action is enabled.  By default, determines if there
121
     * are any instances of type <code>type</code> in the selection context
122
     * lookup, and if there are, returns true.  To refine this behavior further,
123
     * override <code>enabled (java.util.Collection)</code>.
124
     * @return
125
     */
126
    @Override
127
    public final boolean isEnabled() {
128
        ActionStub stubAction = getStub();
129
        Collection <? extends T> targets = stubAction.collection();
130
        boolean result = checkQuantity(targets) && stubAction.isEnabled();
131
        return result;
132
    }
133
134
    boolean checkQuantity(Collection<?> targets) {
135
        return !targets.isEmpty();
136
    }
137
138
    /**
139
     * Determine if this action should be enabled.  This method will only be
140
     * called if the size of the collection is > 0.  The default implementation
141
     * returns <code>true</code>.  If you need to do some further
142
     * test on the collection of objects to determine if the action should
143
     * really be enabled or not, override this method do that here.
144
     *
145
     * @param targets A collection of objects of type <code>type</code>
146
     * @return Whether or not the action should be enabled.
147
     */
148
    protected boolean enabled(Collection<? extends T> targets) {
149
        return true;
150
    }
151
152
    /**
153
     * Overridden to throw an UnsupportedOperationException.  Do not call.
154
     * @param newValue
155
     */
156
    @Override
157
    public final void setEnabled(boolean newValue) {
158
        throw new UnsupportedOperationException();
159
    }
160
161
    /**
162
     * Override to actually do whatever this action does.  This method is
163
     * passed the current collection of objects present in the selection
164
     * context.
165
     * @param targets The objects of type <code>type</code>, which
166
     * this action will use to do whatever it does
167
     */
168
    protected abstract void actionPerformed(Collection<? extends T> targets);
169
170
    /**
171
     * Fetches the collection of objects this action will act on and passes
172
     * them to <code>actionPerformed(Collection<? extends T>).
173
     * @param e The action event.  Ignored.
174
     */
175
    public final void actionPerformed(ActionEvent ignored) {
176
        getStub().actionPerformed(null);
177
    }
178
179
    /**
180
     * Create an instance of this action over a particular context.  This is
181
     * used to handle cases such as popup menus, where a popup menu is created
182
     * against whatever the selection is at the time of its creation;  if the
183
     * selection changes <i>while</i> the popup is onscreen, we do not want
184
     * the popup to operate on the new selection; it should operate on the thing
185
     * that the user right-clicked.  So for a popup menu, an instance of this
186
     * action is created over a snapshot-lookup - a snapshot
187
     * of the context at the moment it is created.
188
     * @param actionContext The context this action instance should operate on.
189
     * @return
190
     */
191
    public final Action createContextAwareInstance(Lookup actionContext) {
192
        return createStub (actionContext);
193
    }
194
195
    ActionStub<T> createStub(Lookup actionContext) {
196
        return new ActionStub<T>(actionContext, this);
197
    }
198
199
    private ActionStub<T> createInternalStub () {
200
        //Don't synchronize, just ensure we are only called from sync methods
201
        assert Thread.holdsLock(this);
202
        ActionStub result = (ActionStub)
203
                createContextAwareInstance (Utilities.actionsGlobalContext());
204
        return result;
205
    }
206
207
    private synchronized ActionStub<T> getStub() {
208
        if (stub == null && attached) {
209
            stub = createInternalStub();
210
            stub.addPropertyChangeListener(stubListener);
211
        }
212
        return stub == null ? createInternalStub() : stub;
213
    }
214
    /**
215
     * Get the help context, if any.  Returns <code>HelpCtx.DEFAULT_HELP</code>
216
     * by default.
217
     * @return A help context
218
     */
219
    public HelpCtx getHelpCtx() {
220
        return HelpCtx.DEFAULT_HELP;
221
    }
222
223
    @Override
224
    public String toString() {
225
        return super.toString() + "[type=" + type.getName() + "]";
226
    }
227
228
    @Override
229
    public boolean equals(Object obj) {
230
        if (obj == null) {
231
            return false;
232
        }
233
        if (getClass() != obj.getClass()) {
234
            return false;
235
        }
236
        final ContextAction<T> other = (ContextAction<T>) obj;
237
        if (this.type != other.type && (this.type == null || !this.type.equals(other.type))) {
238
            return false;
239
        }
240
        return true;
241
    }
242
243
    @Override
244
    public int hashCode() {
245
        int hash = 7;
246
        hash = 47 * hash + (this.type != null ? this.type.hashCode() : 0);
247
        return hash;
248
    }
249
250
    @Override
251
    public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
252
        super.addPropertyChangeListener(listener);
253
        int count = getPropertyChangeListeners().length;
254
        if (count == 1) {
255
            addNotify();
256
        }
257
    }
258
259
    @Override
260
    public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
261
        super.removePropertyChangeListener(listener);
262
        int count = getPropertyChangeListeners().length;
263
        if (count == 0) {
264
            removeNotify();
265
        }
266
    }
267
268
    volatile boolean attached;
269
    void addNotify() {
270
        assert Thread.holdsLock(this);
271
        attached = true;
272
        stub = getStub();
273
    }
274
275
    void removeNotify() {
276
        assert Thread.holdsLock(this);
277
        attached = false;
278
        stub.removePropertyChangeListener(stubListener);
279
        stub = null;
280
    }
281
282
    //for unit tests
283
    synchronized Collection<? extends T> stubCollection() {
284
        return stub == null ? null : stub.collection();
285
    }
286
287
    private class StubListener implements PropertyChangeListener {
288
289
        public void propertyChange(PropertyChangeEvent evt) {
290
            if ("enabled".equals(evt.getPropertyName())) { //NOI18N
291
                firePropertyChange (evt.getPropertyName(),
292
                        evt.getOldValue(), evt.getNewValue());
293
            }
294
        }
295
296
    }
297
298
    //Inner stub action class which delegates to the parent action's methods.
299
    //Used both for context aware instances, and for internal state for
300
    //ContextAction instances
301
    private static class ActionStub<T> implements Action, LookupListener {
302
        private final Lookup.Result<T> lkpResult;
303
        private final Map<String, Object> pairs = new HashMap<String, Object>();
304
        private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
305
        private final Lookup context;
306
        protected final ContextAction parent;
307
        protected boolean enabled;
308
309
        ActionStub(Lookup context, ContextAction parent) {
310
            assert context != null;
311
            this.context = context;
312
            this.parent = parent;
313
            lkpResult = context.lookupResult(parent.type);
314
            lkpResult.addLookupListener(this);
315
            if (getClass() == ActionStub.class) {
316
                //avoid superclass call to Retained.collection() before
317
                //it has initialized
318
                enabled = isEnabled();
319
            }
320
        }
321
322
        public Object getValue(String key) {
323
            Object result = pairs.get(key);
324
            if (result == null) {
325
                result = parent.getValue(key);
326
            }
327
            return result;
328
        }
329
330
        Collection<? extends T> collection() {
331
            return lkpResult.allInstances();
332
        }
333
334
        public void putValue(String key, Object value) {
335
            Object old = pairs.put(key, value);
336
            boolean fire = (old == null) != (value == null);
337
            if (fire) {
338
                fire = value != null && !value.equals(old);
339
                if (fire) {
340
                    supp.firePropertyChange (key, old, value);
341
                }
342
            }
343
        }
344
345
        public void setEnabled(boolean b) {
346
            //Will throw exception
347
            parent.setEnabled(b);
348
        }
349
350
        public boolean isEnabled() {
351
            Collection<? extends T> targets = collection();
352
            assert targets != null;
353
            assert parent != null;
354
            return targets.isEmpty() ? false : parent.checkQuantity(targets) &&
355
                    parent.enabled(targets);
356
        }
357
358
        public void addPropertyChangeListener(PropertyChangeListener listener) {
359
            supp.addPropertyChangeListener(listener);
360
        }
361
362
        public void removePropertyChangeListener(PropertyChangeListener listener) {
363
            supp.removePropertyChangeListener(listener);
364
        }
365
366
        public void actionPerformed(ActionEvent e) {
367
            assert isEnabled() : "Not enabled: " + this;
368
            Collection<? extends T> targets = collection();
369
            parent.actionPerformed(targets);
370
        }
371
372
        void enabledChanged(final boolean enabled) {
373
            Mutex.EVENT.readAccess(new Runnable() {
374
                public void run() {
375
                    supp.firePropertyChange("enabled", !enabled, enabled); //NOI18N
376
                    if (unitTest) {
377
                        synchronized (parent) {
378
                            parent.notifyAll();
379
                        }
380
                        synchronized (this) {
381
                            this.notifyAll();
382
                        }
383
                    }
384
                }
385
            });
386
        }
387
388
        public void resultChanged(LookupEvent ev) {
389
            boolean old = enabled;
390
            enabled = isEnabled();
391
            if (old != enabled) {
392
                enabledChanged (enabled);
393
            }
394
        }
395
396
        @Override
397
        public String toString() {
398
            return super.toString() + "[name=" + getValue (NAME) + //NOI18N
399
                    "delegating={"+ parent + "} context=" + //NOI18N
400
                    context +"]"; //NOI18N
401
        }
402
    }
403
404
    /**
405
     * Subclass of ContextAction which does not support multi-selection -
406
     * like ContextAction, it is sensitive to a particular type.  However,
407
     * it only is enabled if there is exactly one object of type <code>type</code>
408
     * in the selection.
409
     * @param <T> The type this action is sensitive to
410
     */
411
    public static abstract class Single<T> extends ContextAction<T> {
412
        protected Single (Class<T> type) {
413
            super (type);
414
        }
415
416
        protected Single(Class<T> type, String displayName, Image icon) {
417
            super (type, displayName, icon);
418
        }
419
420
        /**
421
         * Delegates to actionePerformed(T)</code> with the first and
422
         * only element of the collection.
423
         * @param targets The objects this action may operate on
424
         */
425
        @Override
426
        protected final void actionPerformed(Collection<? extends T> targets) {
427
            actionPerformed (targets.iterator().next());
428
        }
429
430
        /**
431
         * Actually perform the action.
432
         * @param target The only instance of <code>T</code> in the action
433
         * context.
434
         */
435
        protected abstract void actionPerformed (T target);
436
437
        @Override
438
        boolean checkQuantity(Collection<?> targets) {
439
            return targets.size() == 1;
440
        }
441
442
    /**
443
     * Determine if this action should be enabled.  This method will only be
444
     * called if the size of the collection == 1.  The default implementation
445
     * returns <code>true</code>.  If you need to do some further
446
     * test on the collection of objects to determine if the action should
447
     * really be enabled or not, override this method do that here.
448
     *
449
     * @param targets A collection of objects of type <code>type</code>
450
     * @return Whether or not the action should be enabled.
451
     */
452
        @Override
453
        protected boolean enabled(Collection<? extends T> targets) {
454
            //Overridden only in order to have different javadoc
455
            return super.enabled(targets);
456
        }
457
    }
458
459
    /**
460
     * A context action which, once enabled, remains enabled.
461
     * <p/>
462
     * The canonical example of this sort of action in the NetBeans IDE is
463
     * NextErrorAction:  It becomes enabled when the output window gains
464
     * focus.  But it should remain enabled when focus goes back to the
465
     * editor, and still work against whatever context the output window
466
     * gave it to work on.  Such cases are rare but legitimate.
467
     * <p/>
468
     * Use judiciously - such actions are temporary memory
469
     * leaks - the action will retain the last usable collection of
470
     * objects it had to work on as long as there are any property
471
     * change listeners attached to it.
472
     *
473
     * @param <T> The type this object is sensitive to
474
     */
475
    public static abstract class SurviveSelectionChange<T> extends Single<T> {
476
        protected SurviveSelectionChange (Class<T> type) {
477
            super (type);
478
        }
479
480
        protected SurviveSelectionChange(Class<T> type, String displayName, Image icon) {
481
            super (type, displayName, icon);
482
        }
483
484
        @Override
485
        ActionStub<T> createStub(Lookup actionContext) {
486
            return new RetainingStub<T> (actionContext, this);
487
        }
488
489
        @Override
490
        boolean checkQuantity(Collection<?> targets) {
491
            return super.checkQuantity(targets) || stub != null &&
492
                    ((RetainingStub) stub).retained.size() == 1;
493
        }
494
495
496
        private static final class RetainingStub<T> extends ActionStub<T> {
497
            Collection <? extends T> retained;
498
            RetainingStub(Lookup context, SurviveSelectionChange parent) {
499
                super (context, parent);
500
                assert parent != null;
501
                assert context != null;
502
                retained = super.collection();
503
                assert retained != null;
504
                enabled = isEnabled();
505
            }
506
507
            @Override
508
            Collection <? extends T> collection() {
509
                boolean wasEnabled = enabled;
510
                if (wasEnabled) {
511
                    Collection <? extends T> nue = super.collection();
512
                    assert nue != null;
513
                    //If we were enabled, and now we have too many objects,
514
                    //become disabled, don't keep the old single object
515
                    if (!parent.checkQuantity(nue)) {
516
                        retained = nue;
517
                    }
518
                    if (!nue.isEmpty()) {
519
                        retained = nue;
520
                    }
521
                }
522
                return retained;
523
            }
524
        }
525
    }
526
527
    /**
528
     * ContextAction subclass which requires a specific number of objects
529
     * (presumably &gt; 1) in the context and no more or no less.
530
     * <p/>
531
     * The canonical example in the IDE is DiffAction, which can only
532
     * be enabled if exactly two files are selected.
533
     *
534
     * @param <T> The type of object this action is sensitive to
535
     */
536
    public static abstract class ExactCount<T> extends ContextAction<T> {
537
        private final int count;
538
        /**
539
         * Create a new action sensitive to exactly <code>count</code>
540
         * instances of <code>type</code> present in the selection
541
         * context.
542
         * @param type The type this action is sensitive too
543
         * @param count Precisely how many instances of <code>type</code>
544
         * need to be in the selection context for isEnabled() to be true
545
         * or <code>actionPerformed()</code> to be called.
546
         */
547
        protected ExactCount (Class<T> type, int count) {
548
            super (type);
549
            this.count = count;
550
        }
551
552
        @Override
553
        boolean checkQuantity(Collection<?> targets) {
554
            return targets.size() == count;
555
        }
556
    }
557
}
(-)cfdd8230bdb4 (+401 lines)
Added Link Here
1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2008 Sun Microsystems, Inc. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of either the GNU
7
 * General Public License Version 2 only ("GPL") or the Common
8
 * Development and Distribution License("CDDL") (collectively, the
9
 * "License"). You may not use this file except in compliance with the
10
 * License. You can obtain a copy of the License at
11
 * http://www.netbeans.org/cddl-gplv2.html
12
 * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
13
 * specific language governing permissions and limitations under the
14
 * License.  When distributing the software, include this License Header
15
 * Notice in each file and include the License file at
16
 * nbbuild/licenses/CDDL-GPL-2-CP.  Sun designates this
17
 * particular file as subject to the "Classpath" exception as provided
18
 * by Sun in the GPL Version 2 section of the License file that
19
 * accompanied this code. If applicable, add the following below the
20
 * License Header, with the fields enclosed by brackets [] replaced by
21
 * your own identifying information:
22
 * "Portions Copyrighted [year] [name of copyright owner]"
23
 *
24
 * If you wish your version of this file to be governed by only the CDDL
25
 * or only the GPL Version 2, indicate your decision by adding
26
 * "[Contributor] elects to include this software in this distribution
27
 * under the [CDDL or GPL Version 2] license." If you do not indicate a
28
 * single choice of license, a recipient has the option to distribute
29
 * your version of this file under either the CDDL, the GPL Version 2 or
30
 * to extend the choice of license to its licensees as provided above.
31
 * However, if you add GPL Version 2 code and therefore, elected the GPL
32
 * Version 2 license, then the option applies only if the new code is
33
 * made subject to such option by the copyright holder.
34
 *
35
 * Contributor(s):
36
 *
37
 * Portions Copyrighted 2008 Sun Microsystems, Inc.
38
 */
39
40
package org.openide.util;
41
42
import java.awt.event.ActionEvent;
43
import java.beans.PropertyChangeEvent;
44
import java.beans.PropertyChangeListener;
45
import java.util.Arrays;
46
import java.util.Collection;
47
import java.util.Collections;
48
import java.util.HashSet;
49
import java.util.Set;
50
import javax.swing.Action;
51
import org.junit.AfterClass;
52
import org.junit.Before;
53
import org.junit.BeforeClass;
54
import org.netbeans.junit.MockServices;
55
import org.netbeans.junit.NbTestCase;
56
import org.openide.util.lookup.AbstractLookup;
57
import org.openide.util.lookup.InstanceContent;
58
import org.openide.util.lookup.Lookups;
59
import static org.junit.Assert.*;
60
61
/**
62
 *
63
 * @author Tim Boudreau
64
 */
65
public class ContextActionTest extends NbTestCase {
66
67
    public ContextActionTest(String x) { super (x); }
68
69
    @BeforeClass
70
    public static void setUpClass() throws Exception {
71
    }
72
73
    @AfterClass
74
    public static void tearDownClass() throws Exception {
75
    }
76
77
    private InstanceContent content;
78
    private Lookup lkp;
79
    private static final int TIMEOUT = 100;
80
    
81
82
    @Before
83
    @Override
84
    public void setUp() {
85
        //This will cause ContextAction instances to invoke notifyAll()
86
        //when their enablement changes
87
        ContextAction.unitTest = true;
88
        MockServices.setServices(Provider.class);
89
        ContextGlobalProvider x = Lookup.getDefault().lookup (ContextGlobalProvider.class);
90
        assertNotNull (x);
91
        assertTrue (x instanceof Provider);
92
        Provider p = (Provider) x;
93
        content = p.content;
94
        lkp = p.lkp;
95
        //some sanity checks
96
        setContent ("hello");
97
        assertEquals ("hello", lkp.lookupAll(String.class).iterator().next());
98
        assertEquals ("hello", Utilities.actionsGlobalContext().lookupAll(String.class).iterator().next());
99
        clearContent();
100
        assertEquals (null, lkp.lookup(String.class));
101
        assertEquals (null, Utilities.actionsGlobalContext().lookup(String.class));
102
        assertEquals (0, Utilities.actionsGlobalContext().lookupAll(Object.class).size());
103
    }
104
105
    static Provider instance;
106
    public static class Provider implements ContextGlobalProvider {
107
        private final Lookup lkp;
108
        private final InstanceContent content;
109
        public Provider () {
110
            lkp = new AbstractLookup (content = new InstanceContent());
111
            instance = this;
112
        }
113
114
        public Lookup createGlobalContext() {
115
            return lkp;
116
        }
117
118
    }
119
120
    void setContent(Object... stuff) {
121
        content.set(Arrays.asList(stuff), null);
122
    }
123
124
    void clearContent() {
125
        content.set(Collections.EMPTY_SET, null);
126
    }
127
128
    public void testEnablement() {
129
        assertEquals (0, lkp.lookupAll(String.class).size()); //sanity check
130
        A a = new A();
131
        assertFalse (a.isEnabled());
132
        setContent ("testEnablement");
133
        assertEquals (1, lkp.lookupAll(String.class).size()); //sanity check
134
        assertTrue (a.isEnabled());
135
        clearContent();
136
        assertFalse (a.isEnabled());
137
    }
138
139
    public void testEnablementFired() throws Exception {
140
        System.out.println("testEnablementFired");
141
        A a = new A();
142
        assertEquals (0, lkp.lookupAll(String.class).size());
143
        PCL pcl = new PCL();
144
        a.addPropertyChangeListener(pcl);
145
        assertFalse (a.isEnabled());
146
        setContent ("testEnablementFired");
147
        synchronized (a) {
148
            a.wait(TIMEOUT);
149
        }
150
        synchronized (pcl) {
151
            pcl.wait(TIMEOUT);
152
        }
153
        assertTrue (a.isEnabled());
154
        pcl.assertEnabledChangedTo(true);
155
        clearContent();
156
        assertFalse (a.isEnabled());
157
        synchronized (a) {
158
            a.wait (TIMEOUT);
159
        }
160
        synchronized (pcl) {
161
            pcl.wait(TIMEOUT);
162
        }
163
        pcl.assertEnabledChangedTo(false);
164
        setContent("woo");
165
        synchronized (a) {
166
            a.wait (TIMEOUT);
167
        }
168
        synchronized (pcl) {
169
            pcl.wait(TIMEOUT);
170
        }
171
        pcl.assertEnabledChangedTo(true);
172
        setContent ("hello", "goodbye");
173
        synchronized (a) {
174
            a.wait (TIMEOUT);
175
        }
176
        synchronized (pcl) {
177
            pcl.wait(TIMEOUT);
178
        }
179
        pcl.assertEnabledChangedTo(false);
180
        assertFalse (a.isEnabled());
181
        clearContent();
182
        synchronized (a) {
183
            a.wait (TIMEOUT);
184
        }
185
        synchronized (pcl) {
186
            pcl.wait(TIMEOUT);
187
        }
188
        pcl.assertNotFired();
189
        a.removePropertyChangeListener(pcl);
190
        setContent("hmm");
191
        assertTrue (a.isEnabled());
192
        pcl.assertNotFired();
193
    }
194
195
    public void testContextInstancesDoNotInterfereWithEachOtherOrParent() throws Exception {
196
        System.out.println("testContextInstancesDoNotInterfereWithEachOtherOrParent");
197
        A a = new A();
198
        assertNull (Utilities.actionsGlobalContext().lookup(String.class)); //sanity check
199
        assertEquals ("A", a.getValue(Action.NAME));
200
        Action a1 = a.createContextAwareInstance(Lookup.EMPTY);
201
        assertFalse (a.isEnabled());
202
        assertEquals ("A", a1.getValue(Action.NAME));
203
        Action a2 = a.createContextAwareInstance(Lookups.fixed("testGeneralBehavior"));
204
        assertTrue (a2.isEnabled());
205
        assertFalse (a.isEnabled());
206
        setContent ("foo");
207
        assertTrue (a.isEnabled());
208
        assertFalse (a1.isEnabled());
209
        assertTrue (a2.isEnabled());
210
        clearContent();
211
        assertFalse (a.isEnabled());
212
        assertTrue (a2.isEnabled());
213
    }
214
215
    public void testContextInstancesAreIndependent() throws Exception {
216
        System.out.println("testContextInstancesAreIndependent");
217
        A a = new A();
218
        assertNull (Utilities.actionsGlobalContext().lookup(String.class)); //sanity check
219
        InstanceContent ic = new InstanceContent();
220
        Lookup l = new AbstractLookup (ic);
221
        Action a3 = a.createContextAwareInstance(l);
222
        assertFalse (a3.isEnabled());
223
        PCL pcl = new PCL();
224
        a3.addPropertyChangeListener(pcl);
225
        setContent ("fuddle");
226
        a.assertNotPerformed();
227
        assertTrue (a.isEnabled());
228
        assertFalse (a3.isEnabled());
229
        synchronized (a3) {
230
            //should time out if test is going to pass
231
            a3.wait(TIMEOUT);
232
        }
233
        synchronized (pcl) {
234
            pcl.wait(TIMEOUT);
235
        }
236
        pcl.assertNotFired();
237
        ic.set(Collections.singleton("boo"), null);
238
        synchronized (a3) {
239
            a3.wait(TIMEOUT);
240
        }
241
        synchronized (pcl) {
242
            pcl.wait(TIMEOUT);
243
        }
244
        pcl.assertEnabledChangedTo(true);
245
        clearContent();
246
        assertTrue (a3.isEnabled());
247
        assertFalse (a.isEnabled());
248
    }
249
250
    public void testGetSetValue() {
251
        System.out.println("testGetSetValue");
252
        A a = new A();
253
        Action a1 = a.createContextAwareInstance(Lookup.EMPTY);
254
        Action a2 = a.createContextAwareInstance(Lookups.fixed("testGeneralBehavior"));
255
        a.putValue ("foo", "bar");
256
        assertEquals("bar", a.getValue("foo"));
257
        assertEquals("bar", a1.getValue("foo"));
258
        assertEquals("bar", a2.getValue("foo"));
259
260
        a1.putValue ("x", "y");
261
        assertNull (a.getValue("x"));
262
        assertEquals ("y", a1.getValue("x"));
263
        a.putValue("x", "z");
264
        assertEquals ("y", a1.getValue("x"));
265
        assertEquals ("z", a.getValue("x"));
266
    }
267
268
    public void testSurviveFocusChange() throws InterruptedException {
269
        System.out.println("testSurviveFocusChange");
270
        B b = new B();
271
        assertFalse (b.isEnabled());
272
        setContent ("testSurviveFocusChange");
273
        assertTrue (b.isEnabled());
274
        PCL pcl = new PCL();
275
        b.addPropertyChangeListener(pcl);
276
        setContent ("x");
277
        assertTrue (b.isEnabled());
278
        b.actionPerformed((ActionEvent)null);
279
        b.assertPerformed("x");
280
        clearContent();
281
        assertTrue (b.isEnabled());
282
        b.actionPerformed((ActionEvent) null);
283
        b.assertPerformed("x");
284
        setContent ("a", "b");
285
        assertFalse (b.isEnabled());
286
        synchronized (b) {
287
            b.wait(TIMEOUT);
288
        }
289
        synchronized (pcl) {
290
            pcl.wait(TIMEOUT);
291
        }
292
        assertFalse (b.createContextAwareInstance(Lookup.EMPTY).isEnabled());
293
        assertTrue (b.createContextAwareInstance(Lookups.fixed("foo")).isEnabled());
294
        assertFalse (b.createContextAwareInstance(Lookups.fixed("moo", "goo")).isEnabled());
295
    }
296
297
    public void testSingleNotEnabledOnMoreThanOne() throws Exception {
298
        System.out.println("testSingleNotEnabledOnMoreThanOne");
299
        A a = new A();
300
        assertFalse (a.createContextAwareInstance(Lookups.fixed("moo", "goo")).isEnabled());
301
        assertTrue (a.createContextAwareInstance(Lookups.fixed("foo")).isEnabled());
302
    }
303
304
    public void testExactCount() throws Exception {
305
        System.out.println("testExactCount");
306
        C c = new C();
307
        assertFalse (c.isEnabled());
308
        setContent ("1");
309
        assertFalse (c.isEnabled());
310
        setContent ("1", "2", "3", "4", "5");
311
        assertTrue (c.isEnabled());
312
        clearContent();
313
        assertFalse (c.isEnabled());
314
        setContent ("1", "2", "3", "4", "5");
315
        assertTrue (c.isEnabled());
316
        setContent ("1", "2", "3", "4", "5", "6");
317
        assertFalse (c.isEnabled());
318
    }
319
320
    private class PCL implements PropertyChangeListener {
321
        PropertyChangeEvent evt;
322
323
        public void propertyChange(PropertyChangeEvent evt) {
324
            if ("enabled".equals (evt.getPropertyName())) {
325
                this.evt = evt;
326
            }
327
            synchronized (this) {
328
                notifyAll();
329
            }
330
        }
331
332
        void assertEnabledChangedTo (boolean val) {
333
            PropertyChangeEvent old = this.evt;
334
            this.evt = null;
335
            assertNotNull (old);
336
            Boolean b = Boolean.valueOf(val);
337
            assertEquals (b, old.getNewValue());
338
        }
339
340
        void assertNotFired() {
341
            assertNull (evt);
342
        }
343
    }
344
345
    private static class A extends ContextAction.Single<String> {
346
        String perfString;
347
        A() { super (String.class, "A", null); }
348
349
        @Override
350
        protected void actionPerformed(String target) {
351
            perfString = target;
352
        }
353
354
        void assertPerformed (String expected) {
355
            String old = perfString;
356
            perfString = null;
357
            assertNotNull (old);
358
            assertEquals (expected, old);
359
        }
360
361
        void assertNotPerformed () {
362
            assertNull (perfString);
363
        }
364
    }
365
366
    private static class B extends ContextAction.SurviveSelectionChange<String> {
367
        String perfString;
368
        B() { super (String.class, "B", null); }
369
370
        @Override
371
        protected void actionPerformed(String target) {
372
            perfString = target;
373
        }
374
375
        void assertPerformed (String expected) {
376
            String old = perfString;
377
            perfString = null;
378
            assertNotNull (old);
379
            assertEquals (expected, old);
380
        }
381
    }
382
383
    private static class C extends ContextAction.ExactCount<String> {
384
        Set <String> perfStrings;
385
        C() { super (String.class, 5); }
386
387
388
389
        void assertPerformed (Set<String> expected) {
390
            Set<String> old = perfStrings;
391
            perfStrings = null;
392
            assertNotNull (old);
393
            assertEquals (expected, old);
394
        }
395
396
        @Override
397
        protected void actionPerformed(Collection<? extends String> targets) {
398
            perfStrings = new HashSet <String> (targets);
399
        }
400
    }
401
}

Return to bug 153442