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 |
} |