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 (+949 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.Collection;
47
import java.util.HashMap;
48
import java.util.HashSet;
49
import java.util.LinkedList;
50
import java.util.List;
51
import java.util.Map;
52
import java.util.Set;
53
import javax.swing.AbstractAction;
54
import javax.swing.Action;
55
import javax.swing.ImageIcon;
56
import org.openide.util.lookup.ProxyLookup;
57
58
/**
59
 * An action which operates in the global <i>selection context</i> (a
60
 * Lookup such as the return value of Utilities.actionsGlobalContext()).
61
 * Context actions are sensitive to the presence or absence of a particular
62
 * Java type in a Lookup.  The presence or absence of objects of that type
63
 * can changed based on things like what the user has selected, or what
64
 * logical window contains focus.
65
 * <p/>
66
 * Context actions make it possible to have a global action (i.e. it doesn't
67
 * have to know anything in particular about what it's going to operate on
68
 * to be created) which nonetheless acts on whatever happens to be selected
69
 * at some moment in time.
70
 * <p/>
71
 * This class is a replacement for most uses of NodeAction and CookieAction
72
 * in the Nodes API.
73
 *
74
 * <h4>Using ContextAction in place of NodeAction or CookieAction</h4>
75
 * Replacing CookieAction with ContextAction is fairly straightforward.
76
 * The one use-case which is not supported is writing an action which can
77
 * be enabled in the case of multiple different object types with no
78
 * common superclass (this can still be handled by implementing ContextAwareAction
79
 * on your own subclass of AbstractAction;  you just don't get the convenience
80
 * of this class).
81
 * <p/>
82
 * The most straightforward replacement for <code>CookieAction</code> is
83
 * <code>LookupProviderAction</code>.  It handles one layer of indirection -
84
 * that is, a <code>CookieAction</code> first needs to have at least one <code>Node</code>
85
 * selected;  a <code>Node is itself a <code>Lookup.Provider</code>,
86
 * and so the <code>CookieAction</code>
87
 * is sensitive to some object type in the lookup of one or more selected
88
 * <code>Node</code>s.  So a <code>CookieAction</code> is an action sensitive
89
 * to an object (the cookie) in the lookup of an object in the selection (the node).
90
 * <p/>
91
 * <code>LookupProviderAction</code> accomplishes the same thing, without
92
 * requiring that the second <code>Lookup.Provider</code> be a <code>Node</code>.
93
 * <p>
94
 * CookieAction supported a number of different <i>mode</i>s
95
 * which determined its enablement, based on how many objects were in the
96
 * selection and if there were any lookups in the selection that did <i>not</i>
97
 * contain an instance of the type it was sensitive to.  In <code>LookupProviderAction</code>,
98
 * this distinction is split into its two constituents:  To disallow enablement
99
 * if there is some Lookup.Provider that does <i>not</i> contain the type you
100
 * care about, pass true to the superclass constructor for the <code>all</code>
101
 * parameter.  To control for the number of selected items, override
102
 * <code>checkQuantity(int)</code>.
103
 * <p/>
104
 * It is also possible with <code>ContextAction</code>s to do deeper indirection.
105
 * E.g., say the selection will contain a <code>Node</code>.  <code>Node</code>
106
 * implements <code>Lookup.Provider</code>.  You are interested in <code>Node</code>s
107
 * which have a <code>Project</code> in their lookup, and <code>Project</code>
108
 * is also a <code>Lookup.Provider</code> too.  What you really want to do is
109
 * write an action that is enabled
110
 * <ul><li>When the selection contains a <code>Node</code>
111
 *   <ul><li>which contains a <code>Project</code> in its lookup</code>
112
 *      <ul><li>which contains a <code>Foo</code> in its lookup</code>
113
 *      </ul</li>
114
 *   </ul></li>
115
 * </ul></li>
116
 * The only actually interesting logic here is what you will do with a
117
 * <code>Foo</code> if you can get hold of one.  Handling this is easy:
118
 * <pre>
119
 * ContextAction<Foo> theRealAction = new MyContextAction(); //this is what you wrote
120
 * Action<Node> theGlobalAction = createIndirectAction(Node.class,
121
 *   createIndirectAction(Project.class, theRealAction));
122
 * </pre>
123
 * In the NetBeans IDE, to declaratively install such an action, simply write
124
 * a static method that contains the above code, and reference it in your
125
 * layer file as the <code>methodvalue</code> for your <code>.instance</code>
126
 * or similar file.
127
 * <p/>
128
 * To paraphrase, it's
129
 * <a href="http://en.wikipedia.org/wiki/Turtles_all_the_way_down">lookups
130
 * all the way down</a>.
131
 *
132
 * @see org.openide.util.ContextAwareAction
133
 * @see org.openide.util.Lookup
134
 * @see org.openide.util.Utilities.actionsGlobalContext()
135
 * @author Tim Boudreau
136
 */
137
public abstract class ContextAction<T> extends AbstractAction implements ContextAwareAction, HelpCtx.Provider {
138
    private final Class<T> type;
139
    private final StubListener stubListener = new StubListener();
140
    //A context aware instance which we use internally to trigger
141
    //enabled changes as long as there is at least one property change
142
    //listener attached to us.
143
    //By having the same thing we return from createContextAwareInstance handle
144
    //all internal state, we make it easy to make a survives-focus-change
145
    //subclass just by overriding createStub() to make a stub which retains
146
    //the last usable collection of objects
147
    ActionStub stub;
148
    static boolean unitTest;
149
    
150
    /**
151
     * Create a new ContextAction which will operate on instances of
152
     * type <code>type</code>
153
     * @param type The type this action needs in its context in order to be
154
     * invoked
155
     */
156
    protected ContextAction(Class<T> type) {
157
        this (type, null, null);
158
    }
159
160
    /**
161
     * Create a new ContextAction which will operate on a type <code>type</code>,
162
     * with the specified display name and icon.
163
     *
164
     * @param type The type this action needs in its context in order to be
165
     * invoked
166
     * @param displayName A localized display name
167
     * @param icon An image to use as the action's icon
168
     */
169
    protected ContextAction(Class<T> type, String displayName, Image icon) {
170
        this.type = type;
171
        if (type == null) {
172
            throw new NullPointerException ("Type null");
173
        }
174
        if (displayName != null) {
175
            putValue (Action.NAME, displayName);
176
        }
177
        if (icon != null) {
178
            putValue (Action.SMALL_ICON, new ImageIcon (icon));
179
        }
180
        putValue ("noIconInMenu", true);
181
    }
182
183
    /**
184
     * Whether or not the action is enabled.  By default, determines if there
185
     * are any instances of type <code>type</code> in the selection context
186
     * lookup, and if there are, returns true.  To refine this behavior further,
187
     * override <code>enabled (java.util.Collection)</code>.
188
     * @return
189
     */
190
    @Override
191
    public final boolean isEnabled() {
192
        ActionStub stubAction = getStub();
193
        Collection <? extends T> targets = stubAction.collection();
194
        boolean result = checkQuantity(targets) && stubAction.isEnabled();
195
        return result;
196
    }
197
198
    boolean checkQuantity(Collection<? extends T> targets) {
199
        return checkQuantity (targets.size());
200
    }
201
202
    protected boolean checkQuantity (int numberOfObjects) {
203
        return numberOfObjects > 0;
204
    }
205
206
    /**
207
     * Determine if this action should be enabled.  This method will only be
208
     * called if the size of the collection is > 0.  The default implementation
209
     * returns <code>true</code>.  If you need to do some further
210
     * test on the collection of objects to determine if the action should
211
     * really be enabled or not, override this method do that here.
212
     *
213
     * @param targets A collection of objects of type <code>type</code>
214
     * @return Whether or not the action should be enabled.
215
     */
216
    protected boolean enabled(Collection<? extends T> targets) {
217
        return true;
218
    }
219
220
    /**
221
     * Overridden to throw an UnsupportedOperationException.  Do not call.
222
     * @param newValue
223
     */
224
    @Override
225
    public final void setEnabled(boolean newValue) {
226
        throw new UnsupportedOperationException();
227
    }
228
229
    /**
230
     * Override to actually do whatever this action does.  This method is
231
     * passed the current collection of objects present in the selection
232
     * context.
233
     * @param targets The objects of type <code>type</code>, which
234
     * this action will use to do whatever it does
235
     */
236
    protected abstract void actionPerformed(Collection<? extends T> targets);
237
238
    /**
239
     * Fetches the collection of objects this action will act on and passes
240
     * them to <code>actionPerformed(Collection<? extends T>).
241
     * @param e The action event.  Ignored.
242
     */
243
    public final void actionPerformed(ActionEvent ignored) {
244
        getStub().actionPerformed(null);
245
    }
246
247
    /**
248
     * Create an instance of this action over a particular context.  This is
249
     * used to handle cases such as popup menus, where a popup menu is created
250
     * against whatever the selection is at the time of its creation;  if the
251
     * selection changes <i>while</i> the popup is onscreen, we do not want
252
     * the popup to operate on the new selection; it should operate on the thing
253
     * that the user right-clicked.  So for a popup menu, an instance of this
254
     * action is created over a snapshot-lookup - a snapshot
255
     * of the context at the moment it is created.
256
     * @param actionContext The context this action instance should operate on.
257
     * @return
258
     */
259
    public final Action createContextAwareInstance(Lookup actionContext) {
260
        return createStub (actionContext);
261
    }
262
263
    ActionStub<T> createStub(Lookup actionContext) {
264
        return new ActionStub<T>(actionContext, this);
265
    }
266
267
    private ActionStub<T> createInternalStub () {
268
        //Don't synchronize, just ensure we are only called from sync methods
269
        assert Thread.holdsLock(this);
270
        ActionStub result = (ActionStub)
271
                createContextAwareInstance (Utilities.actionsGlobalContext());
272
        return result;
273
    }
274
275
    private synchronized ActionStub<T> getStub() {
276
        if (stub == null && attached) {
277
            stub = createInternalStub();
278
            stub.addPropertyChangeListener(stubListener);
279
        }
280
        return stub == null ? createInternalStub() : stub;
281
    }
282
    /**
283
     * Get the help context, if any.  Returns <code>HelpCtx.DEFAULT_HELP</code>
284
     * by default.
285
     * @return A help context
286
     */
287
    public HelpCtx getHelpCtx() {
288
        return HelpCtx.DEFAULT_HELP;
289
    }
290
291
    @Override
292
    public String toString() {
293
        return super.toString() + "[type=" + type.getName() + "]";
294
    }
295
296
    @Override
297
    public boolean equals(Object obj) {
298
        if (obj == null) {
299
            return false;
300
        }
301
        if (getClass() != obj.getClass()) {
302
            return false;
303
        }
304
        final ContextAction<T> other = (ContextAction<T>) obj;
305
        if (this.type != other.type && (this.type == null || !this.type.equals(other.type))) {
306
            return false;
307
        }
308
        return true;
309
    }
310
311
    @Override
312
    public int hashCode() {
313
        int hash = 7;
314
        hash = 47 * hash + (this.type != null ? this.type.hashCode() : 0);
315
        return hash;
316
    }
317
318
    @Override
319
    public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
320
        super.addPropertyChangeListener(listener);
321
        int count = getPropertyChangeListeners().length;
322
        if (count == 1) {
323
            addNotify();
324
        }
325
    }
326
327
    @Override
328
    public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
329
        super.removePropertyChangeListener(listener);
330
        int count = getPropertyChangeListeners().length;
331
        if (count == 0) {
332
            removeNotify();
333
        }
334
    }
335
336
    volatile boolean attached;
337
    void addNotify() {
338
        assert Thread.holdsLock(this);
339
        attached = true;
340
        stub = getStub();
341
    }
342
343
    void removeNotify() {
344
        assert Thread.holdsLock(this);
345
        attached = false;
346
        stub.removePropertyChangeListener(stubListener);
347
        stub = null;
348
    }
349
350
    //for unit tests
351
    synchronized Collection<? extends T> stubCollection() {
352
        return stub == null ? null : stub.collection();
353
    }
354
355
    private class StubListener implements PropertyChangeListener {
356
357
        public void propertyChange(PropertyChangeEvent evt) {
358
            if ("enabled".equals(evt.getPropertyName())) { //NOI18N
359
                firePropertyChange (evt.getPropertyName(),
360
                        evt.getOldValue(), evt.getNewValue());
361
            }
362
        }
363
364
    }
365
366
    //Inner stub action class which delegates to the parent action's methods.
367
    //Used both for context aware instances, and for internal state for
368
    //ContextAction instances
369
    private static class ActionStub<T> implements Action, LookupListener {
370
        private final Lookup.Result<T> lkpResult;
371
        private final Map<String, Object> pairs = new HashMap<String, Object>();
372
        private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
373
        private final Lookup context;
374
        protected final ContextAction parent;
375
        protected boolean enabled;
376
377
        ActionStub(Lookup context, ContextAction parent) {
378
            assert context != null;
379
            this.context = context;
380
            this.parent = parent;
381
            lkpResult = context.lookupResult(parent.type);
382
            lkpResult.addLookupListener(this);
383
            if (getClass() == ActionStub.class) {
384
                //avoid superclass call to Retained.collection() before
385
                //it has initialized
386
                enabled = isEnabled();
387
            }
388
        }
389
390
        public Object getValue(String key) {
391
            Object result = pairs.get(key);
392
            if (result == null) {
393
                result = parent.getValue(key);
394
            }
395
            return result;
396
        }
397
398
        Collection<? extends T> collection() {
399
            return lkpResult.allInstances();
400
        }
401
402
        public void putValue(String key, Object value) {
403
            Object old = pairs.put(key, value);
404
            boolean fire = (old == null) != (value == null);
405
            if (fire) {
406
                fire = value != null && !value.equals(old);
407
                if (fire) {
408
                    supp.firePropertyChange (key, old, value);
409
                }
410
            }
411
        }
412
413
        public void setEnabled(boolean b) {
414
            //Will throw exception
415
            parent.setEnabled(b);
416
        }
417
418
        public boolean isEnabled() {
419
            Collection<? extends T> targets = collection();
420
            assert targets != null;
421
            assert parent != null;
422
            return targets.isEmpty() ? false : parent.checkQuantity(targets) &&
423
                    parent.enabled(targets);
424
        }
425
426
        public void addPropertyChangeListener(PropertyChangeListener listener) {
427
            supp.addPropertyChangeListener(listener);
428
        }
429
430
        public void removePropertyChangeListener(PropertyChangeListener listener) {
431
            supp.removePropertyChangeListener(listener);
432
        }
433
434
        public void actionPerformed(ActionEvent e) {
435
            assert isEnabled() : "Not enabled: " + this + " collection " +
436
                    collection() + " of " + parent.type.getName();
437
            Collection<? extends T> targets = collection();
438
            parent.actionPerformed(targets);
439
        }
440
441
        void enabledChanged(final boolean enabled) {
442
            Mutex.EVENT.readAccess(new Runnable() {
443
                public void run() {
444
                    supp.firePropertyChange("enabled", !enabled, enabled); //NOI18N
445
                    if (unitTest) {
446
                        synchronized (parent) {
447
                            parent.notifyAll();
448
                        }
449
                        synchronized (this) {
450
                            this.notifyAll();
451
                        }
452
                    }
453
                }
454
            });
455
        }
456
457
        public void resultChanged(LookupEvent ev) {
458
            boolean old = enabled;
459
            enabled = isEnabled();
460
            if (old != enabled) {
461
                enabledChanged (enabled);
462
            }
463
        }
464
465
        @Override
466
        public String toString() {
467
            return super.toString() + "[name=" + getValue (NAME) + //NOI18N
468
                    "delegating={"+ parent + "} context=" + //NOI18N
469
                    context +"]"; //NOI18N
470
        }
471
472
        @Override
473
        public boolean equals(Object obj) {
474
            if (obj == null) {
475
                return false;
476
            }
477
            if (getClass() != obj.getClass()) {
478
                return false;
479
            }
480
            final ActionStub<T> other = (ActionStub<T>) obj;
481
            if (this.parent != other.parent && (this.parent == null || !this.parent.equals(other.parent))) {
482
                return false;
483
            }
484
            if (this.context != other.context && (this.context == null || !this.context.equals(other.context))) {
485
                return false;
486
            }
487
            return true;
488
        }
489
490
        @Override
491
        public int hashCode() {
492
            int hash = 7;
493
            hash = 59 * hash + (this.context != null ? this.context.hashCode() : 0);
494
            hash = 59 * hash + (this.parent != null ? this.parent.hashCode() : 0);
495
            return hash;
496
        }
497
    }
498
499
    /**
500
     * Subclass of ContextAction which does not support multi-selection -
501
     * like ContextAction, it is sensitive to a particular type.  However,
502
     * it only is enabled if there is exactly one object of type <code>type</code>
503
     * in the selection.
504
     * @param <T> The type this action is sensitive to
505
     */
506
    public static abstract class Single<T> extends ContextAction<T> {
507
        protected Single (Class<T> type) {
508
            super (type);
509
        }
510
511
        protected Single(Class<T> type, String displayName, Image icon) {
512
            super (type, displayName, icon);
513
        }
514
515
        /**
516
         * Delegates to actionePerformed(T)</code> with the first and
517
         * only element of the collection.
518
         * @param targets The objects this action may operate on
519
         */
520
        @Override
521
        protected final void actionPerformed(Collection<? extends T> targets) {
522
            actionPerformed (targets.iterator().next());
523
        }
524
525
        /**
526
         * Actually perform the action.
527
         * @param target The only instance of <code>T</code> in the action
528
         * context.
529
         */
530
        protected abstract void actionPerformed (T target);
531
532
        @Override
533
        protected final boolean checkQuantity(int count) {
534
            return count == 1;
535
        }
536
537
    /**
538
     * Determine if this action should be enabled.  This method will only be
539
     * called if the size of the collection == 1.  The default implementation
540
     * returns <code>true</code>.  If you need to do some further
541
     * test on the collection of objects to determine if the action should
542
     * really be enabled or not, override this method do that here.
543
     *
544
     * @param targets A collection of objects of type <code>type</code>
545
     * @return Whether or not the action should be enabled.
546
     */
547
        @Override
548
        protected boolean enabled(Collection<? extends T> targets) {
549
            //Overridden only in order to have different javadoc
550
            return super.enabled(targets);
551
        }
552
    }
553
554
    /**
555
     * A context action which, once enabled, remains enabled.
556
     * <p/>
557
     * The canonical example of this sort of action in the NetBeans IDE is
558
     * NextErrorAction:  It becomes enabled when the output window gains
559
     * focus.  But it should remain enabled when focus goes back to the
560
     * editor, and still work against whatever context the output window
561
     * gave it to work on.  Such cases are rare but legitimate.
562
     * <p/>
563
     * Use judiciously - such actions are temporary memory
564
     * leaks - the action will retain the last usable collection of
565
     * objects it had to work on as long as there are any property
566
     * change listeners attached to it.
567
     *
568
     * @param <T> The type this object is sensitive to
569
     */
570
    public static abstract class SurviveSelectionChange<T> extends Single<T> {
571
        protected SurviveSelectionChange (Class<T> type) {
572
            super (type);
573
        }
574
575
        protected SurviveSelectionChange(Class<T> type, String displayName, Image icon) {
576
            super (type, displayName, icon);
577
        }
578
579
        @Override
580
        ActionStub<T> createStub(Lookup actionContext) {
581
            return new RetainingStub<T> (actionContext, this);
582
        }
583
584
        @Override
585
        boolean checkQuantity(Collection<? extends T> targets) {
586
            return super.checkQuantity(targets) || stub != null &&
587
                    super.checkQuantity(((RetainingStub) stub).retained);
588
        }
589
590
        private static final class RetainingStub<T> extends ActionStub<T> {
591
            Collection <? extends T> retained;
592
            RetainingStub(Lookup context, SurviveSelectionChange parent) {
593
                super (context, parent);
594
                assert parent != null;
595
                assert context != null;
596
                retained = super.collection();
597
                assert retained != null;
598
                enabled = isEnabled();
599
            }
600
601
            @Override
602
            Collection <? extends T> collection() {
603
                boolean wasEnabled = enabled;
604
                if (wasEnabled) {
605
                    Collection <? extends T> nue = super.collection();
606
                    assert nue != null;
607
                    //If we were enabled, and now we have too many objects,
608
                    //become disabled, don't keep the old single object
609
                    if (!parent.checkQuantity(nue)) {
610
                        retained = nue;
611
                    }
612
                    if (!nue.isEmpty()) {
613
                        retained = nue;
614
                    }
615
                }
616
                return retained;
617
            }
618
        }
619
    }
620
621
    /**
622
     * LookupProviderAction is an action subclass which looks up some
623
     * type which implements Lookup.Provider in the global selection, and then
624
     * actually delegates to some other type in the lookups of the Lookup.Providers
625
     * within.
626
     * <p/>
627
     * The canonical example of this is NodeContextAction, which looks for one or more Nodes
628
     * in the global action context, but what it really needs to operate on
629
     * is some objects that may or may not be in those Nodes' lookups.
630
     * <p/>
631
     * This also handles the case of exclusive multi-selection, where the action
632
     * should only be enabled if like objects are selected.  An example of this
633
     * is OpenAction, which should be enabled if all of the selected files
634
     * own an OpenCookie, but should not be enabled otherwise.
635
     * <p/>
636
     * To do more than one level of indirection of Lookup.Providers, or to
637
     * impose restrictions such as how many instances of a type can be in the
638
     * lookup, use IndirectAction and pass it a subclass of
639
     * ContextAction (such as ContextAction.Single) that does what you want.
640
     * <p/>
641
     * Anything that can be done with this class can be done with
642
     * DelegateAction;  this class just simplifies the common case of one
643
     * layer of indirection.
644
     *
645
     * @param <T> The type of Lookup.Provider to look for in the selection - for
646
     * example, Node.class or Project.class
647
     * @param <R> The type of object this action will actually operate on - for
648
     * example, an OpenCookie instance
649
     */
650
    public static abstract class LookupProviderAction<T extends Lookup.Provider, R> extends ContextAction <T>{
651
        private final Class<R> delegateType;
652
        private final boolean all;
653
        private final ContextAction<R> overDelegateType;
654
        private boolean inDelegate;
655
        /**
656
         * Create a new LookupProviderAction.
657
         * @param type The type of Lookup.Provider to find in the global selection
658
         * @param delegateType The type of object to look for in the lookups of the instances
659
         * of <code>type</code> in the global selection
660
         * @param all If true, all T's in the global selection must contain at least one R
661
         * for this action to be enabled.  If false, it is enabled if there are any R's in
662
         * any of the T's lookups.
663
         */
664
        protected LookupProviderAction (Class<T> type, Class<R> delegateType, boolean all) {
665
            super (type);
666
            assert delegateType != null;
667
            this.delegateType = delegateType;
668
            this.all = all;
669
            overDelegateType = new InternalDelegateAction(delegateType);
670
        }
671
672
        /**
673
         * Create a new LookupProviderAction.
674
         * @param type The type of Lookup.Provider to find in the global selection
675
         * @param delegateType The type of object to look for in the lookups of the instances
676
         * of <code>type</code> in the global selection
677
         * @param all If true, all T's in the global selection must contain at least one R
678
         * for this action to be enabled.  If false, it is enabled if there are any R's in
679
         * any of the T's lookups.
680
         * @param displayName The localized display name
681
         * @param icon The icon
682
         */
683
        protected LookupProviderAction (Class<T> type, Class<R> delegateType, boolean all, String displayName, Image icon) {
684
            super (type, displayName, icon);
685
            assert delegateType != null;
686
            this.delegateType = delegateType;
687
            this.all = all;
688
            overDelegateType = new InternalDelegateAction(delegateType);
689
        }
690
691
        protected final void actionPerformed(Collection<? extends T> targets) {
692
            if (all) {
693
                Collection<? extends R> delegates = delegates(targets);
694
                assert delegates != null && !delegates.isEmpty();
695
            }
696
            Action contextDelegate = overDelegateType.createContextAwareInstance(
697
                    delegateLookup());
698
            assert contextDelegate.isEnabled();
699
            contextDelegate.actionPerformed((ActionEvent) null);
700
        }
701
702
        /**
703
         * Determine if this action should be enabled in the case of
704
         * multi-selection.  By default this method simply returns true
705
         * if lookupProviderCount > 0.  To have an action which only
706
         * is enabled if there is exactly one selected item, override
707
         * and test <code>lookupProviderCount == 1</code>.
708
         * <p/>
709
         * Note that the number passed to this method is the number of
710
         * <i><code>Lookup.Provider</code></i>s in the selection, <b>not</b>
711
         * the number of objects of type R in those Lookup.Providers.
712
         * If you want to control for that, override <code>isEnabled(Collection)</code>.
713
         *
714
         * @param lookupProviderCount The number of Lookup.Providers of type T
715
         * in the selection
716
         * @return true if the action should be enabled
717
         */
718
        @Override
719
        protected boolean checkQuantity(int lookupProviderCount) {
720
            return lookupProviderCount > 0;
721
        }
722
723
        /**
724
         * Creates a ProxyLookup over all selected Lookup.Providers of type T
725
         * @return A lookup that proxies all the lookups of all T's in the
726
         * current global selection
727
         */
728
        private synchronized Lookup delegateLookup() {
729
            inDelegate = true;
730
            try {
731
                List <Lookup> lookups = new LinkedList<Lookup>();
732
                Collection <? extends T> sc = super.getStub().collection();
733
                assert sc != null;
734
                for (Lookup.Provider provider : sc) {
735
                    lookups.add (provider.getLookup());
736
                }
737
                Lookup[] lkps = lookups.toArray (new Lookup[0]);
738
                return new ProxyLookup (lkps);
739
            } finally {
740
                inDelegate = false;
741
            }
742
        }
743
744
        /**
745
         * Get the objects of type R that we really want to operate on.
746
         * @param targets The collection from the lookup of the current selection (which
747
         * is a Lookup.Provider)
748
         * @return A collection of R's.  If all Lookup.Providers in the selection
749
         * must contain an R and one or more do not, returns null.
750
         */
751
        private Set <? extends R> delegates(Collection<? extends T> targets) {
752
            Set <R> realTargets = new HashSet<R>();
753
            for (Lookup.Provider provider : targets) {
754
                Collection <? extends R> delegateCookies =
755
                        provider.getLookup().lookupAll (delegateType);
756
                if (all && delegateCookies.isEmpty()) {
757
                    return null;
758
                } else {
759
                    realTargets.addAll (delegateCookies);
760
                }
761
            }
762
            return realTargets;
763
        }
764
765
        /**
766
         * Actually perform the action on the collection of objects found
767
         * in the Lookup.Provider of type <code>T</code> in the lookup.
768
         * @param targets The collection of objects
769
         */
770
        protected abstract void perform(Collection <? extends R> targets);
771
772
        /**
773
         * Determine if the action should be enabled.  This method is only
774
         * called if the collection is non-empty.  The default implementation
775
         * simply returns true.  Only override this if you really need to
776
         * check the state of the objects in the collection to determine
777
         * if the action can really be performed.
778
         * @param targets The collection of objects found in the
779
         * Lookup.Provider of type <code>T</code> in the selection.
780
         * @return
781
         */
782
        protected boolean isEnabled (Collection<? extends R> targets) {
783
            return true;
784
        }
785
786
        @Override
787
        protected final boolean enabled(Collection<? extends T> targets) {
788
            if (inDelegate) {
789
                //Avoid endless loop when context aware instance also checks
790
                //its enablement to initialize its state
791
                return true;
792
            }
793
            boolean result;
794
            if (all) {
795
                //If all the Lookups must contain at least one R, make
796
                //sure they do
797
                Collection<? extends R> delegates = delegates(targets);
798
                result = delegates != null && !delegates.isEmpty();
799
            } else {
800
                result = true;
801
            }
802
            //Okay, we know we have a selection we can use.  Get an actual
803
            //instance
804
            if (result) {
805
                result = overDelegateType.createContextAwareInstance(delegateLookup()).isEnabled();
806
            }
807
            return result;
808
        }
809
810
        private class InternalDelegateAction extends ContextAction<R> {
811
812
            public InternalDelegateAction(Class<R> type) {
813
                super(type);
814
            }
815
816
            @Override
817
            protected void actionPerformed(Collection<? extends R> targets) {
818
                perform(targets);
819
            }
820
821
            @Override
822
            protected boolean enabled(Collection<? extends R> targets) {
823
                return LookupProviderAction.this.isEnabled(targets);
824
            }
825
826
            @Override
827
            public Object getValue(String key) {
828
                Object result = super.getValue(key);
829
                if (result == null) {
830
                    result = LookupProviderAction.this.getValue(key);
831
                }
832
                return result;
833
            }
834
        }
835
    }
836
837
    /**
838
     * Create an action which looks for a particular type <code>T</code> in
839
     * the selection, where T itself is a subclass <code>Lookup.Provider</code>
840
     * (such as a Node or Project in the NetBeans IDE), and delegates to
841
     * another action which is sensitive to some object in that lookup.
842
     * <p/>
843
     * Such actions can be chained to arbitrarily depths of lookup providers;
844
     * you only need to write a ContextAction that is sensitive to the thing
845
     * you are actually interested in at the end of the chain.
846
     * So, if you want to write an action which is sensitive to, say, a
847
     * ClassPathProvider that belongs to the Project found in the selected
848
     * Node in the global selection, you would do as follows:
849
     * <pre>
850
     * ContextAction<ClassPathProvider> theRealAction = new MyContextAction();
851
     * Action<Node> theGlobalAction = createIndirectAction(Node.class,
852
     *   createIndirectAction(Project.class, theRealAction));
853
     * </pre>
854
     * The returned action will pick up its properties (name, icon, enabled
855
     * state, etc.) from the action passed in to this one, and it will re-fire
856
     * property changes from that one.
857
     * <p>
858
     * If you need more specific control of enablement logic, either override
859
     * <code>enabled()</code> in your <code>ContextAction</code>, or use
860
     * one of the subclasses such as <code>Single</code> or
861
     * <code>SurviveSelectionChange</code> which provide
862
     * specific enablement behavior.
863
     *
864
     * @param <T>
865
     * @param lkpProviderType
866
     * @param theRealAction
867
     * @return
868
     */
869
    public static <T extends Lookup.Provider> ContextAction<T>
870
            createIndirectAction(Class<T> lkpProviderType, ContextAction<?> theRealAction) {
871
        return new IndirectAction (lkpProviderType, theRealAction);
872
    }
873
874
    /**
875
     * A ContextAction which looks for a specific subclass of Lookup.Provider
876
     * in the selection, and then delegates to another ContextAction for that
877
     * type.
878
     * <p/>
879
     * This class can be used to do multiple levels of indirection, simply
880
     * by passing another instance of IndirectAction to the constructor.
881
     * Say that you want to write an action that operates on the
882
     * ClassPathProvider of the Project which the selected Node belongs to.
883
     * This can be written quite simply:
884
     * <pre>
885
     * ContextAction<ClassPathProvider> theRealAction = new MyContextAction();
886
     * Action theGlobalAction = new IndirectAction<Node>(Project.class, new IndirectAction(ClassPathProvider.class, theRealAction));
887
     * </pre>
888
     * <p/>
889
     *
890
     * @param <T> The type of Lookup.Provider to look for in the lookup.
891
     * @param <R> The type that the passed ContextAction is interested in
892
     */
893
    static final class IndirectAction<T extends Lookup.Provider, R> extends ContextAction<T> {
894
        private final ContextAction<R> delegate;
895
        private final PCL pcl = new PCL();
896
        /**
897
         * Create a new IndirectAction.
898
         *
899
         * @param toLookupType The type of Lookup.Provider to find in the
900
         * selection
901
         * @param delegate The action that will look in those Lookup.Providers
902
         * lookups
903
         */
904
        public IndirectAction (Class<T> toLookupType, ContextAction<R> delegate) {
905
            super (toLookupType);
906
            this.delegate = delegate;
907
            delegate.addPropertyChangeListener(WeakListeners.propertyChange(pcl,
908
                    delegate));
909
        }
910
911
        @Override
912
        public Object getValue(String key) {
913
            Object result = super.getValue(key);
914
            if (result == null) {
915
                result = delegate.getValue(key);
916
            }
917
            return result;
918
        }
919
920
        private Lookup delegateLookup(Collection<? extends T> targets) {
921
            List <Lookup> lookups = new LinkedList<Lookup>();
922
            for (Lookup.Provider provider : targets) {
923
                lookups.add (provider.getLookup());
924
            }
925
            Lookup proxy = new ProxyLookup (lookups.toArray(new Lookup[0]));
926
            return proxy;
927
        }
928
929
        @Override
930
        protected void actionPerformed(Collection<? extends T> targets) {
931
            Action delegateStub = delegate.createContextAwareInstance(delegateLookup(targets));
932
            delegateStub.actionPerformed(null);
933
        }
934
935
        @Override
936
        protected boolean enabled(Collection<? extends T> targets) {
937
            Action delegateStub = delegate.createContextAwareInstance(delegateLookup(targets));
938
            return delegateStub.isEnabled();
939
        }
940
941
        private final class PCL implements PropertyChangeListener {
942
            public void propertyChange(PropertyChangeEvent evt) {
943
                firePropertyChange(evt.getPropertyName(), evt.getOldValue(),
944
                        evt.getNewValue());
945
            }
946
        }
947
    }
948
949
}
(-)cfdd8230bdb4 (+677 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 org.openide.util.ContextAction.*;
60
import static org.junit.Assert.*;
61
62
/**
63
 *
64
 * @author Tim Boudreau
65
 */
66
public class ContextActionTest extends NbTestCase {
67
68
    public ContextActionTest(String x) { super (x); }
69
70
    @BeforeClass
71
    public static void setUpClass() throws Exception {
72
    }
73
74
    @AfterClass
75
    public static void tearDownClass() throws Exception {
76
    }
77
78
    private InstanceContent content;
79
    private Lookup lkp;
80
    private static final int TIMEOUT = 100;
81
    
82
83
    @Before
84
    @Override
85
    public void setUp() {
86
        //This will cause ContextAction instances to invoke notifyAll()
87
        //when their enablement changes
88
        ContextAction.unitTest = true;
89
        MockServices.setServices(Provider.class);
90
        ContextGlobalProvider x = Lookup.getDefault().lookup (ContextGlobalProvider.class);
91
        assertNotNull (x);
92
        assertTrue (x instanceof Provider);
93
        Provider p = (Provider) x;
94
        content = p.content;
95
        lkp = p.lkp;
96
        //some sanity checks
97
        setContent ("hello");
98
        assertEquals ("hello", lkp.lookupAll(String.class).iterator().next());
99
        assertEquals ("hello", Utilities.actionsGlobalContext().lookupAll(String.class).iterator().next());
100
        clearContent();
101
        assertEquals (null, lkp.lookup(String.class));
102
        assertEquals (null, Utilities.actionsGlobalContext().lookup(String.class));
103
        assertEquals (0, Utilities.actionsGlobalContext().lookupAll(Object.class).size());
104
    }
105
106
    static Provider instance;
107
    public static class Provider implements ContextGlobalProvider {
108
        private final Lookup lkp;
109
        private final InstanceContent content;
110
        public Provider () {
111
            lkp = new AbstractLookup (content = new InstanceContent());
112
            instance = this;
113
        }
114
115
        public Lookup createGlobalContext() {
116
            return lkp;
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
    public void testNesting() throws Exception {
321
        System.out.println("testNesting");
322
        D d = new D();
323
        Thing thingOne = new Thing ("a");
324
        Thing thingTwo = new Thing ("b");
325
        Thing thingThree = new Thing ("c");
326
        Thing thingFour = new Thing("d");
327
        Thing badThing = new Thing("no");
328
329
        FakeNode node1 = new FakeNode (thingOne);
330
        FakeNode node2 = new FakeNode (thingTwo);
331
        FakeNode node3 = new FakeNode (thingThree);
332
        FakeNode node4 = new FakeNode (thingFour);
333
        FakeNode nodeBad = new FakeNode (badThing);
334
335
        PCL pcl = new PCL();
336
        d.addPropertyChangeListener(pcl);
337
338
        assertFalse (d.isEnabled());
339
        setContent (node1);
340
        assertTrue (d.isEnabled());
341
        synchronized (d) {
342
            d.wait(500);
343
        }
344
        synchronized (pcl) {
345
            pcl.wait(500);
346
        }
347
        pcl.assertEnabledChangedTo(true);
348
349
        clearContent();
350
        synchronized (d) {
351
            d.wait(500);
352
        }
353
        synchronized (pcl) {
354
            pcl.wait(500);
355
        }
356
        pcl.assertEnabledChangedTo(false);
357
358
        assertFalse (d.isEnabled());
359
        setContent (node1, node2, node3, node4);
360
        assertTrue (d.isEnabled());
361
        setContent (node1, node2, node3, node4, nodeBad);
362
        synchronized (d) {
363
            d.wait(500);
364
        }
365
        synchronized (pcl) {
366
            pcl.wait(500);
367
        }
368
        pcl.assertEnabledChangedTo(false);
369
370
        assertFalse (d.isEnabled());
371
372
        setContent (node1, node2, node3, node4);
373
        assertTrue (d.isEnabled());
374
375
        d.actionPerformed((ActionEvent) null);
376
        Set<Thing> expected = new HashSet<Thing>(Arrays.asList(
377
                thingOne, thingTwo, thingThree, thingFour));
378
        d.assertTargets(expected);
379
380
        clearContent();
381
        assertFalse (d.isEnabled());
382
383
        FakeNode doesntHaveAThing = new FakeNode ("Hello");
384
385
        d = new D(true);
386
        assertFalse (d.isEnabled());
387
        setContent (node1);
388
        assertTrue (d.isEnabled());
389
        clearContent();
390
        assertFalse (d.isEnabled());
391
        setContent (node1, node2, node3, node4);
392
        assertTrue (d.isEnabled());
393
        clearContent();
394
        assertFalse (d.isEnabled());
395
        setContent (node1, node2, node3, doesntHaveAThing);
396
        assertFalse(d.isEnabled());
397
    }
398
399
    public void testDeepDelegation () throws Exception {
400
        System.out.println("testDeepDelegation");
401
        E e = new E();
402
        IndirectAction a = new IndirectAction (FakeNode.class,
403
                new IndirectAction(FakeProject.class, e));
404
        Thing thing1 = new Thing ("a");
405
        Thing thing2 = new Thing ("b");
406
        Thing thing3 = new Thing ("c");
407
        Thing thing4 = new Thing("d");
408
        Thing badThing = new Thing("no");
409
410
        FakeProject proj1 = new FakeProject (thing1);
411
        FakeProject proj2 = new FakeProject (thing2);
412
        FakeProject proj3 = new FakeProject (thing3);
413
        FakeProject proj4 = new FakeProject (thing4);
414
        FakeProject projBad = new FakeProject (badThing);
415
416
        FakeNode node1 = new FakeNode (proj1);
417
        FakeNode node2 = new FakeNode (proj2);
418
        FakeNode node3 = new FakeNode (proj3);
419
        FakeNode node4 = new FakeNode (proj4);
420
        FakeNode nodeBad = new FakeNode (projBad);
421
422
        assertFalse (a.isEnabled());
423
        setContent (node1, node2);
424
        assertTrue (a.isEnabled());
425
        setContent (node1, node2, node3);
426
        assertTrue (a.isEnabled());
427
        clearContent();
428
        assertFalse (a.isEnabled());
429
        setContent (node1, node2, node4);
430
        assertTrue (a.isEnabled());
431
        setContent (node1, node2, nodeBad);
432
        assertFalse (a.isEnabled());
433
434
        setContent (node1, node2);
435
        a.actionPerformed((ActionEvent) null);
436
        e.assertTargets(new HashSet<Thing>(Arrays.asList(thing1, thing2)));
437
438
        clearContent();
439
        PCL pcl = new PCL();
440
        a.addPropertyChangeListener(pcl);
441
        setContent (node1, node3);
442
        synchronized (a) {
443
            a.wait(500);
444
        }
445
        synchronized (pcl) {
446
            pcl.wait(500);
447
        }
448
        pcl.assertEnabledChangedTo(true);
449
        clearContent();
450
451
        synchronized (a) {
452
            a.wait(500);
453
        }
454
        synchronized (pcl) {
455
            pcl.wait(500);
456
        }
457
        pcl.assertEnabledChangedTo(false);
458
        setContent (node1, node3);
459
        synchronized (a) {
460
            a.wait(500);
461
        }
462
        synchronized (pcl) {
463
            pcl.wait(500);
464
        }
465
        pcl.assertEnabledChangedTo(true);
466
467
        setContent (nodeBad);
468
        synchronized (a) {
469
            a.wait(500);
470
        }
471
        synchronized (pcl) {
472
            pcl.wait(500);
473
        }
474
        pcl.assertEnabledChangedTo(false);
475
476
    }
477
478
    private class PCL implements PropertyChangeListener {
479
        PropertyChangeEvent evt;
480
481
        public void propertyChange(PropertyChangeEvent evt) {
482
            if ("enabled".equals (evt.getPropertyName())) {
483
                this.evt = evt;
484
            }
485
            synchronized (this) {
486
                notifyAll();
487
            }
488
        }
489
490
        void assertEnabledChangedTo (boolean val) {
491
            PropertyChangeEvent old = this.evt;
492
            this.evt = null;
493
            assertNotNull (old);
494
            Boolean b = Boolean.valueOf(val);
495
            assertEquals (b, old.getNewValue());
496
        }
497
498
        void assertNotFired() {
499
            assertNull (evt);
500
        }
501
    }
502
503
    private static class A extends ContextAction.Single<String> {
504
        String perfString;
505
        A() { super (String.class, "A", null); }
506
507
        @Override
508
        protected void actionPerformed(String target) {
509
            perfString = target;
510
        }
511
512
        void assertPerformed (String expected) {
513
            String old = perfString;
514
            perfString = null;
515
            assertNotNull (old);
516
            assertEquals (expected, old);
517
        }
518
519
        void assertNotPerformed () {
520
            assertNull (perfString);
521
        }
522
    }
523
524
    private static class B extends ContextAction.SurviveSelectionChange<String> {
525
        String perfString;
526
        B() { super (String.class, "B", null); }
527
528
        @Override
529
        protected void actionPerformed(String target) {
530
            perfString = target;
531
        }
532
533
        void assertPerformed (String expected) {
534
            String old = perfString;
535
            perfString = null;
536
            assertNotNull (old);
537
            assertEquals (expected, old);
538
        }
539
    }
540
541
    private static class C extends ContextAction<String> {
542
        Set <String> perfStrings;
543
        C() { super (String.class); }
544
545
        @Override
546
        protected boolean checkQuantity(int numberOfObjects) {
547
            return numberOfObjects == 5;
548
        }
549
550
        void assertPerformed (Set<String> expected) {
551
            Set<String> old = perfStrings;
552
            perfStrings = null;
553
            assertNotNull (old);
554
            assertEquals (expected, old);
555
        }
556
557
        @Override
558
        protected void actionPerformed(Collection<? extends String> targets) {
559
            perfStrings = new HashSet <String> (targets);
560
        }
561
    }
562
563
    private static class FakeNode implements Lookup.Provider {
564
        private final InstanceContent content = new InstanceContent();
565
        private final AbstractLookup lkp = new AbstractLookup (content);
566
        FakeNode (Object... contents) {
567
            content.set(Arrays.asList(contents), null);
568
        }
569
570
        public Lookup getLookup() {
571
            return lkp;
572
        }
573
    }
574
575
    private static class FakeProject implements Lookup.Provider {
576
        private final InstanceContent content = new InstanceContent();
577
        private final AbstractLookup lkp = new AbstractLookup (content);
578
        FakeProject (Object... contents) {
579
            content.set(Arrays.asList(contents), null);
580
        }
581
582
        public Lookup getLookup() {
583
            return lkp;
584
        }
585
    }
586
587
    private static final class Thing {
588
        private final String s;
589
        Thing (String s) {
590
            this.s = s;
591
        }
592
593
        @Override
594
        public boolean equals(Object obj) {
595
            if (obj == null) {
596
                return false;
597
            }
598
            if (getClass() != obj.getClass()) {
599
                return false;
600
            }
601
            final Thing other = (Thing) obj;
602
            if ((this.s == null) ? (other.s != null) : !this.s.equals(other.s)) {
603
                return false;
604
            }
605
            return true;
606
        }
607
608
        @Override
609
        public int hashCode() {
610
            int hash = 3;
611
            hash = 47 * hash + (this.s != null ? this.s.hashCode() : 0);
612
            return hash;
613
        }
614
615
        @Override
616
        public String toString () {
617
            return super.toString() + "[" + s + "]";
618
        }
619
    }
620
621
    private static final class D extends LookupProviderAction<FakeNode, Thing> {
622
        private Set<Thing> performedOn;
623
624
        D() {
625
            this (false);
626
        }
627
        
628
        D(boolean all) {
629
            super (FakeNode.class, Thing.class, all);
630
        }
631
632
        @Override
633
        protected void perform(Collection<? extends Thing> delegates) {
634
            this.performedOn = new HashSet <Thing> (delegates);
635
        }
636
637
        @Override
638
        protected boolean isEnabled(Collection<? extends Thing> targets) {
639
            return !targets.contains(new Thing("no"));
640
        }
641
642
        void assertTargets (Set <Thing> expected) {
643
            Set <Thing> old = performedOn;
644
            performedOn = null;
645
            assertNotNull ("Expected targets " + expected + " but got " +
646
                    old, old);
647
            assertEquals ("Expected targets " + expected + " but got " +
648
                    old, expected, old);
649
        }
650
    }
651
652
    private static final class E extends ContextAction <Thing> {
653
        private Set<Thing> performedOn;
654
        E() {
655
            super (Thing.class);
656
        }
657
658
        @Override
659
        protected void actionPerformed(Collection<? extends Thing> targets) {
660
            this.performedOn = new HashSet<Thing>(targets);
661
        }
662
663
        @Override
664
        protected boolean enabled(Collection<? extends Thing> targets) {
665
            return !targets.contains(new Thing("no"));
666
        }
667
668
        void assertTargets (Set <Thing> expected) {
669
            Set <Thing> old = performedOn;
670
            performedOn = null;
671
            assertNotNull ("Expected targets " + expected + " but got " +
672
                    old, old);
673
            assertEquals ("Expected targets " + expected + " but got " +
674
                    old, expected, old);
675
        }
676
    }
677
}

Return to bug 153442