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